El model de churn de MercaFresh viu fins ara en un notebook: pipeline de preprocessament del mòdul 3, algorisme optimitzat al mòdul 7, llindar de decisió triat per costos al mòdul 6. Però un notebook no pot rebre la trucada del sistema de campanyes a les 3 de la matinada preguntant "quina probabilitat d'abandonament té el client 48210?". Portar un model a producció significa convertir-lo en un servei fiable que altres sistemes puguin fer servir. En aquesta lliçó recorrem aquest camí complet: serialitzar el pipeline amb joblib, triar entre servir en batch o en temps real, construir una API amb FastAPI, empaquetar-la amb Docker i desplegar-la de manera gradual i segura.

Contingut

  1. Del notebook a producció: què canvia
  2. Serialitzar el pipeline amb joblib
  3. Versionatge de l'artefacte i els seus riscos
  4. Patrons de servei: batch vs. online
  5. Una API REST amb FastAPI
  6. Empaquetatge amb Docker
  7. Desplegament gradual: shadow mode i test A/B
  8. Checklist de posada en producció

Del notebook a producció: què canvia

Un notebook i un servei en producció són objectes de naturalesa diferent, encara que continguin el mateix model:

graph LR
    subgraph nb["Notebook (exploracio)"]
        A[Dades historiques CSV] --> B[Pipeline + model]
        B --> C[Metriques i grafics]
    end
    subgraph prod["Produccio (servei)"]
        D[Peticions d'altres sistemes] --> E[Artefacte serialitzat]
        E --> F[Prediccions]
        F --> G[Sistema de campanyes]
        E --> H[Logs i monitoratge]
    end
    nb -- "serialitzar + empaquetar + desplegar" --> prod
Aspecte Notebook Producció
Qui l'executa Tu, a mà Altres sistemes, automàticament
Dades Fitxer històric conegut Peticions noves, de vegades mal formades
Fallades Reexecutes la cel·la S'han de detectar, registrar i no tombar el servei
Entorn El de la teva màquina Reproduïble i idèntic a cada desplegament (lliçó 08-01)
Codi Lineal, exploratori Validació d'entrades, maneig d'errors, logs

La conseqüència pràctica: en producció no es desplega "el notebook", es desplega un artefacte (el pipeline serialitzat) embolcallat en codi de servei (API o procés batch) dins d'un entorn reproduïble (contenidor). Anem peça a peça.

Serialitzar el pipeline amb joblib

Serialitzar és convertir l'objecte Python entrenat —amb tots els seus paràmetres apresos— en un fitxer que es pot desar, copiar a un servidor i tornar a carregar. A l'ecosistema scikit-learn l'estàndard és joblib, eficient amb els arrays grans de NumPy que viuen dins dels models.

El punt crític, que justifica tota la feina del mòdul 3: es serialitza el pipeline complet, no només el model. El Pipeline amb el seu ColumnTransformer desa també el que han après els preprocessadors (mitjanes de l'escalador, categories del codificador). Així, producció aplica exactament les mateixes transformacions que l'entrenament, i eliminem d'arrel la família d'errors "vaig entrenar amb dades escalades però predic amb dades sense escalar".

import joblib

# --- Al final de l'entrenament (notebook o script de training) ---
# pipeline_churn es el Pipeline complet del curs:
# ColumnTransformer (imputacio + escalat + one-hot) -> model optimitzat
pipeline_churn.fit(X_entrenament, y_entrenament)
joblib.dump(pipeline_churn, "model_churn_v3.joblib")

# --- Al servidor de produccio ---
pipeline = joblib.load("model_churn_v3.joblib")

# El pipeline carregat rep dades CRUES, com les del dataset original;
# el preprocessament va a dins:
import pandas as pd
client = pd.DataFrame([{
    "recencia_dies": 45, "frequencia_90d": 2, "despesa_mitjana": 38.50,
    "antiguitat_mesos": 14, "incidencies_lliurament": 3, "ciutat": "Valencia",
    "canal_captacio": "app",
}])
prob_churn = pipeline.predict_proba(client)[0, 1]
print(f"Probabilitat d'abandonament: {prob_churn:.2%}")

Fixa't que en producció fem servir predict_proba i no predict: com vas veure a 06-04, la decisió final es pren comparant la probabilitat amb el llindar triat per costos de negoci, i aquest llindar convé mantenir-lo com a configuració explícita del servei, no enterrat dins del model.

Versionatge de l'artefacte i els seus riscos

Dues advertències importants abans de continuar:

1. Versiona l'artefacte com versiones el codi. Un nom com model.joblib que se sobreescriu és una bomba de rellotgeria: quan alguna cosa falli no sabràs quin model estava servint. Pràctiques mínimes:

  • Nom amb versió i data: model_churn_v3_2026-08-24.joblib.
  • Al costat de l'artefacte, un fitxer de metadades: data d'entrenament, rang de dades usat, mètriques de validació, versions de les biblioteques (pip freeze), i el llindar de decisió.
  • No esborris mai la versió anterior en desplegar la nova: és el teu pla de rollback (ho reprendrem a 08-03, juntament amb els registres de models tipus MLflow).

2. Dos riscos de seguretat i compatibilitat que has de conèixer:

  • Deserialitzar és executar codi. Un fitxer joblib/pickle pot contenir codi arbitrari que s'executa en carregar-lo. Regla d'or: joblib.load només sobre fitxers l'origen dels quals controles (els teus propis entrenaments, el teu emmagatzematge intern). No carreguis mai un model descarregat d'una font no fiable.
  • Sensibilitat a versions. Un pipeline desat amb scikit-learn 1.5 pot fallar —o pitjor, comportar-se subtilment diferent sense error— en carregar-se amb la 1.2 o la 1.7. Per això el requirements.txt amb versions fixades de la lliçó anterior viatja sempre amb l'artefacte: entrenar i servir han de fer servir les mateixes versions.

Patrons de servei: batch vs. online

Abans d'escriure una API, una decisió d'arquitectura: com es consumiran les prediccions? Hi ha dos patrons fonamentals.

Aspecte Batch (per lots) Online (temps real)
Quan es prediu En moments programats (p. ex. cada nit) En l'instant en què arriba la petició
Sobre què Tots els clients (o un lot gran) Un client (o pocs) per petició
Latència exigida Hores: tant és si triga 20 minuts Mil·lisegons: algú espera la resposta
Forma tècnica Script/job programat que escriu a la base de dades API REST sempre aixecada
Complexitat operativa Baixa (un cron job) Mitjana-alta (disponibilitat, escalat, monitoratge)
Exemple MercaFresh Scoring nocturn de churn de tota la base de clients Decidir al checkout si mostrar una oferta de retenció

El cas natural del churn de MercaFresh és batch: l'equip de retenció planifica campanyes setmanals; no necessita saber la probabilitat d'abandonament en aquest mil·lisegon, sinó una taula fresca cada matí. Un scoring nocturn és més simple, més barat i més fàcil d'operar:

# score_nocturn.py — s'executa cada nit a les 02:00 (cron / orquestrador)
import joblib
import pandas as pd
from datetime import date

pipeline = joblib.load("model_churn_v3_2026-08-24.joblib")
LLINDAR = 0.42  # triat per costos a 06-04

clients = pd.read_parquet("clients_features_avui.parquet")  # dades crues actualitzades
clients["prob_churn"] = pipeline.predict_proba(
    clients.drop(columns=["client_id"])
)[:, 1]
clients["accio_retencio"] = clients["prob_churn"] >= LLINDAR

clients[["client_id", "prob_churn", "accio_retencio"]].to_parquet(
    f"scores_churn_{date.today()}.parquet"
)

El patró online es justifica quan la predicció depèn d'informació que només existeix en el moment (el contingut del cistell actual) o quan un altre sistema necessita resposta immediata. Com que molts sistemes acaben necessitant totes dues coses —i perquè és el patró tècnicament més ric— construïm ara la versió online.

Una API REST amb FastAPI

FastAPI és el framework Python més usat avui per servir models: ràpid, amb validació automàtica de dades via pydantic i documentació interactiva generada sola. La idea: el model es carrega una vegada en arrencar el servei, i cada petició HTTP a /predict retorna una predicció.

# app.py — servei de prediccio de churn
from fastapi import FastAPI
from pydantic import BaseModel, Field
import joblib
import pandas as pd

app = FastAPI(title="MercaFresh Churn API")

# S'executa UNA vegada en arrencar, no a cada peticio (carregar es lent)
pipeline = joblib.load("model_churn_v3_2026-08-24.joblib")
LLINDAR = 0.42

class DadesClient(BaseModel):
    """Esquema d'entrada: pydantic valida tipus i rangs automaticament."""
    recencia_dies: int = Field(ge=0, description="Dies des de l'ultima compra")
    frequencia_90d: int = Field(ge=0)
    despesa_mitjana: float = Field(ge=0)
    antiguitat_mesos: int = Field(ge=0)
    incidencies_lliurament: int = Field(ge=0)
    ciutat: str
    canal_captacio: str

@app.post("/predict")
def predir_churn(dades: DadesClient):
    X = pd.DataFrame([dades.model_dump()])   # dict -> DataFrame d'una fila
    prob = float(pipeline.predict_proba(X)[0, 1])
    return {
        "prob_churn": round(prob, 4),
        "accio_retencio": prob >= LLINDAR,
        "model": "churn_v3",
    }

Punts que convé entendre línia a línia:

  • DadesClient(BaseModel) defineix el contracte d'entrada. Si arriba una petició amb recencia_dies: "molts" o sense el camp ciutat, FastAPI la rebutja amb un error 422 clar abans de tocar el model. En producció, la meitat dels incidents són dades mal formades; la validació a la porta els converteix en errors explícits en lloc de prediccions absurdes.
  • Field(ge=0) afegeix restriccions (aquí: més gran o igual que zero) — validació de negoci, no només de tipus.
  • La resposta inclou "model": "churn_v3": identificar quina versió va respondre cada petició és or per al monitoratge (08-03).

S'aixeca i es prova així:

pip install fastapi uvicorn
uvicorn app:app --host 0.0.0.0 --port 8000
# Documentacio interactiva automatica a http://localhost:8000/docs
# provar_api.py — client de prova
import requests

resposta = requests.post("http://localhost:8000/predict", json={
    "recencia_dies": 45, "frequencia_90d": 2, "despesa_mitjana": 38.5,
    "antiguitat_mesos": 14, "incidencies_lliurament": 3,
    "ciutat": "Valencia", "canal_captacio": "app",
})
print(resposta.status_code)  # 200
print(resposta.json())       # {'prob_churn': 0.61..., 'accio_retencio': True, ...}

Amb això, qualsevol sistema de MercaFresh —el backend web, el sistema de campanyes— pot demanar prediccions amb una crida HTTP, sense saber res de Python ni de scikit-learn.

Empaquetatge amb Docker

Queda un problema: l'API funciona a la teva màquina, amb el teu Python i les teves versions. Docker resol el "a la meva màquina funcionava" empaquetant l'aplicació amb el seu entorn complet (sistema base, Python, biblioteques exactes, codi i artefacte) en una imatge immutable. Aquesta imatge s'executa com a contenidor de manera idèntica al teu portàtil, al servidor de MercaFresh o al núvol.

La recepta de la imatge és el Dockerfile:

# Dockerfile del servei de churn de MercaFresh

# 1. Imatge base: Python 3.12 minim sobre Debian (variant "slim")
FROM python:3.12-slim

# 2. Directori de treball dins del contenidor
WORKDIR /app

# 3. Copiar NOMES requirements primer i fer el pip install.
#    Docker desa les capes en cache: si el codi canvia pero requirements no,
#    no es torna a executar el pas mes lent (pip install).
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copiar el codi del servei i l'artefacte del model
COPY app.py .
COPY model_churn_v3_2026-08-24.joblib .

# 5. Port on escolta el servei
EXPOSE 8000

# 6. Ordre d'arrencada del contenidor
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Explicació de les decisions:

  1. FROM python:3.12-slim: es parteix d'una imatge oficial amb Python ja instal·lat; slim retalla el que és innecessari (imatge més petita, menys superfície d'atac).
  2. WORKDIR /app: totes les rutes següents són relatives a /app dins del contenidor.
  3. L'ordre COPY requirements.txtRUN pip installCOPY app.py aprofita la memòria cau per capes: en el cicle habitual (canvies codi, no dependències) reconstruir la imatge triga segons.
  4. L'artefacte del model viatja dins de la imatge: imatge i model es versionen junts, i desplegar una versió és desplegar un binari tancat. (En sistemes grans el model es pot descarregar d'un registre en arrencar; començar amb el model a dins és més simple.)
  5. EXPOSE documenta el port; 6. CMD defineix què executa el contenidor en arrencar.
docker build -t mercafresh/churn-api:v3 .     # construeix la imatge, etiquetada v3
docker run -p 8000:8000 mercafresh/churn-api:v3   # l'executa mapant el port
# L'API respon igual que abans a http://localhost:8000/predict

Per què contenidors, en una frase per motiu: reproduïbilitat (l'entorn viatja amb l'app), aïllament (no interfereix amb res més del servidor), portabilitat (la mateixa imatge corre a qualsevol lloc) i escalat (més trànsit? s'arrenquen més contenidors idèntics, a mà o amb un orquestrador com Kubernetes — que queda fora d'aquest curs).

Desplegament gradual: shadow mode i test A/B

Tens la imatge v3 llesta. Error clàssic: reemplaçar de cop la versió anterior. Si v3 té un problema que la validació no va detectar —i el món real sempre en troba algun—, el descobriràs amb tots els clients afectats. El desplegament professional és gradual:

Fase 1 — Shadow mode (mode ombra). La v3 es desplega al costat de la v2 i rep còpia de totes les peticions, però les seves prediccions no es fan servir: només es registren. Durant una o dues setmanes compares: respon amb la latència esperada? Falla amb algun tipus d'entrada? Les seves probabilitats tenen una distribució raonable respecte de les de v2? Tot el risc d'un desplegament, sense cap impacte en clients.

Fase 2 — Test A/B del model. Superada l'ombra, la v3 comença a decidir per a una fracció aleatòria de clients (p. ex. el 10 %), i la v2 per a la resta. És exactament el test A/B de la lliçó 02-04, amb el model com a tractament: es compara la mètrica de negoci (taxa de retenció després de campanya, cost per client retingut) entre grups, i s'exigeix significància estadística abans de concloure que v3 és millor al món real — no només al conjunt de prova. Nota important: la mètrica offline (AUC, F1) i la de negoci no sempre es mouen juntes; l'A/B és el jutge final.

Fase 3 — Rollout complet. v3 passa al 100 % del trànsit. La v2 no s'esborra: queda llesta per a un rollback immediat si alguna cosa va malament.

graph LR
    A[v3 validada offline] --> B["Shadow mode<br/>prediu, no decideix"]
    B --> C["Test A/B<br/>10 per cent del transit"]
    C --> D["Rollout 100 per cent"]
    B -- problema --> X[Descartar / corregir]
    C -- sense millora significativa --> X
    D -- incident --> R[Rollback a v2]

Checklist de posada en producció

Abans de donar per desplegat el model de churn, repassa:

  • [ ] L'artefacte és el pipeline complet (preprocessament inclòs) i accepta dades crues.
  • [ ] Artefacte versionat, amb metadades (dades, mètriques, llindar, pip freeze) i versió anterior conservada.
  • [ ] Versions de biblioteques idèntiques entre entrenament i servei (requirements fixat, 08-01).
  • [ ] Patró de servei triat conscientment (batch vs. online) segons la necessitat real.
  • [ ] Validació d'entrades a la porta (pydantic): tipus, rangs, camps obligatoris.
  • [ ] El llindar de decisió és configuració explícita, justificada per costos (06-04).
  • [ ] Imatge Docker construïda i provada en local; port i ordre d'arrencada correctes.
  • [ ] Cada resposta registra quina versió del model l'ha generada.
  • [ ] Pla de desplegament gradual: ombra → A/B → rollout, amb rollback preparat.
  • [ ] Logs de peticions i prediccions activats — són la matèria primera del monitoratge (08-03).

Errors Comuns i Consells

  • Serialitzar només el model i reimplementar el preprocessament "a mà" en producció. Tard o d'hora les dues implementacions divergeixen i les prediccions es corrompen en silenci. Sempre el pipeline complet.
  • Carregar el model dins de l'endpoint. joblib.load a cada petició multiplica la latència per cent. Es carrega una vegada, en arrencar el procés.
  • Ignorar els avisos de versió de scikit-learn en carregar un artefacte. Aquell warning de "unpickling from a different version" no és soroll: és el símptoma exacte del risc d'incompatibilitat. Alinea versions.
  • Muntar una API online quan un batch nocturn n'hi havia prou. És l'error d'arquitectura més car: pagues disponibilitat 24/7, escalat i guàrdies per a una necessitat que era una taula diària.
  • Desplegar al 100 % sense fase d'ombra. El conjunt de prova no conté les sorpreses del trànsit real (camps nous, valors extrems, pics de càrrega). L'ombra les troba gratis.
  • Consell: afegeix a l'API un endpoint /health que retorni la versió del model i un "ok". Els sistemes de desplegament el fan servir per saber si el servei és viu, i a tu t'estalvia el clàssic "quina versió hi ha en producció?".

Exercicis

Exercici 1. Per a cada escenari de MercaFresh, decideix batch o online i justifica-ho en una frase: (a) llista diària de clients en risc per a l'equip de retenció per correu electrònic; (b) decidir, mentre el client navega, si mostrar-li un bàner amb descompte de retenció; (c) informe mensual d'evolució del risc mitjà de churn per ciutat.

Exercici 2. L'API de l'exemple rep aquesta petició: {"recencia_dies": -5, "frequencia_90d": 2, "despesa_mitjana": 38.5, "antiguitat_mesos": 14, "incidencies_lliurament": 3, "ciutat": "Valencia", "canal_captacio": "app"}. Què passa i per què és preferible aquest comportament al fet que el pipeline predigués sense més?

Exercici 3. Escriu (sense executar-lo) el pla de desplegament gradual de la versió v4 del model de churn, indicant: què es compara en shadow mode, quina mètrica decideix el test A/B i quina condició dispararia un rollback després del rollout.

Solucions

Solució 1. (a) Batch: la llista es necessita una vegada al dia; un scoring nocturn programat és suficient i molt més simple. (b) Online: la decisió depèn del moment de la visita i algú (el frontend) espera la resposta en mil·lisegons; requereix l'API. (c) Batch: és agregació mensual sobre scores ja calculats; ni tan sols necessita prediccions noves, només els resultats del batch diari.

Solució 2. Pydantic rebutja la petició amb un error 422 abans d'arribar al model, perquè recencia_dies té la restricció Field(ge=0) i arriba -5. És preferible perquè una recència negativa és impossible: gairebé segur que hi ha un bug al sistema que crida (o a la integració). Si el pipeline predigués igualment, retornaria un número amb aparença legítima calculat sobre dades corruptes — una fallada silenciosa que ningú detectaria. Un error explícit a la porta converteix el problema en visible i atribuïble.

Solució 3. Pla tipus: (1) Ombra, 2 setmanes: v4 rep còpia del trànsit real; es comparen amb v3 la taxa d'errors, la latència (p. ex. percentil 95) i la distribució de probabilitats predites (si v4 prediu sistemàticament més alt/baix, investigar abans de continuar). (2) A/B, 4-6 setmanes al 10 %: mètrica de decisió de negoci — per exemple, taxa de retenció dels clients contactats (o cost per client retingut) al grup v4 davant del grup v3, amb test de significància com a 02-04; si no hi ha millora significativa, v4 no avança. (3) Rollout 100 % conservant la imatge v3; rollback si es dispara la taxa d'errors del servei, la latència supera el llindar acordat, o la distribució de prediccions/mètrica de negoci es desvia bruscament del que es va veure a l'A/B.

Conclusió

El model de churn de MercaFresh ja no viu en un notebook: és un pipeline serialitzat i versionat amb joblib, servit cada nit en batch per a les campanyes i disponible en temps real darrere d'una API FastAPI que valida cada entrada, tot empaquetat en una imatge Docker que s'executa igual a qualsevol màquina, i desplegat amb xarxa de seguretat — ombra, test A/B com el de 02-04 i rollback preparat. Però posar el model en producció no és el final de la feina, sinó el principi d'una etapa nova: el món canvia, els clients canvien, i un model entrenat amb el passat es degrada en silenci. Detectar aquesta degradació i decidir quan i com reentrenar és el tema de la propera lliçó: manteniment i monitoratge de models.

Curs de Machine Learning

Mòdul 1: Introducció al Machine Learning

Mòdul 2: Fonaments d'Estadística i Probabilitat

Mòdul 3: Preprocessament de Dades

Mòdul 4: Algorismes de Machine Learning Supervisat

Mòdul 5: Algorismes de Machine Learning No Supervisat

Mòdul 6: Avaluació i Validació de Models

Mòdul 7: Tècniques Avançades i Optimització

Mòdul 8: Implementació i Desplegament de Models

Mòdul 9: Projectes Pràctics

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats