El dataset de churn de MercaFresh ya está limpio, completo, transformado, codificado y escalado. Con las lecciones anteriores hemos reparado y adaptado lo que había; en esta última lección del módulo damos el salto que más rendimiento suele aportar: crear información nueva. La ingeniería de características (feature engineering) consiste en construir variables que condensen conocimiento del negocio —cuánto hace que un cliente no compra, cuánto vale, si su actividad crece o se apaga— para que el modelo no tenga que descubrirlo por su cuenta. Hay un dicho muy citado en la profesión: los datos y las características fijan el techo de lo que se puede lograr; los algoritmos solo se acercan más o menos a ese techo. Aquí aprenderás a construir features de negocio (con el clásico RFM), interacciones y polinomios, y a seleccionar cuáles conservar. Cerraremos ensamblando el dataset definitivo de churn y el checklist de todo el módulo.
Contenido
- Qué es la ingeniería de características y por qué importa tanto
- Features de negocio: el análisis RFM sobre MercaFresh
- Ratios y tendencias: exprimir lo que ya tienes
- Interacciones y polinomios
- Selección de características: quedarse con lo que aporta
- El dataset final de churn de MercaFresh
- Checklist del preprocesamiento completo
Qué es la ingeniería de características y por qué importa tanto
Una característica (feature) es cada columna que el modelo recibe como entrada. La ingeniería de características es el arte de fabricar columnas nuevas a partir de las existentes, incorporando conocimiento del dominio. ¿Por qué suele aportar más que cambiar de algoritmo?
- Los modelos ven poco por sí mismos. Una regresión logística (04-02) solo combina linealmente sus entradas: si la señal del churn está en el ratio gasto/pedido y le das gasto y pedidos por separado, quizá nunca la encuentre. Si le das el ratio ya calculado, la señal está servida.
- El conocimiento de negocio no está en los datos crudos. Que "un cliente que llevaba pedidos semanales y lleva 45 días sin comprar está en riesgo" lo sabe cualquier gestor de MercaFresh; la tabla de pedidos, por sí sola, no lo dice — hay que destilarlo en una columna.
- Es la palanca más rentable. Cambiar de un algoritmo decente a otro sofisticado suele mejorar unos puntos; una buena feature nueva puede transformar el problema. Por eso los equipos experimentados dedican más tiempo a las features que a los modelos.
La materia prima ya la tenemos de las lecciones anteriores: las agregaciones por cliente de 03-03 fueron el primer paso. Ahora las convertimos en un sistema.
Features de negocio: el análisis RFM sobre MercaFresh
El marco RFM es un clásico del análisis de clientes que encaja como un guante en el churn:
| Letra | Significado | Pregunta | Feature en MercaFresh |
|---|---|---|---|
| R | Recencia (Recency) | ¿Cuánto hace de su última compra? | dias_sin_comprar |
| F | Frecuencia (Frequency) | ¿Cada cuánto compra? | pedidos_por_mes |
| M | Valor monetario (Monetary) | ¿Cuánto gasta? | gasto_medio_pedido |
La intuición de negocio: un cliente que compraba mucho, a menudo y hace poco está sano; uno cuya recencia crece mientras su frecuencia cae se está marchando — exactamente lo que el modelo de churn debe aprender. Construyámoslo desde la tabla de pedidos:
import pandas as pd
import numpy as np
rng = np.random.default_rng(7)
# Tabla de pedidos simulada: 600 pedidos de 80 clientes durante un año
n = 600
pedidos = pd.DataFrame({
"id_cliente": rng.integers(101, 181, size=n),
"fecha": pd.Timestamp("2025-08-24")
+ pd.to_timedelta(rng.integers(0, 365, size=n), unit="D"),
"importe": rng.lognormal(3.4, 0.5, size=n).round(2),
})
FECHA_CORTE = pd.Timestamp("2026-08-24") # fecha fija de análisis (recuerda 03-03)
rfm = pedidos.groupby("id_cliente").agg(
ultima_compra=("fecha", "max"),
primera_compra=("fecha", "min"),
num_pedidos=("importe", "count"),
gasto_total=("importe", "sum"),
)
# R: días desde la última compra
rfm["recencia_dias"] = (FECHA_CORTE - rfm["ultima_compra"]).dt.days
# F: pedidos por mes de vida como cliente (evitamos dividir por 0 con clip)
meses_vida = ((FECHA_CORTE - rfm["primera_compra"]).dt.days / 30).clip(lower=1)
rfm["pedidos_por_mes"] = (rfm["num_pedidos"] / meses_vida).round(2)
# M: gasto medio por pedido
rfm["gasto_medio_pedido"] = (rfm["gasto_total"] / rfm["num_pedidos"]).round(2)
print(rfm[["recencia_dias", "pedidos_por_mes", "gasto_medio_pedido"]].head())Tres detalles de oficio:
- La fecha de corte fija (no
now()) hace el cálculo reproducible, como vimos en 03-03. - El
clip(lower=1)evita divisiones por cero con clientes de un solo día: las features nuevas también pueden fabricar valores imposibles, y las reglas de validación de 03-01 se les aplican igual. - Cada feature RFM tiene una lectura de negocio directa: cuando el modelo diga "la recencia es lo que más pesa", el equipo de retención sabrá exactamente qué hacer. Las buenas features son también un canal de comunicación con el negocio.
Una variante muy usada es convertir R, F y M en puntuaciones de 1 a 5 con qcut (¡el binning de 03-03!) y hablar de "clientes 5-5-5" o "1-1-3"; la segmentación fina de estos perfiles la haremos con clustering en el módulo 5 y en el proyecto 09-05.
Ratios y tendencias: exprimir lo que ya tienes
Ratios: relaciones que las columnas sueltas no cuentan
Un ratio pone en relación dos magnitudes y a menudo dice más que ambas por separado:
# Ticket medio: ¿compras grandes y espaciadas, o pequeñas y frecuentes?
rfm["gasto_por_pedido"] = rfm["gasto_total"] / rfm["num_pedidos"]
# Intensidad: ¿qué fracción de su vida como cliente lleva inactivo?
vida_dias = (FECHA_CORTE - rfm["primera_compra"]).dt.days.clip(lower=1)
rfm["ratio_inactividad"] = (rfm["recencia_dias"] / vida_dias).round(3)ratio_inactividad es una joya para el churn: 60 días sin comprar son alarmantes en un cliente semanal (ratio alto) y normales en uno trimestral (ratio bajo). El ratio codifica ese contexto; las columnas sueltas, no.
Tendencia de actividad: ¿crece o se apaga?
El churn es un proceso, no un instante: antes de irse, el cliente se enfría. Una feature de tendencia lo captura comparando su actividad reciente con la anterior:
ultimo_trimestre = FECHA_CORTE - pd.Timedelta(days=90)
trimestre_previo = FECHA_CORTE - pd.Timedelta(days=180)
ped_reciente = (pedidos[pedidos["fecha"] >= ultimo_trimestre]
.groupby("id_cliente").size().rename("pedidos_ult_90d"))
ped_previo = (pedidos[(pedidos["fecha"] >= trimestre_previo)
& (pedidos["fecha"] < ultimo_trimestre)]
.groupby("id_cliente").size().rename("pedidos_prev_90d"))
rfm = rfm.join(ped_reciente).join(ped_previo)
rfm[["pedidos_ult_90d", "pedidos_prev_90d"]] = (
rfm[["pedidos_ult_90d", "pedidos_prev_90d"]].fillna(0)) # sin pedidos = 0 (03-02)
# Tendencia: >1 acelera, ~1 estable, <1 se apaga (+1 para evitar 0/0)
rfm["tendencia"] = ((rfm["pedidos_ult_90d"] + 1)
/ (rfm["pedidos_prev_90d"] + 1)).round(2)Un cliente con tendencia = 0.25 (de 8 pedidos a 1) es un candidato a churn de libro aunque su gasto histórico sea alto. Fíjate en cómo reaparecen las lecciones anteriores: fillna(0) es una imputación con significado de negocio (03-02) y el +1 es el mismo truco de estabilidad que log1p (03-03).
Interacciones y polinomios
A veces la señal está en la combinación de dos variables: el efecto del gasto sobre el churn puede depender del plan (perder 50 €/mes de un cliente básico no es lo mismo que de un premium). Una interacción es simplemente el producto de dos features; un polinomio añade además potencias (x², x³) para capturar curvaturas.
PolynomialFeatures las genera mecánicamente:
from sklearn.preprocessing import PolynomialFeatures
X = rfm[["recencia_dias", "pedidos_por_mes"]].head(3)
pf = PolynomialFeatures(degree=2, include_bias=False)
X_pol = pf.fit_transform(X)
print(pf.get_feature_names_out())
# ['recencia_dias' 'pedidos_por_mes' 'recencia_dias^2'
# 'recencia_dias pedidos_por_mes' 'pedidos_por_mes^2']De 2 columnas salen 5: las originales, sus cuadrados y su producto. Dos advertencias y una preferencia:
- Explosión combinatoria: con 20 features y
degree=2salen 230 columnas; condegree=3, más de 1.700.interaction_only=Truelimita a los productos, pero aun así crece rápido. - Más columnas fabricadas mecánicamente = más riesgo de que el modelo memorice ruido (el overfitting que formalizaremos en 06-05) y más necesidad de selección (siguiente apartado) o de regularización (07-01).
- Preferencia de oficio: antes que polinomios a granel, pocas interacciones elegidas con la cabeza.
recencia_dias * pedidos_por_mes("cuánto silencio acumula un cliente habitualmente activo") vale más que veinte productos ciegos.
Selección de características: quedarse con lo que aporta
Crear features es fácil; el peligro es acabar con 80 columnas donde 50 son ruido, redundancia o peso muerto. Más features no siempre es mejor: encarecen el entrenamiento, dificultan la interpretación y dan al modelo más cuerda para ajustarse al azar. Tres filtros introductorios, de más simple a más formal:
- Varianza casi nula
Una columna que apenas varía no puede distinguir a nadie. Si es_cliente_activo vale 1 para el 99,5 % de las filas, no aporta:
from sklearn.feature_selection import VarianceThreshold
vt = VarianceThreshold(threshold=0.01) # elimina columnas casi constantes
X_filtrado = vt.fit_transform(X_numerico)
print(vt.get_support()) # máscara de columnas conservadas(Ojo: el umbral depende de la escala, así que se aplica sobre datos comparables — otro punto para el escalado de 03-05.)
- Filtrado por correlación
Dos usos de la matriz de correlación de 02-03, ahora como herramienta de selección:
- Con el objetivo: features con correlación ~0 con el churn son candidatas a salir (con la cautela de 02-03: Pearson solo ve relaciones lineales).
- Entre sí: dos features con correlación 0,95 entre ellas (como
gasto_totalynum_pedidossuelen tener) son casi la misma información dos veces; conservar una simplifica sin perder señal.
corr = X_numerico.corr(numeric_only=True)
# Parejas de features muy correlacionadas entre sí (redundancia)
alta = (corr.abs() > 0.9) & (corr.abs() < 1.0)
print([(a, b) for a in corr.columns for b in corr.columns
if alta.loc[a, b] and a < b])Es literalmente el ranking de correlaciones con el churn que hicimos al final de 02-03, aplicado ahora a decidir qué columnas viven.
SelectKBest: el filtro estadístico
SelectKBest: el filtro estadísticoSelectKBest puntúa cada feature con un test estadístico respecto al objetivo y se queda con las k mejores:
from sklearn.feature_selection import SelectKBest, f_classif
skb = SelectKBest(score_func=f_classif, k=5)
X_top = skb.fit_transform(X_numerico, y_churn)
print(dict(zip(X_numerico.columns, skb.scores_.round(1))))
print(X_numerico.columns[skb.get_support()].tolist()) # las 5 elegidasf_classif es, en esencia, un contraste de hipótesis como los de 02-04: ¿difiere la media de esta feature entre los clientes que se van y los que se quedan? Puntuación alta = diferencia clara = feature prometedora. Es un método de filtrado (evalúa cada feature aislada, sin considerar combinaciones): rápido y útil como criba inicial, no como veredicto final. Y como todo lo que aprende del objetivo, se ajusta solo con datos de entrenamiento — a estas alturas del módulo, el reflejo ya debería ser automático.
Existen familias más sofisticadas (selección envolvente, PCA como reducción de dimensionalidad en 05-03, regularización Lasso como selección implícita en 07-01); para cerrar el preprocesamiento, estos tres filtros bastan.
El dataset final de churn de MercaFresh
Hora de ensamblar el trabajo de todo el módulo en la tabla definitiva, una fila por cliente:
| Feature | Origen | Lección |
|---|---|---|
edad, ciudad_* (one-hot), plan_cod |
Tabla de clientes limpia y codificada | 03-01, 03-04 |
satisfaccion + satisfaccion_ausente |
Imputación con indicador (MNAR) | 03-02 |
log_gasto_total |
Transformación logarítmica | 03-03 |
recencia_dias, pedidos_por_mes, gasto_medio_pedido |
RFM | 03-06 |
ratio_inactividad, tendencia |
Ratios y tendencia | 03-06 |
recencia_x_frecuencia |
Interacción elegida a mano | 03-06 |
churn (0/1) |
Variable objetivo: sin compras en 90 días | Definida en 01-05 |
flowchart TD
A["Clientes crudos"] -->|"03-01 limpieza"| B["Clientes limpios"]
B -->|"03-02 nulos"| C["Clientes completos"]
P["Pedidos crudos"] -->|"03-03 agregacion"| D["RFM + ratios + tendencia<br/>(03-06)"]
C --> E["merge por id_cliente"]
D --> E
E -->|"03-03 transformar<br/>03-04 codificar<br/>03-05 escalar"| F["Matriz numerica"]
F -->|"03-06 seleccion"| G["Dataset final de churn"]
Y el preprocesamiento completo, como objeto único ejecutable — el ColumnTransformer que hemos ido rellenando lección a lección, ya con todas sus ramas:
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import (OneHotEncoder, OrdinalEncoder,
PowerTransformer, RobustScaler)
rama_numerica = Pipeline([
("imputar", SimpleImputer(strategy="median", add_indicator=True)), # 03-02
("desasimetrizar", PowerTransformer(method="yeo-johnson")), # 03-03
("escalar", RobustScaler()), # 03-05
])
preprocesador = ColumnTransformer([
("num", rama_numerica,
["edad", "satisfaccion", "recencia_dias", "pedidos_por_mes",
"gasto_medio_pedido", "ratio_inactividad", "tendencia"]),
("cat_nominal", OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
["ciudad"]), # 03-04
("cat_ordinal", OrdinalEncoder(categories=[["basico", "estandar", "premium"]]),
["plan"]), # 03-04
])
# En el módulo 4: preprocesador.fit_transform(X_train) -> alimentar al modeloEste objeto es el resumen ejecutable del módulo: un solo fit sobre entrenamiento aprende medianas, lambdas, cuartiles y categorías; un solo transform prepara cualquier cliente futuro. Este dataset —estas features, este preprocesador— es el que usaremos conceptualmente durante el resto del curso cada vez que hablemos del churn de MercaFresh.
Checklist del preprocesamiento completo
La lista de control del módulo, en orden de aplicación:
- Auditar (03-01):
info(),describe(),nunique(); entender qué representa cada fila. - Limpiar (03-01): duplicados exactos y lógicos; reglas de validez para valores imposibles; normalizar texto y categorías; corregir tipos (
to_numeric,to_datetime); diagnosticar outliers (¿error o realidad?). - Tratar nulos (03-02): diagnosticar mecanismo (MCAR/MAR/MNAR); eliminar solo con seguridad; imputar (mediana por defecto, por grupos si hay estructura); indicadores de ausencia si la ausencia informa.
- Transformar (03-03): corregir asimetrías (
log1p, Yeo-Johnson); discretizar si conviene; descomponer fechas; agregar por entidad hasta la granularidad del problema. - Crear features (03-06): RFM, ratios, tendencias, interacciones elegidas con criterio de negocio.
- Codificar (03-04): ordinal con orden explícito para ordinales reales; one-hot para nominales; cuidado extremo con target encoding.
- Escalar (03-05): según el algoritmo;
RobustScalersi hay outliers legítimos. - Seleccionar (03-06): varianza casi nula, redundancia por correlación,
SelectKBestcomo criba. - Transversal: todo paso que aprende parámetros hace
fitsolo sobre entrenamiento; encapsular enPipeline/ColumnTransformer; documentar cada decisión.
(El orden 5→6→7 no es casual: las features se crean sobre valores legibles, luego se codifican, y el escalado va al final porque cada columna nueva también necesita escala.)
Errores Comunes y Consejos
- Crear features usando el futuro. Si el churn se mide a 90 días vista, ninguna feature puede calcularse con datos posteriores a la fecha de corte: es la fuga de información en su versión temporal, la más sutil de todas.
- Fabricar cientos de features mecánicas y ninguna de negocio.
PolynomialFeatures(degree=3)no sustituye a una conversación con el equipo de retención de MercaFresh. Las mejores features nacen de preguntas de negocio. - No validar las features nuevas. Un ratio puede generar infinitos (división por cero) o valores absurdos: pásale a cada feature nueva la misma auditoría de 03-01 (
describe(), histograma). - Conservar features gemelas.
gasto_total,num_pedidosygasto_medio_pedidojuntas son parcialmente redundantes: revisa la matriz de correlación entre features, no solo contra el objetivo. - Seleccionar features con todo el dataset.
SelectKBestmira el objetivo: si se ajusta con datos que luego serán de test, la evaluación queda contaminada.fiten train, siempre. - No documentar el diccionario de datos. Dentro de tres meses nadie recordará cómo se calculó
tendencia. Mantén una tabla feature → definición → fórmula → lección/fecha. - Consejo: cada vez que se te ocurra una feature, escribe primero en una frase qué historia de negocio cuenta ("cuánto silencio acumula un cliente activo"). Si no sabes formular la frase, probablemente tampoco sea buena feature.
Ejercicios
Ejercicio 1
Propón (sin código) tres features nuevas para el problema de demanda de MercaFresh (predecir cuántas unidades de cada producto se venderán mañana), inspiradas en las técnicas de esta lección. Para cada una, escribe la frase de negocio que cuenta.
Ejercicio 2
Con la tabla rfm de la lección, crea la feature valor_en_riesgo = gasto_medio_pedido * pedidos_por_mes * ratio_inactividad y explica qué mide en términos de negocio. Después calcula su correlación de Spearman con recencia_dias y razona si esperabas ese signo.
Ejercicio 3
Tienes 12 features numéricas y el objetivo churn. Escribe el código que: (a) elimine las de varianza < 0,01; (b) de las restantes, seleccione las 6 mejores con SelectKBest y f_classif; (c) imprima cuáles sobrevivieron. Indica sobre qué subconjunto de datos harías los fit.
Soluciones
Ejercicio 1
Respuestas orientativas (hay muchas válidas):
ventas_media_ult_4_semanas(agregación temporal): "cuánto se ha vendido últimamente es el mejor punto de partida para mañana".es_visperas_festivo(componente de calendario, 03-03): "antes de un festivo, las compras de frescos se disparan".ratio_precio_vs_categoria(ratio): "un producto temporalmente más barato que su categoría roba ventas a sus sustitutos".
Lo importante: cada feature tiene fórmula clara y una historia de negocio en una frase.
Ejercicio 2
rfm["valor_en_riesgo"] = (rfm["gasto_medio_pedido"]
* rfm["pedidos_por_mes"]
* rfm["ratio_inactividad"]).round(2)
print(rfm["valor_en_riesgo"].corr(rfm["recencia_dias"], method="spearman"))Qué mide: el gasto mensual habitual del cliente (ticket × frecuencia) ponderado por su grado de inactividad — es decir, cuántos euros al mes están en peligro de perderse. Un cliente valioso y muy inactivo puntúa alto; uno barato o plenamente activo, bajo. Es una feature de priorización: a quién llamar primero. La correlación con recencia_dias sale positiva, como cabía esperar: la recencia entra en el numerador de ratio_inactividad, así que a más días sin comprar, más valor en riesgo — Spearman (02-03) es la elección correcta porque la relación es monótona pero no lineal.
Ejercicio 3
from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif
# Ambos fit SOLO sobre el conjunto de entrenamiento (X_train, y_train):
# VarianceThreshold no mira el objetivo, pero SelectKBest sí — y la coherencia
# del flujo exige ajustar todo el preprocesamiento con train.
vt = VarianceThreshold(threshold=0.01)
X_var = vt.fit_transform(X_train)
cols_var = X_train.columns[vt.get_support()]
skb = SelectKBest(score_func=f_classif, k=6)
X_sel = skb.fit_transform(X_var, y_train)
print(cols_var[skb.get_support()].tolist())
# A X_test se le aplican los MISMOS objetos ya ajustados:
# X_test_sel = skb.transform(vt.transform(X_test))Ejercicio de reflexión adicional: ¿por qué SelectKBest con todo el dataset contaminaría la evaluación? Porque puntúa cada feature según su relación con el objetivo; si ve las etiquetas de test, la selección resultante está optimizada también para test, y la evaluación posterior ya no mide generalización real.
Conclusión
Has cerrado el módulo con su lección más creativa: qué es la ingeniería de características y por qué fija el techo del modelo, cómo construir features de negocio con el marco RFM (recencia, frecuencia, valor), ratios y tendencias que capturan el enfriamiento de un cliente, interacciones y polinomios con mesura, y tres filtros de selección —varianza casi nula, correlación y SelectKBest— para quedarte con lo que aporta. Y sobre todo, has ensamblado el resultado de las seis lecciones en un producto concreto: el dataset final de churn de MercaFresh y su preprocesador ejecutable, con el checklist de nueve pasos como mapa de todo el módulo.
Ese dataset ya no es una tabla sucia exportada de un CRM: es una matriz numérica, completa, escalada y cargada de conocimiento de negocio, lista para entrenar. En el módulo 4 por fin llega el momento que llevamos tres módulos preparando: los algoritmos de aprendizaje supervisado, empezando por la regresión lineal — el modelo más simple y el mejor lugar para entender, de verdad, qué significa que una máquina aprenda de los datos de MercaFresh.
Curso de Machine Learning
Módulo 1: Introducción al Machine Learning
- ¿Qué es el Machine Learning?
- Historia y evolución del Machine Learning
- Tipos de Machine Learning
- Aplicaciones del Machine Learning
- El flujo de trabajo de un proyecto de Machine Learning
Módulo 2: Fundamentos de Estadística y Probabilidad
- Conceptos básicos de estadística
- Distribuciones de probabilidad
- Correlación y covarianza
- Inferencia estadística
- Teorema de Bayes
Módulo 3: Preprocesamiento de Datos
- Limpieza de datos
- Manejo de datos faltantes
- Transformación de datos
- Codificación de variables categóricas
- Normalización y estandarización
- Ingeniería de características
Módulo 4: Algoritmos de Machine Learning Supervisado
- Regresión lineal
- Regresión logística
- Árboles de decisión
- Máquinas de soporte vectorial (SVM)
- K-Vecinos más cercanos (K-NN)
- Naive Bayes
- Redes neuronales
Módulo 5: Algoritmos de Machine Learning No Supervisado
- Clustering: K-means
- Clustering jerárquico
- Análisis de componentes principales (PCA)
- Análisis de agrupamiento DBSCAN
- Visualización de datos con t-SNE y UMAP
Módulo 6: Evaluación y Validación de Modelos
- División de datos: entrenamiento, validación y prueba
- Métricas de evaluación
- Validación cruzada
- Curva ROC y AUC
- Overfitting y underfitting
Módulo 7: Técnicas Avanzadas y Optimización
- Regularización: Ridge, Lasso y Elastic Net
- Ensemble Learning
- Gradient Boosting
- Redes neuronales profundas (Deep Learning)
- Optimización de hiperparámetros
Módulo 8: Implementación y Despliegue de Modelos
- Frameworks y bibliotecas populares
- Implementación de modelos en producción
- Mantenimiento y monitoreo de modelos
- Consideraciones éticas y de privacidad
Módulo 9: Proyectos Prácticos
- Proyecto 1: Predicción de precios de viviendas
- Proyecto 2: Clasificación de imágenes
- Proyecto 3: Análisis de sentimientos en redes sociales
- Proyecto 4: Detección de fraudes
- Proyecto 5: Segmentación de clientes
