CineClick ja té experiments rastrejats i un model campió versionat i carregable per alias. Abans de servir-lo al mòdul 4 queda un risc silenciós: el model no consumeix dades crues, consumeix features, i aquestes features caldrà recalcular-les en producció. Si el càlcul de producció difereix en alguna cosa del de l'entrenament — un arrodoniment, un valor per defecte, una finestra temporal — el model rebrà números diferents dels que va aprendre i les seves prediccions es degradaran sense cap error visible. Aquest problema es diu training/serving skew, i la indústria ha construït una categoria sencera d'eines per atacar-lo: els feature stores. En aquesta lliçó entendràs què són, quins problemes resolen de debò (skew, point-in-time correctness, reutilització), i — igual d'important — quan no els necessites. Per a CineClick prendrem una decisió deliberadament modesta que el mòdul 4 aprofitarà.
Contingut
- Training/serving skew: la corrupció silenciosa
- El cas
ratio_tickets: una feature, dues implementacions - Què és un feature store: registre, magatzem offline i magatzem online
- Point-in-time correctness: el leakage temporal
- Components i flux d'un feature store
- Panorama d'opcions: Feast i els gestionats
- Mirada crítica: quan no el necessites (i què fa CineClick)
- Tancament del mòdul 3
Training/serving skew: la corrupció silenciosa
Recapitulem la cadena de CineClick: preparar_dades neteja el CSV, construir_features (el codi de features.py) transforma columnes crues en les features que el model espera, entrenar ajusta el RandomForest. Tot això passa en entrenament, amb pandas, sobre un fitxer històric.
Ara imagina el servei del mòdul 4 en producció: arriba una petició amb les dades crues d'un client i cal respondre en mil·lisegons. Algú ha de calcular les features d'aquest client en aquell moment, en un altre codi i un altre context. Aquí s'obre l'escletxa:
- Dues implementacions: la de l'entrenament (pandas, batch) i la del serving (potser Python pur, potser un altre equip, potser un altre llenguatge). Dos codis que "fan el mateix" divergeixen tard o d'hora.
- Fallada silenciosa: si la feature de producció val 0.8 on l'entrenament hauria dit 1.2, no hi ha excepció, no hi ha log d'error. El model prediu; simplement prediu pitjor. És el pitjor tipus de bug: el que no avisa.
- Detecció tardana: es descobreix setmanes després, quan algú nota que les campanyes de retenció rendeixen menys del que les mètriques offline prometien — si és que algú ho nota.
Aquest és el problema número 7 del diagnòstic del mòdul 1 ("les features es calculen al notebook i ningú no sap recrear-les fora"), i és dels que més models ha matat a la indústria. El paper clàssic del deute tècnic en ML el llista entre els patrons més costosos precisament per la seva invisibilitat.
El cas ratio_tickets: una feature, dues implementacions
Fem-ho concret. L'equip de retenció suggereix una feature nova amb bona pinta: la intensitat de fricció del client, ratio_tickets = tiquets_suport / antiguitat_mesos — no és el mateix 6 tiquets en 36 mesos que 6 tiquets en 3.
Laura la implementa a features.py per a l'entrenament:
# features.py (entrenament, pandas, batch)
def construir_features(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
# Clients amb antiguitat 0 (alta aquest mes): evitem dividir per zero
# tractant l'antiguitat com a mínim 1 mes
df["ratio_tickets"] = df["tiquets_suport"] / df["antiguitat_mesos"].clip(lower=1)
...
return dfMesos després (escenari hipotètic però dolorosament realista), un altre desenvolupador implementa el càlcul al servei de predicció, llegint la descripció de la feature en un document:
# servei de predicció (serving, un altre fitxer, una altra persona)
def calcular_ratio_tickets(client: dict) -> float:
if client["antiguitat_mesos"] == 0:
return 0.0 # "si no hi ha antiguitat, no hi ha ràtio", raona
return client["tiquets_suport"] / client["antiguitat_mesos"]Totes dues implementacions són raonables. Totes dues passen els seus tests. I no calculen el mateix: per a un client acabat d'arribar amb 3 tiquets, l'entrenament deia 3/1 = 3.0 (fricció altíssima, senyal fort de churn) i producció diu 0.0 (fricció nul·la). Just en el segment on la feature més informa — clients nous amb problemes —, el model en producció rep el valor oposat al que va aprendre. Ningú no veurà mai un error; només un model que "en producció no funciona tan bé".
Els remeis possibles, de menys a més maquinària:
- Una única font de veritat de codi: entrenament i serving importen la mateixa funció del mateix paquet. Cap feature no s'implementa dues vegades.
- Un feature store: les definicions de features viuen en un sistema central que les calcula i les serveix a tots dos mons.
Vegem l'opció 2 seriosament abans de decidir.
Què és un feature store: registre, offline i online
Un feature store és una capa d'infraestructura dedicada a gestionar features com a actius de primera classe. Les seves tres peces:
- Registre de definicions: un catàleg central on cada feature es defineix una vegada — nom, entitat a la qual pertany (el client), tipus, lògica de càlcul, propietari. És la font única de veritat: entrenament i serving consumeixen la mateixa definició, amb la qual cosa l'skew d'implementació desapareix per construcció. A més, habilita la reutilització: si demà CineClick munta un segon model (per exemple, propensió a upgrade), reutilitza
ratio_ticketsen lloc de reimplementar-la. - Magatzem offline: valors històrics de les features, amb marca temporal, optimitzats per llegir en massa. És d'on es construeixen els datasets d'entrenament — i el seu superpoder és la point-in-time correctness, que mereix el seu propi apartat.
- Magatzem online: els valors actuals de les features per entitat, en un magatzem de baixa latència (típicament Redis o similar). Quan arriba la petició del client 4812, el servei no calcula res de pesat: fa un lookup de mil·lisegons i obté les mateixes features, calculades per la mateixa lògica, que van alimentar l'entrenament.
Point-in-time correctness: el leakage temporal
El magatzem offline resol un segon problema, més subtil que l'skew: el data leakage temporal. Exemple amb CineClick:
Suposa que entrenem amb etiquetes de març: abandonament=1 significa "aquest client es va donar de baixa al març". Per construir el dataset, algú consulta tiquets_suport de la base de dades operacional... avui, al juliol. El client 4812 es va donar de baixa al març després d'obrir 4 tiquets furiosos aquell mateix mes de març, ja en procés de baixa, i després 2 més a l'abril barallant-se per la devolució. El dataset diu tiquets_suport=6 per a un client etiquetat amb dades de març — però en el moment de la predicció real (febrer, quan encara es podia retenir) en tenia 0.
El model aprèn una regla boníssima en entrenament i inútil en producció: "molts tiquets ⇒ churn"... perquè molts d'aquests tiquets són conseqüència de la baixa, no senyal previ. Mètriques offline inflades, rendiment real decebedor. És l'equivalent temporal d'entrenar amb la resposta filtrada.
La solució es diu point-in-time join (o time-travel join): per a cada fila d'entrenament, el magatzem offline recupera el valor que cada feature tenia en la data de l'etiqueta, i ni un dia després:
| id_client | data_etiqueta | tiquets_suport (avui) | tiquets_suport (point-in-time) |
|---|---|---|---|
| 4812 | 2026-03-01 | 6 | 0 |
| 1157 | 2026-03-01 | 2 | 2 |
| 3094 | 2026-03-01 | 5 | 4 |
Fer això a mà amb pandas és possible (merge_asof, cura quirúrgica amb les dates) però és un camp de mines; els feature stores ho donen resolt perquè emmagatzemen l'historial amb marques temporals. És, probablement, la seva aportació tècnica més valuosa. (Nota honesta sobre el nostre projecte: clients_churn.csv és una foto estàtica sense marques temporals, així que aquest risc està latent però no actiu; es tornarà molt real quan al mòdul 6 parlem de reentrenar amb dades que arriben contínuament.)
Components i flux
graph TD
D["Definicions de features<br/>(codi versionat a Git)"] --> REG["Registre del feature store"]
F["Fonts de dades<br/>(BD operacional, esdeveniments)"] --> ING["Pipelines de materialització"]
REG --> ING
ING --> OFF[("Magatzem offline<br/>històric + timestamps")]
ING --> ON[("Magatzem online<br/>valors actuals, baixa latència")]
OFF -->|"point-in-time join"| TR["Dataset d'entrenament"]
ON -->|"lookup en ms"| SRV["Servei de predicció<br/>(mòdul 4)"]
TR --> MOD["Model"]
MOD -.->|"mateixes features,<br/>mateixa definició"| SRVEl flux complet: les definicions (en codi, versionades a Git) es registren; els pipelines de materialització calculen els valors des de les fonts i els escriuen a tots dos magatzems; l'entrenament llegeix de l'offline amb point-in-time joins; el serving llegeix de l'online amb lookups. La simetria entrenament/serving queda garantida per construcció — aquest és el contracte del feature store.
Panorama d'opcions
Sense entrar en tutorials (no és l'objectiu d'aquesta lliçó), el mapa del terreny:
| Opció | Tipus | Trets |
|---|---|---|
| Feast | Open source | L'estàndard de facto autogestionat; agnòstic d'infraestructura (offline sobre el teu warehouse/fitxers, online sobre Redis/DynamoDB...); tu operes les peces |
| SageMaker Feature Store (AWS), Vertex AI Feature Store (GCP), Databricks Feature Store | Gestionats | Integrats amb la seva plataforma; menys operació, més acoblament al proveïdor |
| Tecton, Hopsworks | Comercials especialitzats | Afegeixen transformacions gestionades, features en streaming, SLAs |
Perquè la idea de "definició declarativa" es vegi amb codi, així es declara una feature view a Feast — llegeix-lo com a esquema conceptual, no com a tutorial:
# Definició declarativa: l'entitat i les seves features, UNA sola vegada
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
from datetime import timedelta
client = Entity(name="client", join_keys=["id_client"])
features_churn = FeatureView(
name="features_churn_client",
entities=[client],
ttl=timedelta(days=30),
schema=[
Field(name="antiguitat_mesos", dtype=Int64),
Field(name="hores_setmana", dtype=Float32),
Field(name="tiquets_suport", dtype=Int64),
Field(name="ratio_tickets", dtype=Float32),
],
source=FileSource(path="data/features_historic.parquet",
timestamp_field="data_esdeveniment"),
)A partir d'una definició així, el mateix sistema respon get_historical_features(...) (offline, amb point-in-time) i get_online_features(...) (online, en mil·lisegons). La definició és una; els consumidors, dos.
Mirada crítica: quan no el necessites
I ara, la part que molts cursos ometen. Un feature store és infraestructura seriosa: un magatzem online que cal operar i pagar, pipelines de materialització que cal orquestrar i vigilar, un framework per aprendre, i una font nova d'incidències ("les features d'ahir no es van materialitzar"). Adoptar-lo sense necessitar-lo és deute operatiu pur — l'error simètric al que aquest curs porta combatent des del mòdul 1: tan dolent és el notebook sense enginyeria com l'enginyeria sense problema que la justifiqui.
| Senyals que EL necessites | Senyals que NO el necessites |
|---|---|
| Diversos models comparteixen features i les reimplementen | Un sol model (o features disjuntes per model) |
| Features cares: agregacions sobre historial massiu, finestres temporals, streaming | Features barates de calcular al vol des de la petició |
| Serving online amb pressupost de latència estricte i features precomputades | Batch scoring, o online amb features derivables del payload |
| Necessites point-in-time correctness sobre dades que canvien constantment | Datasets estàtics o amb snapshot ja correcte |
| Diversos equips produeixen/consumeixen features (descobriment, ownership) | Un equip petit amb un repo |
Passem CineClick per aquesta taula: un model, un equip (Laura, Marc i tu), features que es calculen en microsegons a partir de les columnes del mateix client, sense agregacions d'historial ni streaming. Columna dreta de cap a peus. Decisió: CineClick no adopta un feature store avui. Però el problema de l'skew és real i cal tancar-lo igualment, així que adoptem el remei proporcionat — l'opció 1 de l'apartat 2, elevada a regla de projecte:
features.pyés l'única font de veritat de les features. Tota transformació de dades crues a features viu asrc/cineclick_churn/features.py, dins del paquet instal·lable. El pipeline d'entrenament la importa (ja ho fa, via l'stageconstruir_features), i el servei de predicció del mòdul 4 importarà exactament la mateixa funció — mateix paquet, mateixa versió, mateix.clip(lower=1). Prohibit reimplementar una feature fora d'aquest mòdul.
Això és un "feature store de pobres" perfectament digne: resol l'skew d'implementació pel mateix mecanisme (definició única) sense cost d'infraestructura. No dona point-in-time correctness ni magatzem online — el dia que CineClick tingui tres models, features d'agregació sobre esdeveniments de visionament en streaming i un requisit de latència que obligui a precomputar, la taula de dalt dirà una altra cosa i la migració a Feast serà natural, perquè les definicions ja estan centralitzades i testejades. L'arquitectura correcta no és la més completa, és la proporcionada al problema — revisada quan el problema canvia.
Errors Comuns i Consells
- Adoptar un feature store "perquè és el que es fa": si el teu cas és a la columna dreta de la taula, acabes operant Redis i pipelines de materialització per servir features que un
importresolia. Avalua senyals, no modes. - El contrari: negar el problema de l'skew perquè no fas servir feature store: l'skew no es cura ignorant-lo. Si no hi ha store, hi ha d'haver una altra garantia de definició única (paquet compartit com CineClick) i, al mòdul 5, tests que la vigilin.
- Copiar "només aquesta funció" de
features.pyal servei "per no dependre del paquet sencer": és exactament així com neix la segona implementació deratio_tickets. La dependència del paquet és la garantia, no una nosa. - Construir datasets històrics consultant valors actuals: el leakage temporal de l'apartat 4. Si les teves etiquetes tenen data, les teves features han de ser les d'aquella data. Amb o sense feature store, fes-te sempre la pregunta: "aquest valor es coneixia en el moment de la predicció?".
- Confondre feature store amb "la taula de features al warehouse": una taula compartida ajuda a la reutilització, però no dona point-in-time joins ni serving online ni registre de definicions. Pot ser suficient — però sigues conscient de què et falta.
- Oblidar que les definicions de features també es versionen: si
ratio_ticketscanvia de fórmula, els models entrenats amb la fórmula vella la continuen esperant. Les definicions van a Git (amb o sense store) i el lineage del model (lliçó anterior) ha de permetre saber amb quina versió es va entrenar.
Exercicis
-
Caçar l'skew. A més del cas d'
antiguitat_mesos=0, troba dues altres maneres subtils en què la implementació de serving deratio_ticketsde l'apartat 2 podria divergir de la de l'entrenament, fins i tot corregint el cas del zero. Pista: pensa en tipus de dades i en el significat detiquets_suporta cada context. -
Diagnòstic de leakage temporal. Marc proposa afegir la feature
hores_setmana_ultim_mes(mitjana d'hores de visionament de l'últim mes) fent servir la taula de visionaments actual per a etiquetes de fa tres mesos. Explica (a) quin valor tindria aquesta feature en entrenament per a un client que es va donar de baixa fa deu setmanes, (b) per què això infla les mètriques offline, i (c) què necessitaria el dataset per construir aquesta feature correctament. -
Feature store sí o no? Per a cada escenari, decideix amb la taula de senyals i justifica en una o dues frases: (a) una fintech amb 14 models que comparteixen features de comportament transaccional agregades per finestres d'1/7/30 dies, servides online amb pressupost de 50 ms; (b) un equip de dues persones amb un model batch mensual de propensió a compra sobre un extracte estàtic del CRM; (c) CineClick d'aquí a dos anys, amb cinc models (churn, upgrade, recomanació...) que comparteixen agregats d'esdeveniments de visionament en temps gairebé real.
Solucions
-
Exemples de divergència subtil (n'hi ha més): (a) tipus i arrodoniment — a pandas la divisió produeix
float64a partir de columnes enteres; si el servei rep el JSON ambtiquets_suportcom a string ("3") o algú arrodoneix a 2 decimals "per netejar la resposta", els valors deixen de ser idèntics bit a bit i als talls dels arbres a prop del llindar la predicció pot canviar. (b) semàntica de la font — en entrenamenttiquets_suportve del CSV històric (tiquets acumulats segons l'extracte); en producció el servei podria llegir del sistema de suport en viu, que potser també compta tiquets oberts-i-cancel·lats o exclou els de facturació. Mateix nom de camp, població diferent. La moralitat: l'skew no és només codi diferent, també és font diferent — per això la regla de font única cobreix la funció i el contracte de les seves entrades (la signatura del model del registry ajuda amb el segon). -
(a) A prop de 0: el client porta deu setmanes de baixa, així que el seu visionament "de l'últim mes" (comptat des d'avui) és nul — quan l'etiqueta es va generar, era un client actiu amb hores reals. (b) El model aprèn "hores_ultim_mes ≈ 0 ⇒ abandonament", una regla gairebé perfecta al dataset... que en producció no serveix, perquè allà la feature es calcula per a clients encara actius: la mètrica offline mesura la capacitat de reconèixer baixes ja consumades, no de predir-les. (c) Historial de visionament amb marques temporals i un càlcul point-in-time: per a cada client, la mitjana d'hores del mes anterior a la seva data d'etiqueta. Exactament el join que un magatzem offline dona fet — i que sense ell exigeix un
merge_asofacurat sobre l'històric. -
(a) Sí, clarament: molts models compartint features cares (finestres temporals), serving online amb latència estricta — tots els senyals de la columna esquerra alhora. (b) No: un model, batch, dades estàtiques, equip mínim; una
features.pycompartida i disciplina de font única basten, i el cost operatiu de l'store no s'amortitza. (c) Probablement sí, i és el moment de reavaluar: features d'agregació sobre esdeveniments compartides entre cinc models activen els senyals de reutilització, cost de càlcul i point-in-time. L'avantatge de la decisió presa avui és que la migració parteix de definicions ja centralitzades i testejades afeatures.py, no de cinc còpies divergents.
Conclusió
El mòdul 3 tanca el capítol de "gestionar el que s'aprèn". L'experiment churn-cineclick de MLflow desa cada execució amb els seus paràmetres, mètriques, commit i hash de dades — la pregunta "què era la v2?" ja no pot tornar a existir. El registry converteix el guanyador en el model churn-cineclick versió 2, promocionat a @champion (recall 0.68, precision 0.55, F1 0.61) amb revisió humana documentada i rollback a un alias de distància. I les features tenen font única: features.py, al paquet instal·lable, compartida per l'entrenament i pel serving que ve — amb el criteri clar de quins senyals justificarien, demà, un feature store de debò. Repassa l'inventari: codi reproduïble, dades versionades, pipeline declarat, experiments amb memòria, model campió amb nom, versió i signatura, features sense doble implementació. Tot el que un servei de producció necessita carregar està identificat i és traçable — models:/churn-cineclick@champion espera que algú el cridi. El mòdul 4 fa exactament això: triar el patró de desplegament adequat (batch, online o streaming), aixecar un servei de predicció amb FastAPI que carregui el campió i importi les features de la font única, i empaquetar-lo amb Docker perquè corri igual a qualsevol lloc. Laura ja té model; li falta que CineClick pugui fer-lo servir.
Curs de MLOps
Mòdul 1: Fonaments de MLOps
- Què és MLOps i per què els models moren al notebook
- El cicle de vida d'un model de ML en producció
- Nivells de maduresa MLOps i rols de l'equip
- El projecte del curs: del notebook a producció
Mòdul 2: Del notebook al codi reproduïble
- Estructura d'un projecte de ML: del notebook al paquet
- Entorns reproduïbles i gestió de dependències
- Versionat de dades amb DVC
- Pipelines d'entrenament reproduïbles
Mòdul 3: Experiments i registre de models
- Tracking d'experiments amb MLflow
- Model registry: versionar i promocionar models
- Feature stores: quan i per a què
Mòdul 4: Servir models en producció
- Patrons de desplegament: batch, online i streaming
- Un servei de predicció amb FastAPI
- Empaquetatge amb Docker
- Escalat i desplegament: Kubernetes i serverless
- Optimització de la inferència: latència i cost
Mòdul 5: Automatització: CI/CD i orquestració
- CI per a ML: tests de codi, dades i models
- CD: automatitzar el desplegament del model
- Orquestració de pipelines de ML
- Estratègies de release: shadow, canary i A/B
