Vam tancar la lliçó anterior amb una taula en què la regressió logística encertava el 87,1 % de les comandes i la línia base "mai no es retorna" el 83,6 %. El Diego ho va resumir així: "tres punts i mig; no em sembla que compensi muntar tot això". Tenia raó a desconfiar de la xifra, però pel motiu contrari al que pensava: l'exactitud és una mesura tosca que, amb classes desequilibrades, amaga el que de debò importa. En aquesta lliçó aprendrem a avaluar models com a professionals: la matriu de confusió i els quatre tipus de resultat, cadascun amb el seu cost en euros; precisió, sensibilitat, especificitat i F1; les corbes ROC i precisió-recall i l'AUC; la tria del llindar segons el cost (recuperant la banda de revisió humana de 02-04); les mètriques de regressió (MAE, RMSE, R²) per a la previsió de demanda; i les estratègies de validació (hold-out, validació creuada, estratificada i temporal) que eviten que una única divisió afortunada ens enganyi. És important perquè l'avaluació és el que converteix un experiment en una decisió de negoci: sense mètriques adequades i validació honesta no es pot saber si un model mereix desplegar-se.

Contingut

  1. Per què l'exactitud enganya
  2. La matriu de confusió i el cost de cada error
  3. Precisió, sensibilitat, especificitat i F1
  4. Corba ROC, AUC i corba precisió-recall
  5. Triar el llindar segons el cost
  6. Mètriques de regressió: MAE, RMSE i R²
  7. Validació: hold-out, validació creuada, estratificada i temporal
  8. Línia base obligatòria i els tres conjunts: entrenament, validació i test
  9. Comparació correcta dels models de 04-04
  10. Errors Comuns i Consells
  11. Exercicis
  12. Conclusió

  1. Per què l'exactitud enganya

L'exactitud (accuracy) és la proporció d'encerts. És el que retorna score en classificació i el que hem fet servir fins ara. El seu problema apareix tan bon punt les classes estan desequilibrades: a NovaMarket es retorna el 16,4 % de les comandes, així que un "model" que digui sempre "no es retorna" encerta el 83,6 %. Si la taxa fos del 8 %, encertaria el 92 %; en detecció de frau amb targeta, on el frau és el 0,1 %, un model inútil tindria un 99,9 % d'exactitud. La xifra és alta i el model no serveix per a res, perquè no detecta ni un sol cas del que es busca.

La lliçó de fons: l'exactitud tracta igual tots els errors, però per a NovaMarket no és el mateix no anticipar una devolució que revisar una comanda que anava bé. Per raonar cal separar els tipus d'encert i d'error.

  1. La matriu de confusió i el cost de cada error

La matriu de confusió creua el que va predir el model amb el que va passar realment. Per al problema de devolucions (classe positiva = "retornat"):

Predit: no retornat Predit: retornat
Real: no retornat Vertader negatiu (VN): comanda normal tractada com a normal Fals positiu (FP): falsa alarma; es revisa o reté una comanda correcta
Real: retornat Fals negatiu (FN): devolució no anticipada Vertader positiu (VP): devolució detectada a temps

Cada casella té un cost de negoci diferent, i posar-hi nombres és la conversa més important que la Marta i el Diego tenen en tot el projecte:

  • Un FN (devolució que no es va anticipar) costa a NovaMarket uns 30 €: transport de tornada, recondicionament, gestió, i el descompte amb què es reven el producte obert.
  • Un FP (falsa alarma) costa uns 5 €: el temps de l'equip del Diego a revisar la comanda i la petita fricció amb un client que no havia fet res d'estrany (recorda 02-04: si aquestes friccions es concentren en un grup, el cost és també ètic i legal).
  • Un VP permet actuar (confirmar la comanda amb el client, reforçar l'embalatge, no enviar fins a verificar) i evita bona part d'aquests 30 €; un VN no costa res.

Amb scikit-learn, sobre el pipeline de 04-03 i la regressió logística de 04-04:

from novamarket_ml import generar_comandes_ml, embrutar_comandes, preparar_comandes, crear_preparacio
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix

X, y = preparar_comandes(embrutar_comandes(generar_comandes_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)

log = Pipeline([("prep", crear_preparacio()),
                ("model", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
pred = log.predict(Xte)                     # classes amb el llindar per defecte (0,5)
prob = log.predict_proba(Xte)[:, 1]         # probabilitat de devolucio

matriu = confusion_matrix(yte, pred)        # files: real; columnes: predit
print(matriu)
vn, fp, fn, vp = matriu.ravel()
print(f"VN={vn}  FP={fp}  FN={fn}  VP={vp}")

def cost_negoci(matriu, cost_fp=5, cost_fn=30):
    """Tradueix la matriu de confusio a euros: cada FP i cada FN tenen el seu cost."""
    vn, fp, fn, vp = matriu.ravel()
    return fp * cost_fp + fn * cost_fn

print("Cost del model:", cost_negoci(matriu), "€")

Sortida:

[[610  17]
 [ 80  43]]
VN=610  FP=17  FN=80  VP=43
Cost del model: 2485 €

Lectura: de les 750 comandes de test, 123 es van retornar. El model en va detectar 43 (VP), se li'n van escapar 80 (FN) i va donar 17 falses alarmes (FP). En euros: 17 × 5 + 80 × 30 = 2.485 €. La mateixa funció amb la línia base "sempre no" (VP = 0, FN = 123) dona 3.690 €, i amb la regla del Diego (import_comanda > 300: 19 VP, 104 FN, 15 FP) dona 3.195 €. Ara la comparació té un llenguatge que el Diego entén: el model estalvia 1.205 € per cada 750 comandes respecte a no fer res, i 710 € respecte a la seva regla; amb 3.000 comandes diàries, uns 4.800 € al dia davant de la línia base. I encara no hem ajustat el llindar (secció 5).

  1. Precisió, sensibilitat, especificitat i F1

De la matriu de confusió se'n deriven les mètriques estàndard. Amb els nostres nombres (VN 610, FP 17, FN 80, VP 43):

Mètrica Fórmula Pregunta que respon Valor
Exactitud (accuracy) (VP + VN) / total Quina proporció de comandes classifica bé? 653/750 = 0,871
Precisió (precision) VP / (VP + FP) De les que marca com a devolució, quantes ho són? (qualitat de l'alarma) 43/60 = 0,717
Sensibilitat / recall / exhaustivitat VP / (VP + FN) De les devolucions reals, quantes en detecta? (cobertura) 43/123 = 0,350
Especificitat VN / (VN + FP) De les comandes normals, quantes deixa en pau? 610/627 = 0,973
F1 2 · precisió · recall / (precisió + recall) Mitjana harmònica de precisió i recall: alta només si totes dues ho són 0,470

Ara la història real del model és visible: quan marca una comanda encerta el 72 % de les vegades (bona precisió), però només detecta el 35 % de les devolucions (recall baix). Amb l'exactitud no hi havia manera de saber-ho. Precisió i recall estan en tensió: per detectar més devolucions cal marcar més comandes, i entre elles hi haurà més falses alarmes; l'F1 resumeix el compromís en un nombre i és la mètrica més habitual per comparar classificadors amb classes desequilibrades quan no es disposa de costos.

classification_report imprimeix tot això per classe:

from sklearn.metrics import classification_report
print(classification_report(yte, pred, target_names=["no retornat", "retornat"], digits=3))
              precision    recall  f1-score   support
 no retornat      0.884     0.973     0.926       627
    retornat      0.717     0.350     0.470       123
    accuracy                          0.871       750
   macro avg      0.800     0.661     0.698       750
weighted avg      0.857     0.871     0.851       750

Fixa't que la classe majoritària té un F1 de 0,93 i la minoritària de 0,47: les mitjanes "weighted" (ponderades per mida) amaguen com de malament va la classe que importa; la "macro" (mitjana simple) ho reflecteix millor.

  1. Corba ROC, AUC i corba precisió-recall

Totes les mètriques anteriors depenen del llindar amb què convertim la probabilitat en classe (0,5 per defecte). Les corbes avaluen el model per a tots els llindars alhora:

  • La corba ROC dibuixa, per a cada llindar, la taxa de vertaders positius (recall) davant de la taxa de falsos positius (1 − especificitat). Un model aleatori dona la diagonal; un de perfecte puja per l'esquerra fins a la cantonada superior. L'AUC (àrea sota la corba) resumeix la corba en un nombre entre 0,5 (atzar) i 1 (perfecte), i té una interpretació intuïtiva: és la probabilitat que el model doni més puntuació a una devolució real que a una comanda normal triades a l'atzar. No depèn del llindar ni del desequilibri de classes, cosa que la fa ideal per comparar models.
  • La corba precisió-recall dibuixa la precisió davant del recall per a cada llindar. És més informativa que la ROC quan la classe positiva és rara, perquè se centra en el que passa amb aquesta classe; el seu resum és la precisió mitjana (average precision, AP). La línia base d'un model aleatori no és 0,5, sinó la taxa de positius (0,164 aquí).
from sklearn.metrics import roc_auc_score, roc_curve, precision_recall_curve, average_precision_score
import numpy as np

print("AUC ROC:", round(roc_auc_score(yte, prob), 3))
print("Precisio mitjana (AP):", round(average_precision_score(yte, prob), 3))

fpr, tpr, llindars = roc_curve(yte, prob)           # punts de la corba ROC
for u in (0.2, 0.3, 0.5):
    i = np.argmin(np.abs(llindars - u))
    print(f"llindar {u}: taxa FP {fpr[i]:.3f}  recall {tpr[i]:.3f}")
precisio, recall, llindars_pr = precision_recall_curve(yte, prob)
# Per dibuixar: plt.plot(fpr, tpr) i plt.plot(recall, precisio) amb Matplotlib

Sortida:

AUC ROC: 0.844
Precisio mitjana (AP): 0.598
llindar 0.2: taxa FP 0.175  recall 0.715
llindar 0.3: taxa FP 0.099  recall 0.545
llindar 0.5: taxa FP 0.030  recall 0.350

Un AUC de 0,84 és un model clarament útil (en el 84 % de les parelles devolució/no devolució ordena bé). Els tres punts mostren l'intercanvi: baixant el llindar de 0,5 a 0,2, el recall puja del 35 % al 72 % a canvi de molestar el 17,5 % de les comandes normals. Quin convé no ho diu la corba: ho diuen els costos.

  1. Triar el llindar segons el cost

Amb la funció cost_negoci podem recórrer llindars i triar el que minimitza els euros:

print("llindar  precisió  recall  FP   FN   cost €  marcats")
for u in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7):
    pred_u = (prob >= u).astype(int)
    m = confusion_matrix(yte, pred_u)
    prec = m[1, 1] / max(m[1, 1] + m[0, 1], 1)
    rec = m[1, 1] / (m[1, 1] + m[1, 0])
    print(f"{u:5.1f}   {prec:8.3f}  {rec:6.3f}  {m[0,1]:3d}  {m[1,0]:3d}  {cost_negoci(m):6d}   {pred_u.sum():4d}")

Sortida:

llindar  precisió  recall  FP   FN   cost €  marcats
  0.1      0.325   0.854  218   18    1630    323
  0.2      0.440   0.715  112   35    1610    200
  0.3      0.528   0.545   60   56    1980    127
  0.4      0.596   0.455   38   67    2200     94
  0.5      0.717   0.350   17   80    2485     60
  0.6      0.886   0.252    4   92    2780     35
  0.7      0.842   0.130    3  107    3225     19

Amb costos 5 €/30 €, el llindar òptim és 0,2: cost 1.610 € davant dels 2.485 € del llindar per defecte, un 35 % menys. El preu és marcar 200 comandes de 750 (el 27 %), cosa que al Diego li sembla excessiva operativament. Aquí és on encaixa la banda de revisió humana de 02-04: en comptes d'una única frontera, tres zones. Amb probabilitat ≥ 0,5 s'actua automàticament (60 comandes, 43 devolucions reals); entre 0,2 i 0,5 es passa a revisió humana (140 comandes, de les quals 45 es van retornar: la revisora troba gairebé una devolució de cada tres); per sota de 0,2 es deixa passar (550 comandes, 35 devolucions que s'escapen). Si una revisió costa 3 € i la revisora encerta, el cost total ronda els 1.555 €, i a més el sistema genera dades etiquetades a mà per millorar el model. Fixa't en l'ordre lògic: primer es mesura el model amb AUC i corbes, després es decideix el llindar amb costos i capacitat operativa; mai s'ajusta el llindar perquè "l'exactitud quedi bonica".

  1. Mètriques de regressió: MAE, RMSE i R²

Per a la previsió de demanda no hi ha classes: l'error és una distància entre el que s'ha previst i el que és real. Tres mètriques:

Mètrica Fórmula Interpretació Quan
MAE (error absolut mitjà) mitjana de |real − previst| Error típic en les unitats del problema (unitats venudes) Comunicar a negoci; robusta a valors extrems
RMSE (arrel de l'error quadràtic mitjà) √(mitjana de (real − previst)²) Com el MAE però penalitza més els errors grans; sempre ≥ MAE Quan un error gran és molt pitjor que diversos de petits
(coeficient de determinació) 1 − (error quadràtic del model / error quadràtic de predir la mitjana) Fracció de la variabilitat explicada: 1 perfecte, 0 igual que la mitjana, negatiu pitjor que la mitjana Comparar models entre si; no diu l'error en unitats

Sobre el model lineal amb estacionalitat de 04-04 (entrenat amb les setmanes 1-78, avaluat a 79-104):

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.linear_model import LinearRegression
from novamarket_ml import generar_demanda_setmanal

d = generar_demanda_setmanal(104, 42)
d["sen"] = np.sin(2 * np.pi * d["setmana"] / 52); d["cos"] = np.cos(2 * np.pi * d["setmana"] / 52)
d["black_friday"] = ((d["setmana"] - 1) % 52 == 47).astype(int)
feats = ["setmana", "sen", "cos", "black_friday"]
train, test = d[d["setmana"] <= 78], d[d["setmana"] > 78]

reg = LinearRegression().fit(train[feats], train["unitats"])
prev = reg.predict(test[feats])
print("MAE :", round(mean_absolute_error(test["unitats"], prev), 1))
print("RMSE:", round(np.sqrt(mean_squared_error(test["unitats"], prev)), 1))
print("R²  :", round(r2_score(test["unitats"], prev), 3))

base = np.full(len(test), train["unitats"].iloc[-1])      # linia base: repetir l'ultima setmana
print("Linia base MAE:", round(mean_absolute_error(test["unitats"], base), 1),
      " R²:", round(r2_score(test["unitats"], base), 3))

Sortida:

MAE : 16.8
RMSE: 20.2
R²  : 0.829
Linia base MAE: 39.0  R²: -0.224

El model s'equivoca en unes 17 unitats per setmana de mitjana (sobre vendes de 550-670), l'RMSE una mica més gran (20) indica que hi ha alguna setmana amb error gran (la desviació màxima és de 43 unitats, a la setmana 95), i explica el 83 % de la variabilitat. La línia base ingènua ("la setmana que ve vendrem el mateix que aquesta") té un MAE de 39 i un R² negatiu: pitjor que predir la mitjana del període. La recta simple de 04-02 tenia MAE 47 i R² −0,44. Sense línia base, un MAE de 17 no vol dir res; amb ella, sabem que el model redueix l'error a menys de la meitat.

  1. Validació: hold-out, validació creuada, estratificada i temporal

Fins ara hem fet servir una única divisió entrenament/test (hold-out). És ràpida, però el resultat depèn de quines comandes van caure a cada banda: repetint la divisió amb deu llavors diferents, l'AUC de la logística oscil·la entre 0,80 i 0,86. Amb un sol test podríem haver tingut sort (o mala sort) i treure conclusions equivocades en comparar dos models que es diferencien en 0,02.

La validació creuada k-fold resol el problema: es divideix el conjunt en k trossos (folds), s'entrena k vegades fent servir cada tros una vegada com a test i els k−1 restants com a entrenament, i es fa la mitjana de les k mesures. Totes les dades es fan servir per avaluar i per entrenar, i la desviació entre folds indica la incertesa. Variants:

  • Estratificada (StratifiedKFold): cada fold conserva la proporció de classes; imprescindible amb classes desequilibrades.
  • Temporal (TimeSeriesSplit): per a sèries, els folds respecten l'ordre: s'entrena amb el passat i s'avalua amb el bloc següent, mai a l'inrevés.
  • Leave-one-out, grups (GroupKFold, perquè les comandes d'un mateix client no es reparteixin entre entrenament i test): altres variants per a casos concrets.
from sklearn.model_selection import cross_val_score, StratifiedKFold, TimeSeriesSplit

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
pipe = Pipeline([("prep", crear_preparacio()), ("model", LogisticRegression(max_iter=1000))])
aucs = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")     # ajusta el pipeline a cada fold
print("AUC per fold:", aucs.round(3), " mitjana:", round(aucs.mean(), 3), "±", round(aucs.std(), 3))

tscv = TimeSeriesSplit(n_splits=4, test_size=13)                  # blocs de 13 setmanes (un trimestre)
for i, (tr_idx, te_idx) in enumerate(tscv.split(d)):
    print(f"fold {i}: entrena setmanes 1-{d['setmana'].iloc[tr_idx[-1]]}, "
          f"avalua {d['setmana'].iloc[te_idx[0]]}-{d['setmana'].iloc[te_idx[-1]]}")
maes = -cross_val_score(LinearRegression(), d[feats], d["unitats"], cv=tscv,
                        scoring="neg_mean_absolute_error")
print("MAE per trimestre:", maes.round(1), " mitjana:", round(maes.mean(), 1))

Sortida:

AUC per fold: [0.842 0.815 0.848 0.821 0.857]  mitjana: 0.836 ± 0.016
fold 0: entrena setmanes 1-52, avalua 53-65
fold 1: entrena setmanes 1-65, avalua 66-78
fold 2: entrena setmanes 1-78, avalua 79-91
fold 3: entrena setmanes 1-91, avalua 92-104
MAE per trimestre: [25.2 16.4 13.  20. ]  mitjana: 18.6

Explicació:

  • cross_val_score rep el pipeline complet, i a cada fold torna a ajustar la imputació, l'escalat i el model només amb la part d'entrenament d'aquell fold: la validació creuada hereta la protecció contra fuites de 04-03. scoring="roc_auc" tria la mètrica; n'hi ha desenes ("f1", "recall", "neg_mean_absolute_error"...; scikit-learn fa servir el criteri "més alt és millor", d'aquí el signe negatiu en els errors).
  • L'AUC mitjà de la logística és 0,836 ± 0,016. Quan comparem dos models, una diferència menor que aquesta desviació no és concloent.
  • A la sèrie, cada fold entrena amb més història i avalua el trimestre següent, exactament com es farà servir el model en producció. El primer fold, entrenat amb un sol any, és el pitjor (25 unitats): amb un any no es pot estimar bé l'estacionalitat anual. Si en comptes de TimeSeriesSplit féssim servir KFold barrejat, el MAE mitjà baixaria a unes 16,7 unitats, una estimació optimista que en producció no es compliria.

  1. Línia base obligatòria i els tres conjunts: entrenament, validació i test

Dues regles de disciplina tanquen la lliçó:

Sempre una línia base. Abans de celebrar qualsevol mètrica, calcula la mateixa mètrica per al model més ximple raonable: DummyClassifier(strategy="most_frequent") en classificació (sempre la classe majoritària), la mitjana o l'últim valor en regressió, i la regla manual vigent (la del Diego). Un model es justifica per la distància a la línia base, en la mètrica i en euros.

Tres conjunts, no dos. Si fem servir el test per triar entre models, llindars i hiperparàmetres, deixem de tenir una mesura honesta: cada decisió que prenem mirant-lo "filtra" informació del test al model, i el resultat final serà optimista. Per això el flux professional separa tres conjunts:

flowchart LR
    D[Dades històriques] --> T[Entrenament<br/>~60-70 %]
    D --> V[Validació<br/>~15-20 %]
    D --> S[Test<br/>~15-20 %]
    T -->|fit| M[Models candidats]
    V -->|comparar models,<br/>llindars, hiperparàmetres| M
    M -->|el triat| F[Model final]
    S -->|UNA sola vegada:<br/>estimació honesta| F
  • Entrenament: per ajustar paràmetres (fit).
  • Validació: per prendre decisions (quin algorisme, quin llindar, quins hiperparàmetres). A la pràctica, la validació creuada sobre el conjunt d'entrenament fa aquest paper sense necessitat d'un tros a part.
  • Test: es toca una sola vegada, al final, per estimar el rendiment del model ja triat. En aquesta lliçó hem fet servir el test per explorar llindars amb finalitats didàctiques; en un projecte real aquesta exploració es faria amb validació creuada. La lliçó següent (04-06) desenvolupa l'ajust d'hiperparàmetres amb aquest esquema.

  1. Comparació correcta dels models de 04-04

Repetim la comparació de 04-04, aquesta vegada amb les mètriques adequades i el cost de negoci:

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import pandas as pd

models = {
    "Regressió logística": LogisticRegression(max_iter=1000),
    "k veïns (k=15)": KNeighborsClassifier(n_neighbors=15),
    "Arbre (prof. 4)": DecisionTreeClassifier(max_depth=4, random_state=42),
    "Bosc aleatori": RandomForestClassifier(n_estimators=200, min_samples_leaf=5, random_state=42),
    "Línia base (sempre no)": DummyClassifier(strategy="most_frequent"),
}
files = []
for nom, m in models.items():
    p = Pipeline([("prep", crear_preparacio()), ("model", m)]).fit(Xtr, ytr)
    pr, pb = p.predict(Xte), p.predict_proba(Xte)[:, 1]
    files.append([nom, accuracy_score(yte, pr), precision_score(yte, pr, zero_division=0),
                  recall_score(yte, pr), f1_score(yte, pr), roc_auc_score(yte, pb),
                  cost_negoci(confusion_matrix(yte, pr))])
taula = pd.DataFrame(files, columns=["model", "exactitud", "precisió", "recall", "F1", "AUC", "cost €"])
print(taula.set_index("model").round(3))

Sortida:

                        exactitud  precisió  recall     F1    AUC  cost €
model
Regressió logística         0.871     0.717   0.350  0.470  0.844    2485
k veïns (k=15)              0.841     0.667   0.065  0.119  0.737    3470
Arbre (prof. 4)             0.855     0.592   0.366  0.452  0.792    2495
Bosc aleatori               0.859     0.689   0.252  0.369  0.825    2830
Línia base (sempre no)      0.836     0.000   0.000  0.000  0.500    3690

La taula explica una història molt més rica que l'exactitud sola:

  • k veïns semblava "només" 3 punts pitjor que la logística en exactitud; en realitat detecta el 6,5 % de les devolucions (recall 0,065) i el seu AUC de 0,74 el deixa lluny: és gairebé la línia base amb maquillatge.
  • L'arbre de profunditat 4 té menys exactitud que el bosc, però millor recall i F1 i menys cost (2.495 € davant 2.830 €) amb el llindar per defecte. Amb l'exactitud hauríem triat el bosc.
  • La logística guanya en tot (AUC 0,844, cost 2.485 €) i, després d'ajustar el llindar (secció 5), baixa a 1.610 €: un 56 % menys que la línia base. En validació creuada, l'avantatge de la logística sobre el bosc (0,836 davant 0,822 d'AUC mitjà) és de l'ordre de la desviació entre folds, així que tots dos són candidats raonables i a 04-06 veurem si el bosc millora ajustant-ne els hiperparàmetres.
  • La línia base té AUC 0,5 i cost 3.690 €: és la referència contra la qual es justifica tota la resta.

La Marta ja pot respondre al Diego amb el seu llenguatge: "el model, amb el llindar ben posat, estalvia uns 2.000 € per cada 750 comandes respecte a no fer res, i amb la banda de revisió no us arriben més de 140 comandes a revisar per cada 750".

Errors Comuns i Consells

  • Reportar només l'exactitud. Amb classes desequilibrades és gairebé sempre enganyosa. Mostra la matriu de confusió, precisió, recall, F1 i AUC, i si pots, euros.
  • Confondre precisió i recall. Precisió: del que marco, quant encerto. Recall: del que existeix, quant en trobo. Quin importa més depèn del cost de FP davant de FN.
  • Deixar el llindar a 0,5 per defecte. El llindar és una decisió de negoci; recorre'l amb la funció de cost i considera una banda de revisió humana.
  • Fer servir el test per decidir. Cada mirada al test per triar alguna cosa el contamina. Decideix amb validació creuada; el test, una vegada.
  • Barrejar sèries temporals. Fes servir TimeSeriesSplit; el KFold barrejat dona un optimisme que en producció es paga.
  • Comparar models per una única divisió. Diferències petites poden ser soroll; fes servir validació creuada i mira la desviació entre folds.
  • Oblidar la línia base. Un MAE de 17 o un AUC de 0,84 no volen dir res sense saber què fa el model ximple i la regla actual.
  • Ignorar els grups. Si un mateix client té comandes en entrenament i en test, el model el pot "reconèixer"; GroupKFold per id_client ho evita.

Exercicis

Exercici 1. El Diego revisa els costos: després de parlar amb logística, una devolució no anticipada costa 20 € (no 30) i una falsa alarma 8 € (cal trucar al client). Recalcula la taula de llindars amb cost_negoci(m, cost_fp=8, cost_fn=20). Canvia el llindar òptim? En quina direcció i per què?

Exercici 2. Calcula amb validació creuada estratificada de 5 folds l'F1 i el recall de la logística i de l'arbre de profunditat 4 (scoring="f1" i scoring="recall"). Quin dels dos té millor recall mitjà? La diferència supera la desviació entre folds? Comenta si canviaria la teva decisió respecte a la taula de la secció 9.

Exercici 3. A la previsió de demanda, substitueix la línia base "última setmana" per la mitjana de les 4 últimes setmanes d'entrenament i per "la mateixa setmana de l'any anterior" (unitats de la setmana −52). Calcula el MAE de cadascuna a les setmanes 79-104. Quina és millor? Per què la de l'any anterior surt tan malament en aquesta sèrie, i què caldria corregir perquè fos una línia base raonable?

Solucions

Solució 1. Amb FP a 8 € i FN a 20 €, el cost de deixar escapar una devolució baixa i el d'una falsa alarma puja, així que compensa marcar menys. Els costos queden: 0,1 → 2.104 €; 0,2 → 1.596 €; 0,3 → 1.600 €; 0,4 → 1.644 €; 0,5 → 1.736 €. El mínim numèric continua a 0,2, però ara 0,3 és a només 4 € i 0,4 a 48 € (amb els costos originals, 0,3 costava 370 € més que 0,2): la corba s'ha aplanat i desplaçat cap a llindars més alts. En general, com més car és el FP en relació amb el FN, més alt convé posar el llindar (més precisió, menys recall), i viceversa. Quan la corba de cost és plana al voltant de l'òptim, decideix el criteri operatiu: 0,3 marca 127 comandes davant de les 200 de 0,2 per pràcticament el mateix cost, així que el Diego triaria 0,3.

Solució 2. Amb StratifiedKFold(5, shuffle=True, random_state=42), la logística obté un F1 mitjà de 0,50 (± 0,04) i un recall mitjà de 0,39 (± 0,04); l'arbre de profunditat 4, F1 0,44 (± 0,07) i recall 0,33 (± 0,07) (els valors poden variar unes centèsimes segons la versió). La logística és millor en mitjana en totes dues mètriques, però la diferència de recall (0,05) és de l'ordre de la desviació entre folds, i l'arbre és a més molt més variable d'un fold a un altre (la inestabilitat dels arbres que vam comentar a 04-04). La decisió no canvia respecte a la secció 9: la logística continua sent preferible per AUC, F1 i estabilitat, però la validació creuada ens obliga a dir-ho amb precisió: "detecta una mica més de devolucions i de manera més estable; amb una sola divisió aquest avantatge podria no veure's".

Solució 3. La mitjana de les 4 últimes setmanes d'entrenament (unes 649 unitats) dona un MAE d'unes 35 unitats, una mica millor que "última setmana" (39) perquè fa la mitjana del soroll setmanal. La "mateixa setmana de l'any anterior" dona un MAE d'unes 118 unitats: surt molt malament perquè la sèrie té una tendència de 2,5 unitats per setmana, és a dir, unes 130 unitats més que un any abans, i la línia base ignora aquest creixement. Per fer-la raonable caldria sumar-hi la tendència (per exemple, el creixement mitjà observat entre els dos anys) o expressar-la com a "any anterior × factor de creixement"; així es converteix en una línia base estacional molt difícil de batre, i és la que la Marta hauria de fer servir per justificar el model davant del Diego. La lliçó: la línia base també ha de ser assenyada; una línia base absurda fa que qualsevol model sembli bo.

Conclusió

En aquesta lliçó hem après a avaluar amb honestedat. L'exactitud enganya amb classes desequilibrades (83,6 % sense detectar cap devolució); la matriu de confusió separa VP, FP, FN i VN, i la funció cost_negoci els tradueix a euros (30 € per devolució no anticipada, 5 € per falsa alarma), l'idioma del Diego. D'aquí surten la precisió, la sensibilitat/recall, l'especificitat i l'F1, i les corbes ROC (amb l'AUC, 0,84 per a la logística) i precisió-recall, que avaluen tots els llindars alhora. Després hem triat el llindar per cost (0,2 en comptes de 0,5, un 35 % menys de cost) i l'hem combinat amb la banda de revisió humana de 02-04. Per a la demanda hem fet servir MAE, RMSE i R² davant d'una línia base. I hem substituït la divisió única per validació creuada estratificada i temporal (TimeSeriesSplit), hem exigit una línia base i hem separat entrenament, validació i test, amb el test reservat per a una única mirada final. La comparació dels models de 04-04 amb aquestes eines ha canviat la classificació: la logística continua primera, l'arbre avança el bosc en cost i k veïns queda desemmascarat.

Queda una pregunta oberta des de 04-04: el bosc aleatori i l'arbre sense límit encertaven molt més en entrenament que en test, i k veïns amb k = 1 encertava el 100 % en entrenament i el 81 % en test. A l'última lliçó del mòdul, Sobreajust, Regularització i Ajust d'Hiperparàmetres, posarem nom a aquest fenomen, veurem com detectar-lo amb corbes d'aprenentatge i validació, quines tècniques el controlen (regularització, poda, ensamblatges) i com triar els hiperparàmetres amb GridSearchCV sense tocar el test, tancant el flux complet que la Marta ja domina.

Fonaments d'Intel·ligència Artificial (IA)

Mòdul 1: Introducció a la Intel·ligència Artificial

Mòdul 2: Principis Bàsics de la IA

Mòdul 3: Algorismes en IA

Mòdul 4: Aprenentatge Automàtic (Machine Learning)

Mòdul 5: Xarxes Neuronals i Deep Learning

Mòdul 6: Lògica i Sistemes Experts

Mòdul 7: Eines i Llenguatges de Programació en IA

Mòdul 8: Projectes i Casos d'Estudi

Mòdul 9: Exercicis i Pràctiques

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats