La lliçó anterior va acabar amb una inquietud: totes les nostres mètriques es van calcular sobre una única divisió entrenament/prova, i aquella divisió depèn de l'atzar d'un random_state. I si amb una altra llavor l'F1 hagués estat notablement diferent? En aquesta lliçó primer demostrarem que aquesta variabilitat és real, i després la resoldrem amb la validació creuada (cross-validation, CV): en lloc d'avaluar una vegada, avaluar K vegades rotant les dades, i reportar mitjana i desviació. És la tècnica estàndard per comparar models de manera honesta —la usarem per enfrontar per fi els algorismes del mòdul 4 sobre el churn de MercaFresh— i la base sobre la qual es construeix la cerca d'hiperparàmetres que veurem a 07-05.
Contingut
- El problema: una divisió, un número, molta variància
- K-fold pas a pas
cross_val_scoreicross_validatea la pràcticaStratifiedKFoldper al churn- Interpretar mitjana ± desviació: és significativa la diferència?
- Torneig honest: els models del mòdul 4 cara a cara
- Pipeline dins de la CV: avaluació sense fuites
- Variants i cost computacional
El problema: una divisió, un número, molta variància
Comprovem-ho empíricament: mateix model, mateixes dades, diferent llavor de divisió.
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
import numpy as np
resultats = []
for llavor in range(20): # 20 divisions diferents
X_ent, X_pr, y_ent, y_pr = train_test_split(
X, y, test_size=0.2, random_state=llavor, stratify=y
)
model = LogisticRegression(max_iter=1000).fit(X_ent, y_ent)
resultats.append(f1_score(y_pr, model.predict(X_pr)))
resultats = np.array(resultats)
print(f"F1 minim : {resultats.min():.3f}")
print(f"F1 maxim : {resultats.max():.3f}")
print(f"F1 mitja : {resultats.mean():.3f} +/- {resultats.std():.3f}")Una sortida típica amb un dataset mitjà: mínim 0.58, màxim 0.69, mitjana 0.63 ± 0.03. Onze punts percentuals de diferència entre la pitjor i la millor llavor, sense canviar absolutament res del model. Dues conseqüències:
- Un únic número ("el meu F1 és 0.66") amaga una loteria: potser et va tocar una llavor afortunada.
- Comparar dos models amb divisions diferents (o fins i tot amb la mateixa, si la diferència és petita) pot portar a conclusions errònies: la diferència observada pot ser pur atzar de la partició.
El bucle anterior ja apunta a la solució: avaluar diverses vegades i resumir amb mitjana i desviació. La validació creuada fa exactament això, però de manera més eficient: sense malbaratar dades i garantint que cada fila s'usa exactament una vegada com a prova.
K-fold pas a pas
La validació creuada K-fold funciona així:
- Es divideix el conjunt de dades (el d'entrenament; la prova final de 06-01 continua dins la seva caixa forta) en K blocs ("folds") de la mateixa mida. Valors habituals: K = 5 o K = 10.
- Es repeteixen K rondes. A la ronda i, el fold i actua de conjunt d'avaluació i els K−1 restants, d'entrenament.
- S'obtenen K mètriques, una per ronda, i se'n reporta la mitjana i la desviació estàndard.
flowchart TD
D["Dades d'entrenament<br/>dividides en 5 folds: F1 F2 F3 F4 F5"] --> R1
subgraph Rondes["5 rondes d'entrenament i avaluacio"]
R1["Ronda 1: entrena F2-F5, avalua F1 → metrica m1"]
R2["Ronda 2: entrena F1,F3-F5, avalua F2 → metrica m2"]
R3["Ronda 3: entrena F1,F2,F4,F5, avalua F3 → metrica m3"]
R4["Ronda 4: entrena F1-F3,F5, avalua F4 → metrica m4"]
R5["Ronda 5: entrena F1-F4, avalua F5 → metrica m5"]
end
R1 --> M["Resultat: mitjana(m1..m5) ± desviacio(m1..m5)"]
R2 --> M
R3 --> M
R4 --> M
R5 --> M
Avantatges davant de la divisió única:
- Cada fila s'avalua exactament una vegada (i s'entrena K−1 vegades): cap dada no es "malbarata" com a prova permanent.
- La mitjana de K mesures és molt més estable que una mesura solta.
- La desviació estàndard quantifica la incertesa: sabem fins a quin punt fiar-nos del número.
Important: la CV produeix K models d'un sol ús l'única finalitat dels quals és estimar el rendiment. El model final que aniria a producció s'entrena després amb totes les dades d'entrenament.
cross_val_score i cross_validate a la pràctica
scikit-learn automatitza tot el procés:
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(max_iter=1000)
# CV de 5 folds mesurant F1; cv=5 usa StratifiedKFold automaticament en classificacio
scores = cross_val_score(model, X_entrenament, y_entrenament, cv=5, scoring="f1")
print("F1 per fold:", np.round(scores, 3)) # p. ex. [0.61 0.65 0.63 0.60 0.66]
print(f"F1: {scores.mean():.3f} +/- {scores.std():.3f}")Detalls clau:
scoringaccepta les mètriques de 06-02 pel nom:"accuracy","precision","recall","f1","roc_auc"(la veurem a 06-04),"neg_mean_absolute_error","r2"... Les d'error van en negatiu (neg_) perquè scikit-learn sempre maximitza: un MAE de 18 apareix com a −18.cross_val_scorerep el model sense entrenar: ell s'encarrega de clonar, entrenar i avaluar a cada fold.
Quan es volen diverses mètriques alhora (i els temps), s'usa cross_validate:
from sklearn.model_selection import cross_validate
res = cross_validate(
model, X_entrenament, y_entrenament, cv=5,
scoring=["f1", "recall", "precision", "accuracy"],
return_train_score=True, # util per diagnosticar overfitting (06-05)
)
print(f"F1 val : {res['test_f1'].mean():.3f} +/- {res['test_f1'].std():.3f}")
print(f"Recall : {res['test_recall'].mean():.3f}")
print(f"F1 entren: {res['train_f1'].mean():.3f}") # bretxa entrenament-validacio -> 06-05
print(f"Temps/fold: {res['fit_time'].mean():.2f} s")StratifiedKFold per al churn
Amb el churn al 20 %, els folds han de conservar aquesta proporció, per la mateixa raó que estratificàvem a 06-01: un fold que per atzar tingués un 12 % de churn donaria una mètrica no comparable. StratifiedKFold estratifica cada fold:
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X_entrenament, y_entrenament, cv=cv, scoring="f1")Dos apunts:
- En classificació, passar
cv=5(un enter) ja usaStratifiedKFoldper sota; construir l'objecte explícit permet fixarshuffle=Trueirandom_state(recomanable: barreja abans de tallar els folds i fa l'experiment reproduïble). - En regressió,
cv=5usaKFoldsense estratificar, que és el que pertoca.
Interpretar mitjana ± desviació: és significativa la diferència?
Suposa dos models de churn amb CV de 5 folds:
- Regressió logística: F1 = 0.63 ± 0.02
- Arbre de decisió (
max_depth=5): F1 = 0.61 ± 0.04
És la logística "millor"? Aquí reapareix la lliçó 02-04: una mitjana mostral porta incertesa, i abans de declarar un guanyador cal preguntar-se si la diferència és significativa o compatible amb l'atzar. La intuïció pràctica, en l'esperit dels intervals de confiança:
- Si els rangs mitjana ± desviació se solapen àmpliament (com aquí: 0.61–0.65 davant de 0.57–0.65), l'evidència que un model sigui millor és feble.
- Si un model queda clarament per sobre fins i tot comptant la desviació (p. ex., 0.70 ± 0.02 davant de 0.61 ± 0.03), la diferència és difícilment atribuïble a l'atzar.
- Per afinar més es poden comparar els resultats fold a fold (tots dos models avaluats sobre els mateixos folds formen parelles, la mateixa idea del t-test aparellat de 02-04), tot i que amb només 5 valors el contrast formal té poca potència; a la pràctica professional la regla del solapament, usada amb prudència, resol la majoria de decisions.
Moralitat: reporta sempre mitjana ± desviació, i desconfia de rànquings decidits per diferències de mil·lèsimes.
Torneig honest: els models del mòdul 4 cara a cara
Al mòdul 4 vam presentar cada algorisme per separat, amb divisions instrumentals. Ara podem comparar-los amb rigor: mateixa CV, mateixos folds, mateixa mètrica.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import StratifiedKFold, cross_validate
models = {
"Regressio logistica": LogisticRegression(max_iter=1000),
"Arbre (max_depth=5)": DecisionTreeClassifier(max_depth=5, random_state=42),
"SVM (RBF)": SVC(),
"K-NN (k=15)": KNeighborsClassifier(n_neighbors=15),
"Naive Bayes": GaussianNB(),
"MLP": MLPClassifier(hidden_layer_sizes=(32,), max_iter=1000,
random_state=42),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # mateixos folds per a tots
for nom, clf in models.items():
pipe = Pipeline([("scaler", StandardScaler()), ("clf", clf)]) # vegeu la seccio seguent
res = cross_validate(pipe, X_entrenament, y_entrenament, cv=cv, scoring=["f1", "recall"])
print(f"{nom:22s} F1 = {res['test_f1'].mean():.3f} +/- {res['test_f1'].std():.3f}"
f" Recall = {res['test_recall'].mean():.3f}")Un resultat típic sobre el churn de MercaFresh:
| Model | F1 (mitjana ± std) | Recall | Comentari |
|---|---|---|---|
| Regressió logística | 0.63 ± 0.02 | 0.60 | Sòlida, estable, interpretable |
| SVM (RBF) | 0.63 ± 0.03 | 0.58 | Empata, amb més cost de còmput |
| MLP | 0.62 ± 0.04 | 0.59 | Similar i més variable |
| Arbre (max_depth=5) | 0.61 ± 0.04 | 0.62 | Competitiu; sensible a la partició |
| K-NN (k=15) | 0.59 ± 0.03 | 0.55 | Una mica per darrere |
| Naive Bayes | 0.56 ± 0.03 | 0.66 | Millor recall, pitjor precision |
(Els números concrets dependran de les teves dades; el que és transferible és el mètode.) Lectures: amb els solapaments de les desviacions, la logística, l'SVM i l'MLP estan empatats a la pràctica — i llavors decideixen altres criteris: interpretabilitat, velocitat, simplicitat (la logística guanya tots tres a MercaFresh). Fixa't també que cada model usa aquí els seus hiperparàmetres "raonables" sense optimitzar; la cerca sistemàtica que podria canviar aquest rànquing es recolza en aquesta mateixa CV i és el tema de 07-05 (GridSearchCV).
Pipeline dins de la CV: avaluació sense fuites
Atenció a un detall del codi anterior: l'StandardScaler va dins del Pipeline que es passa a cross_validate. No és estètica, és la regla anti-fuites de 06-01 aplicada a la CV:
# INCORRECTE: escalar abans de la CV
X_esc = StandardScaler().fit_transform(X_entrenament) # veu TOTS els folds alhora
cross_val_score(LogisticRegression(max_iter=1000), X_esc, y_entrenament, cv=5)
# CORRECTE: el Pipeline reajusta l'escalador a cada ronda,
# nomes amb els folds d'entrenament d'aquella ronda
pipe = Pipeline([("scaler", StandardScaler()),
("clf", LogisticRegression(max_iter=1000))])
cross_val_score(pipe, X_entrenament, y_entrenament, cv=5)En la versió incorrecta, l'escalador s'ajusta amb dades que a cada ronda faran de "prova", contaminant les K avaluacions. Amb imputació, selecció de features o codificacions dependents del target, la fuita pot inflar seriosament les mètriques. Regla d'or: tot pas que aprengui de les dades va dins del Pipeline, i el Pipeline sencer va dins de la CV. És la recompensa de la feina del mòdul 3: els nostres preprocessaments ja estaven empaquetats en Pipelines/ColumnTransformers, així que encaixen aquí sense canvis.
Variants i cost computacional
Dues variants que convé conèixer:
- LeaveOneOut (LOO): el cas extrem K = n (cada fila és un fold). Aprofita al màxim datasets molt petits, però exigeix entrenar n models i la seva estimació té alta variància. Rarament compensa davant d'un 5-fold o 10-fold repetit.
- TimeSeriesSplit: per a dades temporals com la demanda de MercaFresh, on el K-fold clàssic és invàlid (entrenaria amb el futur per avaluar el passat, la fuita temporal de 06-01). Genera talls creixents que sempre entrenen amb el passat i avaluen amb el bloc següent: entrena [1], avalua [2]; entrena [1,2], avalua [3]; entrena [1,2,3], avalua [4]...
from sklearn.model_selection import TimeSeriesSplit
cv_temporal = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(model_demanda, X_demanda, y_demanda,
cv=cv_temporal, scoring="neg_mean_absolute_error")Sobre el cost: la CV multiplica el temps d'entrenament per K (i pel nombre de models comparats, i més endavant per cada combinació d'hiperparàmetres de 07-05). Guia pràctica:
| Situació | Estratègia raonable |
|---|---|
| Dataset petit/mitjà, models ràpids | 5-fold o 10-fold sense dubtar |
| Models costosos (SVM/MLP grans, moltes dades) | 5-fold, o 3-fold en exploració inicial |
| Dataset enorme (milions de files) | Una divisió única ben feta ja és estable |
| Sèries temporals | TimeSeriesSplit |
| Dataset diminut (< 200 files) | 10-fold o LOO, assumint-ne la variància |
cross_validate(..., n_jobs=-1) paral·lelitza els folds entre els nuclis de la CPU, un alleujament gairebé gratuït.
Errors Comuns i Consells
- Fer CV sobre tot el dataset, prova inclosa. La CV substitueix el conjunt de validació, no el de prova: s'aplica sobre les dades d'entrenament, i la prova de 06-01 continua reservada per al veredicte final.
- Preprocessar abans de la CV. La fuita silenciosa més freqüent en notebooks reals. Pipeline dins de la CV, sempre.
- Comparar models amb folds diferents. Usa el mateix objecte
StratifiedKFold(mateixa llavor) per a tots els contendents; si no, part de la diferència serà soroll de partició. - Reportar només la mitjana. Sense la desviació no es pot jutjar si una diferència entre models importa.
- Usar K-fold barrejat en sèries temporals. És avaluar predint el passat amb el futur; usa
TimeSeriesSplit. - Oblidar reentrenar al final. Després de triar model amb CV, el model definitiu s'entrena amb totes les dades d'entrenament abans de l'avaluació final en prova (i abans de producció).
- Consell: fixa un "protocol d'avaluació" a l'inici del projecte (CV, mètrica, llavor) i no el canviïs a mig camí; canviar les regles del joc quan ja has vist resultats convida a l'autoengany.
Exercicis
Exercici 1
Explica per què aquest codi sobreestima el rendiment i corregeix-lo:
from sklearn.impute import SimpleImputer
X_imp = SimpleImputer(strategy="mean").fit_transform(X_entrenament)
X_esc = StandardScaler().fit_transform(X_imp)
scores = cross_val_score(LogisticRegression(max_iter=1000), X_esc, y_entrenament,
cv=5, scoring="f1")Exercici 2
Amb CV de 5 folds obtens: model A, recall = 0.64 ± 0.05; model B, recall = 0.66 ± 0.06. El teu cap a MercaFresh vol anunciar que "B és millor". Què li respondries, i què faries per augmentar la confiança en la comparació?
Exercici 3
Escriu el codi que compara DecisionTreeClassifier amb max_depth 3, 5 i 10 sobre el churn usant el mateix StratifiedKFold(5, shuffle=True, random_state=42) i scoring="f1", imprimint mitjana ± desviació de cadascun. (Estàs fent a mà, en miniatura, el que GridSearchCV automatitzarà a 07-05.)
Solucions
Solució 1. L'imputador i l'escalador s'ajusten amb tot X_entrenament abans de la CV, de manera que a cada ronda el fold d'avaluació ja ha influït en la mitjana d'imputació i en els paràmetres d'escalat: fuita de dades que infla les mètriques. Correcció — posar tots dos passos en un Pipeline que es reajusti dins de cada ronda:
from sklearn.pipeline import Pipeline
pipe = Pipeline([
("imputer", SimpleImputer(strategy="mean")),
("scaler", StandardScaler()),
("clf", LogisticRegression(max_iter=1000)),
])
scores = cross_val_score(pipe, X_entrenament, y_entrenament, cv=5, scoring="f1")Solució 2. La diferència (0.02) és molt menor que les desviacions (0.05–0.06): els rangs 0.59–0.69 i 0.60–0.72 se solapen gairebé del tot, així que l'avantatge de B és perfectament compatible amb l'atzar de la partició — com vam veure a 02-04, una diferència no és una conclusió fins que es descarta que sigui soroll. Per guanyar confiança: (a) avaluar tots dos amb els mateixos folds i comparar fold a fold (comparació aparellada); (b) repetir la CV amb diverses llavors (p. ex., RepeatedStratifiedKFold) per tenir més mesures; (c) si després d'això continuen empatats, triar per altres criteris (cost, interpretabilitat, recall mínim garantit).
Solució 3.
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for profunditat in [3, 5, 10]:
arbre = DecisionTreeClassifier(max_depth=profunditat, random_state=42)
scores = cross_val_score(arbre, X_entrenament, y_entrenament, cv=cv, scoring="f1")
print(f"max_depth={profunditat:2d} -> F1 = {scores.mean():.3f} +/- {scores.std():.3f}")En usar el mateix objecte cv, els tres arbres s'avaluen sobre folds idèntics i la comparació és justa. Un patró típic: 3 es queda curt, 10 sobreajusta i 5 guanya — el perquè d'aquesta forma d'"U invertida" el veurem amb les corbes de validació a 06-05, i l'automatització d'aquesta cerca, a 07-05.
Conclusió
La validació creuada resol la fragilitat de la divisió única: el K-fold rota les dades perquè cada fila sigui avaluada una vegada, cross_val_score/cross_validate ho automatitzen, StratifiedKFold preserva la proporció de churn a cada fold, i el resultat es llegeix com a mitjana ± desviació, aplicant l'escepticisme estadístic de 02-04 abans de declarar vencedors. Amb ella hem pogut celebrar el primer torneig just entre els models del mòdul 4, amb el Pipeline dins de la CV com a garantia anti-fuites, i hem deixat apuntades les variants (LeaveOneOut, TimeSeriesSplit) i el cost computacional. Però en tot aquest torneig els classificadors decidien amb el seu llindar per defecte de 0.5, i ja sabem des de 04-02 que aquell llindar és una palanca de negoci. La propera lliçó l'explota a fons: recorrerem tots els llindars possibles alhora amb la corba ROC i resumirem el resultat en un sol número, l'AUC.
Curs de Machine Learning
Mòdul 1: Introducció al Machine Learning
- Què és el Machine Learning?
- Història i evolució del Machine Learning
- Tipus de Machine Learning
- Aplicacions del Machine Learning
- El flux de treball d'un projecte de Machine Learning
Mòdul 2: Fonaments d'Estadística i Probabilitat
- Conceptes bàsics d'estadística
- Distribucions de probabilitat
- Correlació i covariància
- Inferència estadística
- Teorema de Bayes
Mòdul 3: Preprocessament de Dades
- Neteja de dades
- Gestió de dades mancants
- Transformació de dades
- Codificació de variables categòriques
- Normalització i estandardització
- Enginyeria de característiques
Mòdul 4: Algorismes de Machine Learning Supervisat
- Regressió lineal
- Regressió logística
- Arbres de decisió
- Màquines de suport vectorial (SVM)
- K veïns més propers (K-NN)
- Naive Bayes
- Xarxes neuronals
Mòdul 5: Algorismes de Machine Learning No Supervisat
- Clustering: K-means
- Clustering jeràrquic
- Anàlisi de components principals (PCA)
- Anàlisi d'agrupament DBSCAN
- Visualització de dades amb t-SNE i UMAP
Mòdul 6: Avaluació i Validació de Models
- Divisió de dades: entrenament, validació i prova
- Mètriques d'avaluació
- Validació creuada
- Corba ROC i AUC
- Overfitting i underfitting
Mòdul 7: Tècniques Avançades i Optimització
- Regularització: Ridge, Lasso i Elastic Net
- Ensemble Learning
- Gradient Boosting
- Xarxes neuronals profundes (Deep Learning)
- Optimització d'hiperparàmetres
Mòdul 8: Implementació i Desplegament de Models
- Frameworks i biblioteques populars
- Implementació de models en producció
- Manteniment i monitoratge de models
- Consideracions ètiques i de privadesa
Mòdul 9: Projectes Pràctics
- Projecte 1: Predicció de preus d'habitatges
- Projecte 2: Classificació d'imatges
- Projecte 3: Anàlisi de sentiments a les xarxes socials
- Projecte 4: Detecció de fraus
- Projecte 5: Segmentació de clients
