CineClick ya tiene experimentos rastreados y un modelo campeón versionado y cargable por alias. Antes de servirlo en el módulo 4 queda un riesgo silencioso: el modelo no consume datos crudos, consume features, y esas features habrá que recalcularlas en producción. Si el cálculo de producción difiere en algo del de entrenamiento — un redondeo, un valor por defecto, una ventana temporal — el modelo recibirá números distintos de los que aprendió y sus predicciones se degradarán sin ningún error visible. Este problema se llama training/serving skew, y la industria ha construido una categoría entera de herramientas para atacarlo: los feature stores. En esta lección entenderás qué son, qué problemas resuelven de verdad (skew, point-in-time correctness, reutilización), y — igual de importante — cuándo no los necesitas. Para CineClick tomaremos una decisión deliberadamente modesta que el módulo 4 aprovechará.
Contenido
- Training/serving skew: la corrupción silenciosa
- El caso
ratio_tickets: una feature, dos implementaciones - Qué es un feature store: registro, almacén offline y almacén online
- Point-in-time correctness: el leakage temporal
- Componentes y flujo de un feature store
- Panorama de opciones: Feast y los gestionados
- Mirada crítica: cuándo no lo necesitas (y qué hace CineClick)
- Cierre del módulo 3
Training/serving skew: la corrupción silenciosa
Recapitulemos la cadena de CineClick: preparar_datos limpia el CSV, construir_features (el código de features.py) transforma columnas crudas en las features que el modelo espera, entrenar ajusta el RandomForest. Todo eso ocurre en entrenamiento, con pandas, sobre un fichero histórico.
Ahora imagina el servicio del módulo 4 en producción: llega una petición con los datos crudos de un cliente y hay que responder en milisegundos. Alguien tiene que calcular las features de ese cliente en ese momento, en otro código y otro contexto. Ahí se abre la grieta:
- Dos implementaciones: la de entrenamiento (pandas, batch) y la de serving (quizá Python puro, quizá otro equipo, quizá otro lenguaje). Dos códigos que "hacen lo mismo" divergen tarde o temprano.
- Fallo silencioso: si la feature de producción vale 0.8 donde entrenamiento habría dicho 1.2, no hay excepción, no hay log de error. El modelo predice; simplemente predice peor. Es el peor tipo de bug: el que no avisa.
- Detección tardía: se descubre semanas después, cuando alguien nota que las campañas de retención rinden menos de lo que las métricas offline prometían — si es que alguien lo nota.
Este es el problema número 7 del diagnóstico del módulo 1 ("las features se calculan en el notebook y nadie sabe recrearlas fuera"), y es de los que más modelos ha matado en la industria. El paper clásico de la deuda técnica en ML lo lista entre los patrones más costosos precisamente por su invisibilidad.
El caso ratio_tickets: una feature, dos implementaciones
Hagámoslo concreto. El equipo de retención sugiere una feature nueva con buena pinta: la intensidad de fricción del cliente, ratio_tickets = tickets_soporte / antiguedad_meses — no es lo mismo 6 tickets en 36 meses que 6 tickets en 3.
Laura la implementa en features.py para el entrenamiento:
# features.py (entrenamiento, pandas, batch)
def construir_features(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
# Clientes con antiguedad 0 (alta este mes): evitamos dividir por cero
# tratando la antiguedad como minimo 1 mes
df["ratio_tickets"] = df["tickets_soporte"] / df["antiguedad_meses"].clip(lower=1)
...
return dfMeses después (escenario hipotético pero dolorosamente realista), otro desarrollador implementa el cálculo en el servicio de predicción, leyendo la descripción de la feature en un documento:
# servicio de prediccion (serving, otro fichero, otra persona)
def calcular_ratio_tickets(cliente: dict) -> float:
if cliente["antiguedad_meses"] == 0:
return 0.0 # "si no hay antiguedad, no hay ratio", razona
return cliente["tickets_soporte"] / cliente["antiguedad_meses"]Ambas implementaciones son razonables. Ambas pasan sus tests. Y no calculan lo mismo: para un cliente recién llegado con 3 tickets, entrenamiento decía 3/1 = 3.0 (fricción altísima, señal fuerte de churn) y producción dice 0.0 (fricción nula). Justo en el segmento donde la feature más informa — clientes nuevos con problemas —, el modelo en producción recibe el valor opuesto al que aprendió. Nadie verá un error jamás; solo un modelo que "en producción no funciona tan bien".
Los remedios posibles, de menos a más maquinaria:
- Una única fuente de verdad de código: entrenamiento y serving importan la misma función del mismo paquete. Ninguna feature se implementa dos veces.
- Un feature store: las definiciones de features viven en un sistema central que las calcula y las sirve a ambos mundos.
Veamos la opción 2 en serio antes de decidir.
Qué es un feature store: registro, offline y online
Un feature store es una capa de infraestructura dedicada a gestionar features como activos de primera clase. Sus tres piezas:
- Registro de definiciones: un catálogo central donde cada feature se define una vez — nombre, entidad a la que pertenece (el cliente), tipo, lógica de cálculo, propietario. Es la fuente única de verdad: entrenamiento y serving consumen la misma definición, con lo que el skew de implementación desaparece por construcción. Además habilita la reutilización: si mañana CineClick monta un segundo modelo (por ejemplo, propensión a upgrade), reutiliza
ratio_ticketsen vez de reimplementarla. - Almacén offline: valores históricos de las features, con marca temporal, optimizados para leer en masa. Es de donde se construyen los datasets de entrenamiento — y su superpoder es la point-in-time correctness, que merece su propio apartado.
- Almacén online: los valores actuales de las features por entidad, en un almacén de baja latencia (típicamente Redis o similar). Cuando llega la petición del cliente 4812, el servicio no calcula nada pesado: hace un lookup de milisegundos y obtiene las mismas features, calculadas por la misma lógica, que alimentaron el entrenamiento.
Point-in-time correctness: el leakage temporal
El almacén offline resuelve un segundo problema, más sutil que el skew: el data leakage temporal. Ejemplo con CineClick:
Supón que entrenamos con etiquetas de marzo: abandono=1 significa "este cliente se dio de baja en marzo". Para construir el dataset, alguien consulta tickets_soporte de la base de datos operacional... hoy, en julio. El cliente 4812 se dio de baja en marzo tras abrir 4 tickets furiosos ese mismo mes de marzo, ya en proceso de baja, y luego 2 más en abril peleándose por la devolución. El dataset dice tickets_soporte=6 para un cliente etiquetado con datos de marzo — pero en el momento de la predicción real (febrero, cuando aún se podía retener) tenía 0.
El modelo aprende una regla buenísima en entrenamiento e inútil en producción: "muchos tickets ⇒ churn"... porque muchos de esos tickets son consecuencia de la baja, no señal previa. Métricas offline infladas, rendimiento real decepcionante. Es el equivalente temporal de entrenar con la respuesta filtrada.
La solución se llama point-in-time join (o time-travel join): para cada fila de entrenamiento, el almacén offline recupera el valor que cada feature tenía en la fecha de la etiqueta, y ni un día después:
| id_cliente | fecha_etiqueta | tickets_soporte (hoy) | tickets_soporte (point-in-time) |
|---|---|---|---|
| 4812 | 2026-03-01 | 6 | 0 |
| 1157 | 2026-03-01 | 2 | 2 |
| 3094 | 2026-03-01 | 5 | 4 |
Hacer esto a mano con pandas es posible (merge_asof, cuidado quirúrgico con las fechas) pero es un campo de minas; los feature stores lo dan resuelto porque almacenan el historial con marcas temporales. Es, probablemente, su aportación técnica más valiosa. (Nota honesta sobre nuestro proyecto: clientes_churn.csv es una foto estática sin marcas temporales, así que este riesgo está latente pero no activo; se volverá muy real cuando en el módulo 6 hablemos de reentrenar con datos que llegan continuamente.)
Componentes y flujo
graph TD
D["Definiciones de features<br/>(codigo versionado en Git)"] --> REG["Registro del feature store"]
F["Fuentes de datos<br/>(BD operacional, eventos)"] --> ING["Pipelines de materializacion"]
REG --> ING
ING --> OFF[("Almacen offline<br/>historico + timestamps")]
ING --> ON[("Almacen online<br/>valores actuales, baja latencia")]
OFF -->|"point-in-time join"| TR["Dataset de entrenamiento"]
ON -->|"lookup en ms"| SRV["Servicio de prediccion<br/>(modulo 4)"]
TR --> MOD["Modelo"]
MOD -.->|"mismas features,<br/>misma definicion"| SRVEl flujo completo: las definiciones (en código, versionadas en Git) se registran; los pipelines de materialización calculan los valores desde las fuentes y los escriben en ambos almacenes; entrenamiento lee del offline con point-in-time joins; serving lee del online con lookups. La simetría entrenamiento/serving queda garantizada por construcción — ese es el contrato del feature store.
Panorama de opciones
Sin entrar en tutoriales (no es el objetivo de esta lección), el mapa del terreno:
| Opción | Tipo | Rasgos |
|---|---|---|
| Feast | Open source | El estándar de facto autogestionado; agnóstico de infraestructura (offline sobre tu warehouse/ficheros, online sobre Redis/DynamoDB...); tú operas las piezas |
| SageMaker Feature Store (AWS), Vertex AI Feature Store (GCP), Databricks Feature Store | Gestionados | Integrados con su plataforma; menos operación, más acoplamiento al proveedor |
| Tecton, Hopsworks | Comerciales especializados | Añaden transformaciones gestionadas, features en streaming, SLAs |
Para que la idea de "definición declarativa" se vea con código, así se declara una feature view en Feast — léelo como esquema conceptual, no como tutorial:
# Definicion declarativa: la entidad y sus features, UNA sola vez
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
from datetime import timedelta
cliente = Entity(name="cliente", join_keys=["id_cliente"])
features_churn = FeatureView(
name="features_churn_cliente",
entities=[cliente],
ttl=timedelta(days=30),
schema=[
Field(name="antiguedad_meses", dtype=Int64),
Field(name="horas_semana", dtype=Float32),
Field(name="tickets_soporte", dtype=Int64),
Field(name="ratio_tickets", dtype=Float32),
],
source=FileSource(path="data/features_historico.parquet",
timestamp_field="fecha_evento"),
)A partir de una definición así, el mismo sistema responde get_historical_features(...) (offline, con point-in-time) y get_online_features(...) (online, en milisegundos). La definición es una; los consumidores, dos.
Mirada crítica: cuándo no lo necesitas
Y ahora, la parte que muchos cursos omiten. Un feature store es infraestructura seria: almacén online que operar y pagar, pipelines de materialización que orquestar y vigilar, un framework que aprender, y una fuente nueva de incidencias ("las features de ayer no se materializaron"). Adoptarlo sin necesitarlo es deuda operativa pura — el error simétrico al que este curso lleva combatiendo desde el módulo 1: tan malo es el notebook sin ingeniería como la ingeniería sin problema que la justifique.
| Señales de que LO necesitas | Señales de que NO lo necesitas |
|---|---|
| Varios modelos comparten features y las reimplementan | Un solo modelo (o features disjuntas por modelo) |
| Features caras: agregaciones sobre historial masivo, ventanas temporales, streaming | Features baratas de calcular al vuelo desde la petición |
| Serving online con presupuesto de latencia estricto y features precomputadas | Batch scoring, o online con features derivables del payload |
| Necesitas point-in-time correctness sobre datos que cambian constantemente | Datasets estáticos o con snapshot ya correcto |
| Varios equipos producen/consumen features (descubrimiento, ownership) | Un equipo pequeño con un repo |
Pasemos a CineClick por esta tabla: un modelo, un equipo (Laura, Marc y tú), features que se calculan en microsegundos a partir de las columnas del propio cliente, sin agregaciones de historial ni streaming. Columna derecha en toda regla. Decisión: CineClick no adopta un feature store hoy. Pero el problema del skew es real y hay que cerrarlo igualmente, así que adoptamos el remedio proporcionado — la opción 1 del apartado 2, elevada a regla de proyecto:
features.pyes la única fuente de verdad de las features. Toda transformación de datos crudos a features vive ensrc/cineclick_churn/features.py, dentro del paquete instalable. El pipeline de entrenamiento la importa (ya lo hace, vía el stageconstruir_features), y el servicio de predicción del módulo 4 importará exactamente la misma función — mismo paquete, misma versión, mismo.clip(lower=1). Prohibido reimplementar una feature fuera de ese módulo.
Esto es un "feature store de pobres" perfectamente digno: resuelve el skew de implementación por el mismo mecanismo (definición única) sin coste de infraestructura. No da point-in-time correctness ni almacén online — el día que CineClick tenga tres modelos, features de agregación sobre eventos de visionado en streaming y un requisito de latencia que obligue a precomputar, la tabla de arriba dirá otra cosa y la migración a Feast será natural, porque las definiciones ya están centralizadas y testeadas. La arquitectura correcta no es la más completa, es la proporcionada al problema — revisada cuando el problema cambia.
Errores Comunes y Consejos
- Adoptar un feature store "porque es lo que se hace": si tu caso está en la columna derecha de la tabla, acabas operando Redis y pipelines de materialización para servir features que un
importresolvía. Evalúa señales, no modas. - Lo contrario: negar el problema del skew porque no usas feature store: el skew no se cura ignorándolo. Si no hay store, tiene que haber otra garantía de definición única (paquete compartido como CineClick) y, en el módulo 5, tests que la vigilen.
- Copiar "solo esta función" de
features.pyal servicio "para no depender del paquete entero": es exactamente así como nace la segunda implementación deratio_tickets. La dependencia del paquete es la garantía, no un estorbo. - Construir datasets históricos consultando valores actuales: el leakage temporal del apartado 4. Si tus etiquetas tienen fecha, tus features deben ser las de esa fecha. Con o sin feature store, hazte siempre la pregunta: "¿este valor se conocía en el momento de la predicción?".
- Confundir feature store con "la tabla de features en el warehouse": una tabla compartida ayuda a la reutilización, pero no da point-in-time joins ni serving online ni registro de definiciones. Puede ser suficiente — pero sé consciente de qué te falta.
- Olvidar que las definiciones de features también se versionan: si
ratio_ticketscambia de fórmula, los modelos entrenados con la fórmula vieja siguen esperándola. Las definiciones van en Git (con o sin store) y el lineage del modelo (lección anterior) debe permitir saber con qué versión se entrenó.
Ejercicios
-
Cazar el skew. Además del caso de
antiguedad_meses=0, encuentra otras dos formas sutiles en que la implementación de serving deratio_ticketsdel apartado 2 podría divergir de la de entrenamiento, aun corrigiendo el caso del cero. Pista: piensa en tipos de datos y en el significado detickets_soporteen cada contexto. -
Diagnóstico de leakage temporal. Marc propone añadir la feature
horas_semana_ultimo_mes(media de horas de visionado del último mes) usando la tabla de visionados actual para etiquetas de hace tres meses. Explica (a) qué valor tendría esta feature en entrenamiento para un cliente que se dio de baja hace diez semanas, (b) por qué eso infla las métricas offline, y (c) qué necesitaría el dataset para construir esta feature correctamente. -
¿Feature store sí o no? Para cada escenario, decide con la tabla de señales y justifica en una o dos frases: (a) una fintech con 14 modelos que comparten features de comportamiento transaccional agregadas por ventanas de 1/7/30 días, servidas online con presupuesto de 50 ms; (b) un equipo de dos personas con un modelo batch mensual de propensión a compra sobre un extracto estático del CRM; (c) CineClick dentro de dos años, con cinco modelos (churn, upgrade, recomendación...) que comparten agregados de eventos de visionado en tiempo casi real.
Soluciones
-
Ejemplos de divergencia sutil (hay más): (a) tipos y redondeo — en pandas la división produce
float64a partir de columnas enteras; si el servicio recibe el JSON contickets_soportecomo string ("3") o alguien redondea a 2 decimales "para limpiar la respuesta", los valores dejan de ser idénticos bit a bit y en los cortes de los árboles cerca del umbral la predicción puede cambiar. (b) semántica de la fuente — en entrenamientotickets_soporteviene del CSV histórico (tickets acumulados según el extracto); en producción el servicio podría leer del sistema de soporte en vivo, que quizá cuenta también tickets abiertos-y-cancelados o excluye los de facturación. Mismo nombre de campo, población distinta. La moraleja: el skew no es solo código distinto, también es fuente distinta — por eso la regla de fuente única cubre la función y el contrato de sus entradas (la firma del modelo del registry ayuda con lo segundo). -
(a) Cerca de 0: el cliente lleva diez semanas de baja, así que su visionado "del último mes" (contado desde hoy) es nulo — cuando la etiqueta se generó, era un cliente activo con horas reales. (b) El modelo aprende "horas_ultimo_mes ≈ 0 ⇒ abandono", una regla casi perfecta en el dataset... que en producción no sirve, porque ahí la feature se calcula para clientes aún activos: la métrica offline mide la capacidad de reconocer bajas ya consumadas, no de predecirlas. (c) Historial de visionado con marcas temporales y un cálculo point-in-time: para cada cliente, la media de horas del mes anterior a su fecha de etiqueta. Exactamente el join que un almacén offline da hecho — y que sin él exige un
merge_asofcuidadoso sobre el histórico. -
(a) Sí, claramente: muchos modelos compartiendo features caras (ventanas temporales), serving online con latencia estricta — todas las señales de la columna izquierda a la vez. (b) No: un modelo, batch, datos estáticos, equipo mínimo; una
features.pycompartida y disciplina de fuente única bastan, y el coste operativo del store no se amortiza. (c) Probablemente sí, y es el momento de reevaluar: features de agregación sobre eventos compartidas entre cinco modelos activan las señales de reutilización, coste de cálculo y point-in-time. La ventaja de la decisión tomada hoy es que la migración parte de definiciones ya centralizadas y testeadas enfeatures.py, no de cinco copias divergentes.
Conclusión
El módulo 3 cierra el capítulo de "gestionar lo que se aprende". El experimento churn-cineclick de MLflow guarda cada ejecución con sus parámetros, métricas, commit y hash de datos — la pregunta "¿qué era la v2?" ya no puede volver a existir. El registry convierte al ganador en el modelo churn-cineclick versión 2, promocionado a @champion (recall 0.68, precision 0.55, F1 0.61) con revisión humana documentada y rollback a un alias de distancia. Y las features tienen fuente única: features.py, en el paquete instalable, compartida por entrenamiento y por el serving que viene — con el criterio claro de qué señales justificarían, mañana, un feature store de verdad. Repasa el inventario: código reproducible, datos versionados, pipeline declarado, experimentos con memoria, modelo campeón con nombre, versión y firma, features sin doble implementación. Todo lo que un servicio de producción necesita cargar está identificado y es trazable — models:/churn-cineclick@champion espera a que alguien lo llame. El módulo 4 hace exactamente eso: elegir el patrón de despliegue adecuado (batch, online o streaming), levantar un servicio de predicción con FastAPI que cargue al campeón e importe las features de la fuente única, y empaquetarlo con Docker para que corra igual en cualquier parte. Laura ya tiene modelo; le falta que CineClick pueda usarlo.
Curso de MLOps
Módulo 1: Fundamentos de MLOps
- Qué es MLOps y por qué los modelos mueren en el notebook
- El ciclo de vida de un modelo de ML en producción
- Niveles de madurez MLOps y roles del equipo
- El proyecto del curso: del notebook a producción
Módulo 2: Del notebook al código reproducible
- Estructura de un proyecto de ML: del notebook al paquete
- Entornos reproducibles y gestión de dependencias
- Versionado de datos con DVC
- Pipelines de entrenamiento reproducibles
Módulo 3: Experimentos y registro de modelos
- Tracking de experimentos con MLflow
- Model registry: versionar y promocionar modelos
- Feature stores: cuándo y para qué
Módulo 4: Servir modelos en producción
- Patrones de despliegue: batch, online y streaming
- Un servicio de predicción con FastAPI
- Empaquetado con Docker
- Escalado y despliegue: Kubernetes y serverless
- Optimización de la inferencia: latencia y coste
Módulo 5: Automatización: CI/CD y orquestación
- CI para ML: tests de código, datos y modelos
- CD: automatizar el despliegue del modelo
- Orquestación de pipelines de ML
- Estrategias de release: shadow, canary y A/B
