Al final del mòdul anterior vam deixar plantejada una pregunta: "és bo de veritat el meu model?". Respondre-la amb rigor és tota una disciplina, i comença per una cosa aparentment senzilla: decidir amb quines dades s'entrena el model i amb quines dades s'avalua. Als mòduls 4 i 5 vam usar train_test_split de manera instrumental, sense justificar-lo; en aquesta lliçó entendrem per què és imprescindible separar les dades, quin paper juga cada conjunt (entrenament, validació i prova), com estratificar quan les classes estan desequilibrades —com el churn de MercaFresh— i, sobretot, com evitar la fuita de dades, aquell error silenciós que ja vam anticipar a les lliçons 03-02 i 03-05 i que invalida avaluacions senceres sense donar cap error d'execució.

Contingut

  1. Memoritzar no és aprendre: per què no s'avalua sobre allò entrenat
  2. La divisió train/test amb train_test_split
  3. El conjunt de validació: la divisió en tres
  4. Estratificació amb classes desequilibrades
  5. Fuita de dades: l'enemic silenciós
  6. Divisions temporals: quan el temps importa
  7. El mapa complet dels tres conjunts

Memoritzar no és aprendre: per què no s'avalua sobre allò entrenat

A la lliçó 01-01 vam definir el Machine Learning com la capacitat de generalitzar a partir d'exemples: no volem un model que recordi les comandes passades de MercaFresh, sinó un que encerti amb les comandes que encara no han passat.

L'analogia clàssica és l'examen. Si un professor lliura als seus alumnes les preguntes exactes de l'examen una setmana abans, tots trauran un 10 memoritzant les respostes. Aquell 10 no mesura si han après la matèria; mesura la seva memòria. Per saber si de veritat en saben, cal examinar-los amb preguntes que no han vist.

Amb els models passa exactament el mateix:

  • Avaluar sobre les dades d'entrenament mesura quant ha memoritzat el model. Un arbre de decisió sense límit de profunditat (ho vam veure a 04-03) pot assolir un 100 % d'accuracy en entrenament simplement creant una fulla per client.
  • Avaluar sobre dades mai vistes mesura quant ha generalitzat, que és l'únic que importa en producció.

Un exemple mínim ho demostra:

from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# X, y: dataset de churn de MercaFresh construit al modul 3
# (features RFM: recencia, frequencia, despesa mitjana, etc.)
X_entrenament, X_prova, y_entrenament, y_prova = train_test_split(
    X, y, test_size=0.2, random_state=42
)

arbre = DecisionTreeClassifier(random_state=42)  # sense max_depth: creix lliure
arbre.fit(X_entrenament, y_entrenament)

print("Accuracy en entrenament:", accuracy_score(y_entrenament, arbre.predict(X_entrenament)))
print("Accuracy en prova:      ", accuracy_score(y_prova, arbre.predict(X_prova)))

Un resultat típic seria 1.00 en entrenament i 0.78 en prova. El primer número és una il·lusió; el segon és l'estimació honesta de com es comportarà el model amb el proper client real. A aquesta bretxa entre tots dos números li posarem nom i remei a la lliçó 06-05 (overfitting).

La divisió train/test amb train_test_split

L'eina estàndard és train_test_split, que ja coneixes de manera instrumental. Ara en desglossem els paràmetres:

from sklearn.model_selection import train_test_split

X_entrenament, X_prova, y_entrenament, y_prova = train_test_split(
    X, y,
    test_size=0.2,      # proporcio reservada per a prova
    random_state=42,    # llavor: fa la divisio reproduible
    shuffle=True,       # barreja les files abans de dividir (per defecte True)
)
  • test_size: les mides habituals són 20 %–30 % per a prova. Amb datasets grans (centenars de milers de files) n'hi ha prou amb un percentatge menor, perquè en termes absoluts continua havent-hi molts exemples de prova.
  • random_state: la divisió és aleatòria; fixar la llavor garanteix que tu, el teu company i el teu "jo del futur" obtingueu exactament la mateixa partició. Sense ella, cada execució dona resultats lleugerament diferents (això ho explotarem a 06-03 per motivar la validació creuada).
  • shuffle: barreja les files abans de tallar. És essencial si el CSV ve ordenat (per data d'alta, per província...): sense barrejar, la prova podria contenir només clients recents o d'una sola regió. La gran excepció és la sèrie temporal, que veurem més avall: allà barrejar és precisament l'error.
Escenari test_size orientatiu Comentari
Dataset petit (< 1.000 files) 0.2–0.3 Cada fila compta; la validació creuada (06-03) ajudarà
Dataset mitjà (milers–desenes de milers) 0.2 L'estàndard de facto
Dataset molt gran (> 100.000) 0.1 o menys El 10 % ja són milers d'exemples de prova
Dades temporals tall per data Mai aleatori; vegeu la secció de divisions temporals

El conjunt de validació: la divisió en tres

Amb entrenament i prova sembla suficient... fins que comencem a prendre decisions mirant la prova. Imagina aquest flux a MercaFresh:

  1. Entrenes un arbre amb max_depth=3 → accuracy en prova: 0.81.
  2. Proves max_depth=5 → 0.84.
  3. Proves max_depth=8 → 0.83.
  4. Et quedes amb max_depth=5 i reportes "el meu model té un 84 % d'accuracy".

Aquell 84 % ja no és una estimació honesta: has usat la prova per triar l'hiperparàmetre, així que la prova ha participat (indirectament) en la construcció del model. És com deixar que l'alumne repeteixi l'examen deu vegades i quedar-se amb la millor nota: la prova s'ha "gastat".

La solució és dividir en tres conjunts:

  • Entrenament (train): ajusta els paràmetres interns del model (fit).
  • Validació (validation): compara alternatives — hiperparàmetres, algorismes, conjunts de features — i tria la millor. Es consulta moltes vegades.
  • Prova (test): es toca una sola vegada, al final, per estimar el rendiment real del model ja triat.

train_test_split no té un mode de tres vies, però n'hi ha prou d'encadenar dues crides:

# Primera divisio: separem la prova (20 %) i no la toquem mes
X_temp, X_prova, y_temp, y_prova = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Segona divisio: del 80 % restant, un 25 % per a validacio
# (0.25 x 0.8 = 0.2 -> repartiment final 60/20/20)
X_entrenament, X_val, y_entrenament, y_val = train_test_split(
    X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp
)

print(len(X_entrenament), len(X_val), len(X_prova))  # p. ex. 6000, 2000, 2000

Un repartiment habitual és 60/20/20 o 70/15/15. Avancem dues coses: la validació creuada (06-03) permet prescindir d'un conjunt de validació fix reutilitzant l'entrenament de manera intel·ligent, i les eines que automatitzen la cerca d'hiperparàmetres sobre validació (GridSearchCV i companyia) són el tema de 07-05.

Estratificació amb classes desequilibrades

El churn de MercaFresh volta el 20 %: de cada 100 clients, uns 20 es donen de baixa. Amb una divisió purament aleatòria, per atzar la prova podria quedar-se amb un 14 % o un 26 % de churn, i les mètriques calculades sobre ella deixarien de ser comparables amb la realitat.

El paràmetre stratify=y obliga que cada conjunt conservi les proporcions de classe del dataset original:

import numpy as np

X_entrenament, X_prova, y_entrenament, y_prova = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print("Churn global        :", np.mean(y))              # p. ex. 0.200
print("Churn en entrenament:", np.mean(y_entrenament))  # aprox. 0.200
print("Churn en prova      :", np.mean(y_prova))        # aprox. 0.200

Regles pràctiques:

  • Amb classificació, usa stratify=y gairebé sempre; mai no fa mal i és crític amb desequilibri.
  • Com més petit el dataset o més rara la classe minoritària, més important és estratificar.
  • En regressió no existeix stratify directe (no hi ha classes), tot i que es pot estratificar per trams de la variable objectiu si cal.

Fuita de dades: l'enemic silenciós

La fuita de dades (data leakage) es produeix quan informació que no estaria disponible en el moment de predir es cola a l'entrenament o a l'avaluació. El model sembla excel·lent en prova i fracassa en producció. A 03-02 i 03-05 ho vam anticipar amb una regla: fit en entrenament, transform en prova. Ara ho veiem en profunditat amb tres fuites subtils.

Fuita 1: escalar (o imputar) abans de dividir

# INCORRECTE: l'escalador "veu" la prova
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_escalat = scaler.fit_transform(X)          # mitjana i desviacio de TOT el dataset
X_entrenament, X_prova, ... = train_test_split(X_escalat, y, ...)

# CORRECTE: primer dividir, despres ajustar nomes amb entrenament
X_entrenament, X_prova, y_entrenament, y_prova = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_entrenament_esc = scaler.fit_transform(X_entrenament)  # apren NOMES de l'entrenament
X_prova_esc = scaler.transform(X_prova)                  # aplica el que ha apres

En el cas incorrecte, la mitjana i la desviació usades per escalar incorporen informació de les files de prova. La fuita és petita amb StandardScaler, però amb imputació per la mitjana, selecció de features per correlació o codificacions basades en el target pot ser enorme. La manera robusta de blindar-se és el Pipeline del mòdul 3, que aplica automàticament fit-en-entrenament/transform-en-prova; a 06-03 veurem que dins de la validació creuada el Pipeline és directament obligatori.

Fuita 2: duplicats repartits entre entrenament i prova

Si el dataset de MercaFresh té files duplicades (el mateix client exportat dues vegades, o el mateix client amb dos comptes), la barreja pot deixar una còpia a entrenament i una altra a prova. El model "encerta" en prova perquè ja va veure aquella fila exacta en entrenament: memòria disfressada de generalització.

# Abans de dividir: detectar i eliminar duplicats
print("Duplicats exactes:", df.duplicated().sum())
df = df.drop_duplicates()

# Variant mes subtil: diverses files del MATEIX client (snapshots mensuals).
# La solucio es dividir PER CLIENT, no per fila:
from sklearn.model_selection import GroupShuffleSplit

gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
idx_entrenament, idx_prova = next(gss.split(df, groups=df["client_id"]))

La regla general: si diverses files comparteixen una entitat (client, botiga, sessió), aquesta entitat ha de caure sencera a entrenament o sencera a prova.

Fuita 3: informació temporal futura

És la més traïdora. Suposa que per predir el churn de març construeixes la feature "despesa mitjana de l'últim trimestre"... calculada amb dades d'abril. O que una columna com motiu_baixa només s'emplena després que el client es doni de baixa: és una fuita perfecta, perquè prediu el churn amb una conseqüència del churn. Senyals d'alarma:

  • Una feature amb correlació sospitosament alta amb el target (un accuracy del 99 % és motiu de desconfiança, no de celebració!).
  • Columnes que en el sistema real s'emplenen després de l'esdeveniment que vols predir.
  • Agregats (mitjanes, comptadors) calculats sobre finestres que inclouen el futur.

La pregunta de control és sempre la mateixa: "tindria aquesta dada disponible en el moment exacte de fer la predicció?". Si la resposta és no, fora.

Divisions temporals: quan el temps importa

Per a la predicció de demanda de MercaFresh (quantes unitats de fruita vendrem la setmana vinent?) les dades tenen ordre temporal, i el model s'usarà sempre així: entrenat amb el passat, predint el futur. L'avaluació ha d'imitar aquest ús:

# INCORRECTE per a series temporals: barreja passat i futur
train_test_split(X, y, test_size=0.2, shuffle=True)

# CORRECTE: tall cronologic
df = df.sort_values("data")
tall = "2025-10-01"
entrenament = df[df["data"] < tall]   # p. ex. gener 2023 - setembre 2025
prova       = df[df["data"] >= tall]  # octubre - desembre 2025

Si barregem, el model entrena amb vendes de desembre per "predir" les del juny anterior: a la pràctica està veient el futur, i la mètrica resultant és pura fantasia (és la fuita 3 a escala de divisió). Amb tres conjunts, l'ordre es manté: el més antic per a entrenament, l'intermedi per a validació i el més recent per a prova. Existeix a més una variant de validació creuada específica per a sèries temporals (TimeSeriesSplit) que veurem breument a 06-03.

El mapa complet dels tres conjunts

flowchart TD
    D["Dataset complet de MercaFresh<br/>(despres de la neteja i sense duplicats)"] -->|"train_test_split<br/>stratify=y"| T["Entrenament (60%)"]
    D -->|"train_test_split<br/>stratify=y"| V["Validacio (20%)"]
    D -->|"apartat des del principi"| P["Prova (20%)"]

    T -->|"fit()"| M["Model(s) candidats"]
    V -->|"comparar i triar<br/>hiperparametres / algorisme"| M
    M -->|"model final triat"| F["Avaluacio final"]
    P -->|"una sola vegada"| F
    F --> R["Estimacio honesta del<br/>rendiment en produccio"]

Tres papers, tres regles:

Conjunt Qui l'usa Quantes vegades Pregunta que respon
Entrenament El fit() del model i dels transformadors Moltes Quins patrons hi ha a les dades?
Validació L'humà (o la cerca d'hiperparàmetres) Moltes Quina alternativa trio?
Prova L'avaluació final Una Com funcionarà en producció?

Errors Comuns i Consells

  • Avaluar sobre entrenament i reportar aquell número. És l'error número u del principiant. L'accuracy d'entrenament només serveix per diagnosticar overfitting (06-05), mai com a mètrica de qualitat.
  • "Gastar" la prova. Cada vegada que mires el resultat en prova i canvies alguna cosa del model, la prova perd valor. Decideix amb validació; reserva la prova per al veredicte final.
  • Oblidar random_state. Sense llavor fixa, no podràs reproduir els teus resultats ni comparar experiments de manera justa.
  • No estratificar amb classes desequilibrades. Amb un churn del 20 % i un dataset petit, la proporció en prova es pot desviar molt per atzar.
  • Escalar/imputar abans de dividir. La fuita clàssica. Usa Pipeline i no te n'hauràs de recordar.
  • Barrejar dades temporals. Si el model predirà el futur, avalua'l predint el futur.
  • Consell: fes la divisió tan aviat com puguis en el teu flux de treball, just després de la neteja bàsica, i tracta X_prova/y_prova com si fossin dins d'una caixa forta.

Exercicis

Exercici 1

Un company de MercaFresh t'ensenya aquest codi i presumeix d'un 97 % d'accuracy. Identifica dos problemes metodològics.

scaler = StandardScaler()
X_esc = scaler.fit_transform(X)
X_entrenament, X_prova, y_entrenament, y_prova = train_test_split(X_esc, y, test_size=0.2)
model.fit(X_entrenament, y_entrenament)
print(accuracy_score(y_entrenament, model.predict(X_entrenament)))

Exercici 2

Crea una divisió 70/15/15 (entrenament/validació/prova) estratificada per a un dataset de churn X, y, amb random_state=7, i verifica que la proporció de churn és similar als tres conjunts. Pista: la segona crida a train_test_split ha de repartir el 30 % restant a parts iguals.

Exercici 3

MercaFresh vol predir la demanda diària de cada producte amb dades de 2023–2025. Proposa (sense codi) com dividiries les dades en entrenament/validació/prova i justifica per què shuffle=True seria un error aquí.

Solucions

Solució 1. (a) Fuita de dades: l'StandardScaler s'ajusta amb tot el dataset abans de dividir, de manera que la mitjana i la desviació incorporen informació de la prova; el correcte és dividir primer i fer fit_transform només a entrenament i transform a prova. (b) Avaluació sobre entrenament: l'accuracy reportat es calcula amb y_entrenament i prediccions sobre X_entrenament — mesura memorització, no generalització; s'hauria de calcular sobre la prova (i a més hi falta random_state per a reproduïbilitat i stratify=y si hi ha desequilibri, que serien millores addicionals).

Solució 2.

# Pas 1: separar el 30 % que despres es repartira entre validacio i prova
X_entrenament, X_resta, y_entrenament, y_resta = train_test_split(
    X, y, test_size=0.30, random_state=7, stratify=y
)
# Pas 2: dividir aquest 30 % per la meitat -> 15 % i 15 %
X_val, X_prova, y_val, y_prova = train_test_split(
    X_resta, y_resta, test_size=0.50, random_state=7, stratify=y_resta
)

import numpy as np
for nom, vec in [("entrenament", y_entrenament), ("validacio", y_val), ("prova", y_prova)]:
    print(f"{nom}: {len(vec)} files, churn = {np.mean(vec):.3f}")

Gràcies a stratify, els tres percentatges de churn han de ser pràcticament idèntics al global (~0.20).

Solució 3. Divisió cronològica: per exemple, entrenament amb 2023 i 2024, validació amb el primer semestre de 2025 i prova amb el segon semestre de 2025 (els talls exactes depenen del volum de dades, però l'ordre passat → validació → prova és innegociable). shuffle=True seria un error perquè barrejaria files de dates futures a l'entrenament: el model aprendria, per exemple, de les vendes de la campanya de Nadal de 2025 per "predir" dies anteriors, una fuita d'informació temporal que infla les mètriques i no reflecteix l'ús real (entrenar amb el passat per predir el futur).

Conclusió

En aquesta lliçó hem convertit el train_test_split mecànic de mòduls anteriors en una metodologia: avaluar sobre dades no vistes perquè generalitzar no és memoritzar; afegir un conjunt de validació per triar hiperparàmetres sense contaminar la prova; estratificar quan les classes estan desequilibrades, com el churn de MercaFresh; blindar-se contra les fuites de dades (escalar abans de dividir, duplicats repartits, informació futura); i respectar l'ordre cronològic quan les dades són temporals. Ja sabem amb quines dades avaluar; la següent pregunta és amb quin número: l'accuracy que hem anat usant té una trampa seriosa amb classes desequilibrades, i a la propera lliçó desplegarem l'arsenal complet de mètriques —matriu de confusió, precision, recall, F1, MAE, RMSE, R²— per mesurar exactament allò que li importa al negoci.

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