Fins ara hem treballat amb dades còmodes: la taula que retorna generar_comandes_ml no té buits, ni duplicats, ni dates, ni columnes de text lliure, i totes les seves columnes estan llestes per entrar a fit. El comandes.csv real de NovaMarket no s'hi assembla gens: a 02-03 vam veure que tenia imports buits, codis postals mal teclejats i comandes duplicades per una integració defectuosa. Allà vam aprendre a diagnosticar la qualitat i el biaix de les dades; en aquesta lliçó aprendrem a transformar-les perquè un algorisme les pugui fer servir: omplir o eliminar absents, tractar atípics, convertir categories en nombres sense enganyar el model, escalar, extreure informació de les dates, crear característiques noves amb coneixement del negoci, seleccionar les útils, evitar la fuita d'informació i, sobretot, encadenar tot això en un pipeline que s'ajusta només amb les dades d'entrenament. És important perquè aquí es decideix la major part de la qualitat d'un model: un algorisme mediocre amb bones característiques sol guanyar un algorisme sofisticat amb dades mal preparades, i perquè els errors d'aquesta fase (sobretot la fuita d'informació) produeixen models que semblen excel·lents al laboratori i fracassen en producció.

Contingut

  1. Per què la preparació s'emporta el 60-80 % del temps
  2. Les comandes "brutes" de NovaMarket: la funció embrutar_comandes
  3. Valors absents, duplicats i atípics
  4. Codificació de variables categòriques
  5. Escalat de variables numèriques
  6. Dates i enginyeria de característiques
  7. Selecció de característiques
  8. Fuita d'informació (data leakage)
  9. Dividir abans de transformar: Pipeline i ColumnTransformer
  10. Taula resum: quina tècnica per a cada tipus de columna
  11. Errors Comuns i Consells
  12. Exercicis
  13. Conclusió

  1. Per què la preparació s'emporta el 60-80 % del temps

Les enquestes a professionals de la dada coincideixen des de fa anys: entre el 60 % i el 80 % del temps d'un projecte de machine learning se'n va a obtenir, netejar i transformar dades, i només una fracció petita a entrenar models. Les raons són estructurals:

  • Els algorismes de scikit-learn (i de gairebé qualsevol llibreria) exigeixen una matriu numèrica sense buits: cada fila una instància, cada columna un nombre. Les dades de negoci vénen amb text, dates, buits i codis.
  • Les dades reals tenen errors (l'import de 99.999 € que era 99,99 €), inconsistències (la mateixa categoria escrita de tres maneres) i duplicats.
  • La informació útil poques vegades és a les columnes tal qual: cal construir-la (l'import per article, la taxa de devolució prèvia del client) a partir de coneixement del negoci. Aquesta "enginyeria de característiques" és on la Marta aporta més valor.
  • Hi ha decisions que no pot prendre l'algorisme: quines columnes existiran realment en el moment de predir, què fer amb un valor absent, si un atípic és un error o una dada valuosa.

A 02-03 vam construir un informe de qualitat (percentatge de nuls, duplicats, valors fora de rang, biaix per grups) amb el mòdul csv. Aquesta lliçó fa el pas següent: fer servir pandas i scikit-learn per transformar. La diferència d'enfocament és important: a 02-03 l'objectiu era saber què hi havia a les dades; aquí és deixar-les llestes per a fit sense introduir-hi trampes.

  1. Les comandes "brutes" de NovaMarket: la funció embrutar_comandes

Per practicar necessitem dades realistes. Partim de generar_comandes_ml (04-01) i hi afegim, amb codi, els problemes típics que la Marta va trobar a comandes.csv: identificadors, data de la comanda, dues columnes de text noves (mètode de pagament i tipus d'enviament), valors absents, un import impossible, files duplicades i una columna que només existeix després de la devolució (motiu_devolucio). Afegeix la funció a novamarket_ml.py:

import numpy as np
import pandas as pd

def embrutar_comandes(comandes, llavor=42):
    """Copia 'realista' de les comandes: NaN, duplicats, un atipic, dates, text i una fuita."""
    rng = np.random.default_rng(llavor)
    brut = comandes.copy()
    n = len(brut)
    brut.insert(0, "id_comanda", [f"P{100000 + i}" for i in range(n)])
    brut.insert(1, "id_client", rng.integers(1, 1201, size=n))            # uns 1.200 clients
    dates = pd.Timestamp("2025-09-01") + pd.to_timedelta(rng.integers(0, 90, size=n), unit="D")
    brut.insert(2, "data_comanda", dates)                                 # 90 dies de comandes
    brut["metode_pagament"] = rng.choice(["targeta", "paypal", "contrareemborsament", "bizum"],
                                         size=n, p=[0.55, 0.25, 0.08, 0.12])
    brut["tipus_enviament"] = rng.choice(["estandard", "rapid", "urgent"], size=n, p=[0.6, 0.3, 0.1])
    # valors absents
    brut.loc[rng.random(n) < 0.05, "dies_lliurament"] = np.nan
    brut.loc[rng.random(n) < 0.02, "import_comanda"] = np.nan
    brut.loc[rng.random(n) < 0.03, "metode_pagament"] = np.nan
    # un atipic impossible (error de tecleig)
    brut.loc[17, "import_comanda"] = 99999.0
    # columna que nomes existeix DESPRES de la devolucio (fuita d'informacio)
    motius = rng.choice(["no li agrada", "defectuos", "talla/model", "va arribar tard"], size=n)
    brut["motiu_devolucio"] = np.where(brut["retornat"] == 1, motius, "")
    # tres files duplicades
    brut = pd.concat([brut, brut.iloc[[5, 42, 300]]], ignore_index=True)
    return brut.sort_values("data_comanda").reset_index(drop=True)

comandes = generar_comandes_ml(3000, 42)
brut = embrutar_comandes(comandes, 42)
print(brut.shape)
print(brut.isna().sum())
print("Duplicats:", brut.duplicated().sum())
print(brut["import_comanda"].describe().round(1))

Sortida (resumida):

(3003, 13)
import_comanda          56
dies_lliurament        153
metode_pagament        100
(la resta de columnes)   0
Duplicats: 3
count     2947.0
mean       158.8
std       1841.7
min          5.9
50%        107.9
75%        163.6
max      99999.0

El diagnòstic (el que fèiem a 02-03, ara amb pandas): 3.003 files, tres de duplicades; 56 imports, 153 dies de lliurament i 100 mètodes de pagament absents; i un import màxim de 99.999 € que dispara la mitjana (158,8 € davant d'una mediana de 107,9 €) i la desviació típica. Nota: isna().sum() compta els nuls per columna, duplicated().sum() les files repetides i describe() dona els estadístics bàsics.

  1. Valors absents, duplicats i atípics

3.1 Duplicats

Gairebé sempre s'eliminen; el dubte és què vol dir "duplicat" (tota la fila igual, o el mateix id_comanda?). Aquí, la fila completa:

net = brut.drop_duplicates()
print("Despres de treure duplicats:", net.shape)      # (3000, 13)

3.2 Valors absents

Dues estratègies:

Estratègia Quan Com a scikit-learn
Eliminar files (o columnes) amb absents Poques files afectades i absència aleatòria; o columna amb >50 % buida df.dropna()
Imputar (omplir) amb un valor És l'habitual: no es llencen dades SimpleImputer(strategy=...)

Les estratègies d'imputació bàsiques: mitjana (numèriques simètriques), mediana (numèriques amb atípics, com import_comanda), moda o valor més freqüent (categòriques), constant ("desconegut", 0), i mètodes avançats (imputar amb un model a partir de les altres columnes, KNNImputer/IterativeImputer). Un consell extra: de vegades l'absència és informació (que falti el mètode de pagament pot indicar un canal concret); en aquest cas s'afegeix una columna binària "estava absent" (SimpleImputer(add_indicator=True)).

Encara no imputarem a mà: ho farem dins del pipeline de la secció 9, i de seguida veuràs per què és important.

3.3 Atípics (outliers)

Un atípic és un valor molt allunyat de la resta. El criteri més usat per detectar-los és el rang interquartílic (IQR): es calculen el primer i el tercer quartil (Q1, Q3), i es considera atípic el que queda per sota de Q1 − 1,5·IQR o per sobre de Q3 + 1,5·IQR:

q1, q3 = net["import_comanda"].quantile([0.25, 0.75])
iqr = q3 - q1
limit_superior = q3 + 1.5 * iqr
print(f"Q1={q1:.1f}  Q3={q3:.1f}  IQR={iqr:.1f}  limit superior={limit_superior:.1f}")
atipics = net[net["import_comanda"] > limit_superior]
print("Atipics per IQR:", len(atipics))
print(atipics["import_comanda"].sort_values(ascending=False).head(4).values)

Sortida:

Q1=63.8  Q3=163.7  IQR=99.9  limit superior=313.5
Atipics per IQR: 114
[99999.    632.61   608.63   578.5 ]

I aquí arriba la decisió que no pot prendre l'algorisme: el criteri marca 114 comandes, però només una és un error. Les 113 comandes entre 314 i 633 € són atípics estadístics i clients reals que a més es retornen molt (són justament els que preocupen el Diego); eliminar-los destruiria el senyal. El de 99.999 € és impossible a NovaMarket (cap producte no supera els 5.000 €) i és un error de tecleig. Les opcions davant d'un atípic:

  • Eliminar la fila si és un error evident o impossible (el nostre cas).
  • Corregir si es coneix l'error (99.999 → 99,99 si hi ha manera de confirmar-ho).
  • Retallar (winsoritzar) al límit: substituir tot el que superi un valor per aquest valor.
  • Deixar-lo i fer servir models robustos (arbres) o transformacions (logaritme de l'import).
net = net[net["import_comanda"].isna() | (net["import_comanda"] < 5000)].copy()
print("Despres de treure l'impossible:", net.shape)    # (2999, 13)

(Conservem les files amb import absent, isna(), per imputar-les després.)

  1. Codificació de variables categòriques

Els algorismes necessiten nombres, i categoria, metode_pagament, tipus_enviament i codi_postal_zona són text. La temptació és assignar enters arbitraris (llar=0, electronica=1, informatica=2, accessoris=3); és un error per a categories sense ordre: el model interpretaria que accessoris (3) és "més" que llar (0) i que informatica està "entre" totes dues, relacions que no existeixen. Les alternatives correctes depenen del tipus de categoria:

Tècnica Idea Quan Exemple
One-hot (OneHotEncoder) Una columna binària per categoria Nominals (sense ordre) amb poques categories categoriacategoria_llar, categoria_electronica, ... (0/1)
Ordinal (OrdinalEncoder amb ordre explícit) Un enter que respecta l'ordre real Ordinals (amb ordre) tipus_enviament: estandard=0 < rapid=1 < urgent=2
Freqüència Substituir cada categoria per la seva freqüència Nominals amb moltíssimes categories (milers de codis postals), quan el one-hot crearia massa columnes codi_postal → proporció de comandes d'aquest codi
Target encoding Substituir per la taxa mitjana de l'etiqueta en aquesta categoria Moltes categories; exigeix fer-ho dins de validació per no filtrar l'etiqueta codi_postal → taxa de devolució d'aquest codi

Amb OneHotEncoder(handle_unknown="ignore") el model no falla si en producció apareix una categoria nova ("crypto" com a mètode de pagament): simplement hi posa zeros. Amb OrdinalEncoder(categories=[[...]]) fixem nosaltres l'ordre; sense aquest argument assignaria enters alfabètics, és a dir, arbitraris.

  1. Escalat de variables numèriques

import_comanda va de 6 a 633 i num_articles d'1 a 5. Per a un arbre de decisió tant li fa (pregunta "import > 195?" sense comparar columnes), però per a qualsevol algorisme basat en distàncies (k veïns, k-means, SVM) o en gradient (regressió logística i lineal amb regularització, xarxes neuronals) les columnes grans dominen i l'entrenament se'n ressent. Dos escalats habituals:

Escalat Fórmula Resultat Quan
Estandardització (StandardScaler) (x − mitjana) / desviació típica Mitjana 0, desviació 1; sense límits Per defecte per a logística, SVM, k-NN, PCA, xarxes
Min-max (MinMaxScaler) (x − mín) / (màx − mín) Entre 0 i 1 Quan cal un rang acotat (algunes xarxes, imatges); sensible a atípics

Exemple amb tres comandes (import_comanda, num_articles) = (16,25, 2), (31,60, 1), (25,49, 3): estandarditzades queden (−1,30, 0), (1,13, −1,22), (0,17, 1,22); amb min-max, (0, 0,5), (1, 0), (0,6, 1). Ara les dues columnes pesen igual. Regla pràctica: escala sempre llevat que facis servir només arbres o boscos; i recorda que les mitjanes i desviacions es calculen amb el conjunt d'entrenament (secció 9).

  1. Dates i enginyeria de característiques

6.1 Dates

Una data no es pot fer servir tal qual, però conté informació: dia de la setmana, mes, si és festiu, quants dies han passat des d'un origen. pandas ho fa amb l'accessor .dt:

net["dia_setmana"] = net["data_comanda"].dt.dayofweek          # 0 = dilluns ... 6 = diumenge
net["mes"] = net["data_comanda"].dt.month
net["cap_de_setmana"] = (net["dia_setmana"] >= 5).astype(int)
net["dies_des_inici"] = (net["data_comanda"] - net["data_comanda"].min()).dt.days

Per a la previsió de demanda (04-04) farem servir el mateix sobre la sèrie setmanal: la setmana de l'any captura l'estacionalitat i les "vendes de la setmana anterior" (un lag) capturen la inèrcia.

6.2 Enginyeria de característiques

És crear columnes noves que expressin coneixement del negoci. Tres tipus que la Marta afegeix al predictor de devolucions:

# Ratio: quant costa cada article (una comanda de 400 € per 1 article no es el mateix que per 5)
net["import_per_article"] = net["import_comanda"] / net["num_articles"]

# Agregats per client, calculats NOMES amb les comandes anteriors a cadascuna
net = net.sort_values(["data_comanda", "id_comanda"])
per_client = net.groupby("id_client")
net["comandes_previes"] = per_client.cumcount()                          # 0 a la primera comanda
net["devolucions_previes"] = per_client["retornat"].cumsum() - net["retornat"]
net["taxa_devolucio_previa"] = (net["devolucions_previes"]
                                / net["comandes_previes"].replace(0, np.nan)).fillna(0)

Per al client 1124, per exemple, la seqüència de comandes queda: comandes prèvies 0, 1, 2, 3, 4, 5; devolucions prèvies 0, 0, 0, 0, 0, 1 (la seva cinquena comanda es va retornar i només compta a partir de la sisena); taxa prèvia 0, ..., 0, 0,2. Fixa't en el detall de restar net["retornat"]: cumsum() inclou la fila actual, i la devolució de la comanda actual no es coneix quan cal predir-la. Aquesta cura és la diferència entre una característica legítima i una fuita (secció 8).

Text bàsic: per a ressenyes.csv (cas 4) les primeres característiques són la longitud de la ressenya, el nombre de signes d'exclamació o la presència de paraules clau ("trencat", "tard", "perfecte"). En pandas: ressenyes["text"].str.len(), ressenyes["text"].str.count("!"), ressenyes["text"].str.contains("trencat|danyat").astype(int). La representació seriosa del text (bosses de paraules, embeddings) pertany al mòdul 5.

  1. Selecció de característiques

Més columnes no sempre és millor: les irrellevants afegeixen soroll, allarguen l'entrenament i faciliten el sobreajust (04-06). Dues eines senzilles per decidir:

  • Correlació amb l'etiqueta (numèriques): net[cols + ["retornat"]].corr()["retornat"]. A les nostres dades: import_comanda 0,35, client_nou 0,30, import_per_article 0,28, dies_lliurament 0,16, num_articles −0,08, i pràcticament 0 per a dia_setmana, cap_de_setmana, comandes_previes i taxa_devolucio_previa (aquesta última perquè, amb 90 dies d'històric, gairebé cap client no té devolucions prèvies; amb dos anys de dades seria de les millors). La correlació només detecta relacions lineals: una característica amb correlació zero pot ser útil combinada amb una altra.
  • Importància de característiques d'un model: els boscos aleatoris (04-04) reparteixen un 100 % d'"importància" entre les columnes segons quant ajuden a separar. Entrenant-ne un amb el pipeline de la secció 9 obtenim: import_comanda 0,27, import_per_article 0,19, client_nou 0,17, dies_lliurament 0,08, ... i les tres columnes de codi_postal_zona al voltant de 0,01 cadascuna. És la comprovació que vam anunciar a 04-01: la zona no aporta res, com correspon a unes dades on no influeix. A 02-04 vam veure el cas contrari, un històric esbiaixat on el model aprenia a sospitar de la zona B; la importància de característiques és una de les eines per detectar-ho.

Amb aquesta informació la Marta pot treure dia_setmana, mes i cap_de_setmana (soroll en aquestes dades), i plantejar-se treure codi_postal_zona també per prudència ètica, no només estadística.

  1. Fuita d'informació (data leakage)

És l'error més perillós d'aquesta fase, perquè no produeix errades sinó resultats massa bons. Hi ha fuita quan el model fa servir, en entrenar, informació que no existirà en el moment de predir. La columna motiu_devolucio és l'exemple perfecte: s'omple quan el client retorna, així que és buida a totes les comandes no retornades i plena a totes les retornades:

print(pd.crosstab(net["motiu_devolucio"] != "", net["retornat"]))
retornat             0    1
motiu_devolucio
False             2506    0
True                 0  493

Si afegim una característica hi_ha_motiu al model, encerta el 100 % en test (ho hem comprovat). És un resultat espectacular i completament inútil: quan arriba una comanda nova, el motiu de devolució encara no existeix. Altres fuites freqüents, més subtils:

  • Columnes omplertes després de l'esdeveniment: data_devolucio, import_reemborsat, estat_final = "reemborsat".
  • Agregats calculats amb totes les dades, inclòs el futur: la taxa de devolució del client incloent-hi la comanda actual (per això restem retornat a 6.2), o la mitjana de vendes de tot l'any en preveure una setmana d'aquell any.
  • Transformacions ajustades amb el test: imputar amb la mediana de tot el dataset, escalar amb la mitjana de tot el dataset. És una fuita petita, però fuita.
  • Duplicats repartits entre entrenament i test: el model "ja ha vist" l'exemple.

La regla per detectar-la: per a cada columna, pregunta't "tindré aquest valor, exactament així, en el moment en què el model hagi de decidir?". I desconfia de resultats massa bons.

  1. Dividir abans de transformar: Pipeline i ColumnTransformer

Tot l'anterior convergeix en una regla i una eina. La regla: primer es divideix en entrenament i test, i totes les transformacions que "aprenen alguna cosa" de les dades (la mediana per imputar, la mitjana i desviació per escalar, les categories del one-hot) s'ajusten només amb l'entrenament (fit en train) i després s'apliquen tal qual al test (transform en test). L'eina: Pipeline encadena passos, i ColumnTransformer aplica a cada grup de columnes la seva pròpia cadena. Així el pipeline complet, preparació + model, es comporta com un únic model amb fit/predict, i és impossible equivocar-se en l'ordre.

from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression

# 1) Traiem l'etiqueta, la fuita i les columnes identificadores (no son caracteristiques)
X = net.drop(columns=["retornat", "motiu_devolucio", "id_comanda", "id_client",
                      "data_comanda", "devolucions_previes"])
y = net["retornat"]

# 2) PRIMER dividim
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y)

# 3) Definim que fer amb cada grup de columnes
numeriques = ["import_comanda", "num_articles", "dies_lliurament", "import_per_article",
              "comandes_previes", "taxa_devolucio_previa", "dies_des_inici"]
binaries = ["client_nou", "cap_de_setmana"]
nominals = ["categoria", "codi_postal_zona", "metode_pagament"]
ordinals = ["tipus_enviament"]

preparacio = ColumnTransformer([
    ("num", Pipeline([("imputar", SimpleImputer(strategy="median")),
                      ("escalar", StandardScaler())]), numeriques),
    ("bin", "passthrough", binaries),
    ("nom", Pipeline([("imputar", SimpleImputer(strategy="most_frequent")),
                      ("onehot", OneHotEncoder(handle_unknown="ignore"))]), nominals),
    ("ord", OrdinalEncoder(categories=[["estandard", "rapid", "urgent"]]), ordinals),
])

# 4) Pipeline complet: preparacio + model
model = Pipeline([("preparacio", preparacio),
                  ("classificador", LogisticRegression(max_iter=1000))])

# 5) fit ajusta TOT (medianes, escales, categories i el classificador) nomes amb train
model.fit(X_train, y_train)
print("Encerts en test:", round(model.score(X_test, y_test), 3))
matriu = preparacio.transform(X_train)
print("Matriu que veu el classificador:", matriu.shape)
print(preparacio.get_feature_names_out()[:6])

Sortida:

Encerts en test: 0.871
Matriu que veu el classificador: (2249, 21)
['num__import_comanda' 'num__num_articles' 'num__dies_lliurament'
 'num__import_per_article' 'num__comandes_previes' 'num__taxa_devolucio_previa']

Explicació:

  • Cada grup de columnes rep el seu tractament: les numèriques s'imputen amb la mediana (robusta a atípics) i s'estandarditzen; les binàries passen tal qual (passthrough); les nominals s'imputen amb la moda i es converteixen a one-hot; l'ordinal rep enters amb l'ordre que fixem. Les 15 columnes d'entrada es converteixen en 21 columnes numèriques (el one-hot expandeix categoria en 4, codi_postal_zona en 3 i metode_pagament en 4).
  • model.fit(X_train, y_train) calcula medianes, mitjanes, desviacions i categories amb X_train i entrena la regressió logística; model.score(X_test, y_test) aplica aquestes mateixes medianes i escales a X_test (sense recalcular-les) i avalua. La fuita de la secció 8 (tercer punt) queda descartada per construcció.
  • Quan el model passi a producció, se li lliura el pipeline sencer: rep una comanda tal com arriba (amb els seus textos i els seus buits) i retorna la predicció. Ningú no ha de recordar "primer imputar amb 107,9, després restar 125 i dividir per 84".
  • L'encert (87,1 %) és semblant al de l'arbre de 04-01 i al de la logística de 04-02, malgrat tenir més columnes i dades més brutes. És normal: l'exactitud és una mesura tosca (04-05) i diverses columnes noves són soroll en aquestes dades; el valor del pipeline és que ara és correcte, reproduïble i desplegable, i que les característiques de negoci (ratios, historial) estan a punt per a quan l'històric sigui més llarg.

Un últim detall útil: model.named_steps["classificador"].coef_ dona els coeficients de la logística per columna transformada. En aquestes dades, els més grans en valor absolut són client_nou (2,0), import_comanda (1,0), dies_lliurament (0,6) i categoria_electronica (0,5): el model ha redescobert la "veritat oculta" del generador de 04-01. A 04-04 aprendrem a llegir-los amb propietat.

9.1 Empaquetar la preparació per a les lliçons següents

Les lliçons 04-04, 04-05 i 04-06 reutilitzaran exactament aquesta preparació. Per no repetir codi, afegeix a novamarket_ml.py dues funcions: preparar_comandes(brut), que aplica la neteja i l'enginyeria de les seccions 3 i 6 i retorna X i y, i crear_preparacio(), que construeix el ColumnTransformer de la secció 9 (una instància nova cada vegada, perquè cada pipeline ajusti la seva):

def preparar_comandes(brut):
    """Neteja + enginyeria de 04-03: retorna X (caracteristiques) i y (retornat)."""
    net = brut.drop_duplicates()
    net = net[net["import_comanda"].isna() | (net["import_comanda"] < 5000)].copy()
    net["dia_setmana"] = net["data_comanda"].dt.dayofweek
    net["mes"] = net["data_comanda"].dt.month
    net["cap_de_setmana"] = (net["dia_setmana"] >= 5).astype(int)
    net["dies_des_inici"] = (net["data_comanda"] - net["data_comanda"].min()).dt.days
    net["import_per_article"] = net["import_comanda"] / net["num_articles"]
    net = net.sort_values(["data_comanda", "id_comanda"])
    per_client = net.groupby("id_client")
    net["comandes_previes"] = per_client.cumcount()
    net["devolucions_previes"] = per_client["retornat"].cumsum() - net["retornat"]
    net["taxa_devolucio_previa"] = (net["devolucions_previes"]
                                    / net["comandes_previes"].replace(0, np.nan)).fillna(0)
    X = net.drop(columns=["retornat", "motiu_devolucio", "id_comanda", "id_client",
                          "data_comanda", "devolucions_previes"])
    y = net["retornat"]
    return X, y

NUMERIQUES = ["import_comanda", "num_articles", "dies_lliurament", "import_per_article",
              "comandes_previes", "taxa_devolucio_previa", "dies_des_inici"]
BINARIES = ["client_nou", "cap_de_setmana"]
NOMINALS = ["categoria", "codi_postal_zona", "metode_pagament"]
ORDINALS = ["tipus_enviament"]

def crear_preparacio():
    """ColumnTransformer de 04-03, a punt per encadenar amb qualsevol model."""
    from sklearn.pipeline import Pipeline
    from sklearn.compose import ColumnTransformer
    from sklearn.impute import SimpleImputer
    from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler
    return ColumnTransformer([
        ("num", Pipeline([("imputar", SimpleImputer(strategy="median")),
                          ("escalar", StandardScaler())]), NUMERIQUES),
        ("bin", "passthrough", BINARIES),
        ("nom", Pipeline([("imputar", SimpleImputer(strategy="most_frequent")),
                          ("onehot", OneHotEncoder(handle_unknown="ignore"))]), NOMINALS),
        ("ord", OrdinalEncoder(categories=[["estandard", "rapid", "urgent"]]), ORDINALS),
    ])

A partir d'ara, obtenir les dades a punt serà una línia: X, y = preparar_comandes(embrutar_comandes(generar_comandes_ml(3000, 42), 42)), i qualsevol model s'entrena amb Pipeline([("prep", crear_preparacio()), ("model", ...)]).

  1. Taula resum: quina tècnica per a cada tipus de columna

Tipus de columna Exemple Absents Transformació Compte amb
Numèrica contínua import_comanda, import_per_article Mediana (o mitjana si és simètrica) Estandarditzar (o min-max); de vegades logaritme Atípics: decidir si són errors o senyal
Numèrica discreta / recompte num_articles, comandes_previes Mediana o 0 si "absent = cap" Estandarditzar Els recomptes previs han d'excloure l'esdeveniment actual
Binària client_nou, cap_de_setmana Moda o constant Cap (passthrough) Que sigui 0/1, no "sí"/"no"
Categòrica nominal categoria, metode_pagament Moda o categoria "desconegut" One-hot; freqüència/target si hi ha moltes categories Mai enters arbitraris; handle_unknown
Categòrica ordinal tipus_enviament Moda Ordinal amb ordre explícit Fixar l'ordre a mà
Data data_comanda Poques vegades absent Extreure dia de la setmana, mes, dies des de…, lags No fer servir la data en brut ni dates posteriors a l'esdeveniment
Text lliure ressenya, descripció Cadena buida Longitud, recomptes, paraules clau (mòdul 5 per a més) Cost de processament; idioma
Identificador id_comanda, id_client Eliminar (serveix per agrupar, no com a característica) Un model pot memoritzar l'id
Posterior a l'esdeveniment motiu_devolucio, data_devolucio Eliminar Fuita d'informació

Errors Comuns i Consells

  • Imputar o escalar abans de dividir. És la fuita silenciosa més comuna. Divideix primer; ajusta les transformacions dins d'un Pipeline amb les dades d'entrenament.
  • Codificar categories amb enters arbitraris. Inventa un ordre que no existeix. Fes servir one-hot per a nominals i ordinal només quan l'ordre sigui real i explícit.
  • Eliminar tots els atípics "perquè ho diu l'IQR". El criteri detecta candidats; la decisió (error o senyal) és de negoci. Les comandes de 400 € són les que el Diego vol vigilar, no les que cal esborrar.
  • Fer servir columnes del futur. Abans d'incloure una columna, pregunta't si existirà amb aquest valor en el moment de la predicció. Un resultat sospitosament perfecte gairebé sempre és una fuita.
  • Confondre identificadors amb característiques. id_client serveix per calcular agregats per client; mai com a nombre que entra al model.
  • No escalar abans de fer servir distàncies o gradient. k-NN, k-means, SVM, logística regularitzada i xarxes ho necessiten; els arbres no.
  • Oblidar handle_unknown="ignore". En producció apareixeran categories noves i el pipeline fallarà en transformar.
  • Fer la preparació a mà en un quadern i no desar-la. Si no és en un Pipeline, no es pot reproduir ni desplegar.

Exercicis

Exercici 1. Canvia al pipeline la imputació de les numèriques de median a mean i afegeix add_indicator=True al SimpleImputer de les nominals (crearà una columna que marca si el mètode de pagament faltava). Quantes columnes té ara la matriu transformada? Canvia l'encert de manera apreciable? Justifica per què la mediana era l'opció més prudent per a import_comanda quan encara hi era el valor 99.999.

Exercici 2. Comet expressament la fuita "escalar abans de dividir": aplica StandardScaler().fit_transform a les columnes numèriques del dataset complet (imputa abans amb la mediana global), divideix després i entrena la logística. Compara l'encert amb el del pipeline correcte. Es nota gaire? Per què aquesta fuita és menys greu que la de motiu_devolucio i tot i així cal evitar-la?

Exercici 3. Prepara la sèrie de demanda de 04-02 per a un model supervisat: a partir de generar_demanda_setmanal(104, 42), crea les columnes setmana_any (data.dt.isocalendar().week), mes, unitats_setmana_anterior (unitats.shift(1)) i mitjana_4_setmanes (mitjana mòbil de les 4 setmanes prèvies, unitats.shift(1).rolling(4).mean()). Per què cal fer servir shift(1) abans de la mitjana mòbil? Què passa amb les primeres files i què en faries?

Solucions

Solució 1. La matriu passa de 21 a 23 columnes: l'indicador d'absència es genera abans de l'OneHotEncoder del mateix sub-pipeline, així que aquest el tracta com una categoria més i l'expandeix en dues columnes (nom__missingindicator_metode_pagament_False/True); si volguessis una sola columna 0/1 hauries de treure l'indicador fora del one-hot. L'encert en test es manté al voltant de 0,87 (les diferències són d'una o dues mil·lèsimes, dins del soroll). La mediana era més prudent perquè, si el 99.999 hagués passat desapercebut, la mitjana d'import_comanda hauria pujat de 125 € a 159 € i tots els imports absents s'haurien omplert amb un valor inflat; la mediana (107,9 €) no es va moure ni un cèntim amb l'atípic. En general, mediana per a columnes amb cua llarga o atípics, mitjana per a columnes simètriques i netes.

Solució 2. L'encert queda pràcticament igual (al voltant de 0,87). Amb 3.000 files, la mitjana i la desviació calculades amb el 100 % de les dades amb prou feines difereixen de les calculades amb el 75 %, així que l'efecte és imperceptible. És menys greu que motiu_devolucio perquè no introdueix l'etiqueta a les característiques, només un pessic d'informació sobre la distribució del test. Tot i així cal evitar-la perquè: (a) amb datasets petits o amb atípics al test sí que distorsiona; (b) és el senyal d'un flux de treball mal muntat, en què altres fuites més greus passaran desapercebudes; i (c) en producció no existeix "el test": l'escalador ha d'estar ajustat i desat per endavant, que és justament el que garanteix el Pipeline.

Solució 3. shift(1) desplaça la sèrie una fila cap avall, de manera que a la fila de la setmana t apareixen les unitats de la setmana t−1; sense això, rolling(4).mean() inclouria la mateixa setmana t, és a dir, l'etiqueta que es vol predir: una fuita. Les primeres files queden amb NaN (la setmana 1 no en té d'anterior; les 4 primeres no tenen mitjana mòbil completa). Es poden eliminar (dropna(), perdent 4 setmanes de 104) o imputar dins del pipeline; amb una sèrie llarga, eliminar-les és el més net. La columna setmana_any captura l'estacionalitat i les de lag la inèrcia; a 04-04 entrenarem amb elles la regressió lineal i comprovarem que l'error mitjà baixa molt respecte a la recta de 04-02.

Conclusió

En aquesta lliçó hem convertit unes comandes "brutes" de NovaMarket en la matriu numèrica que un algorisme necessita, i pel camí hem fixat les tècniques de preparació: duplicats (eliminar), absents (imputar amb mediana/moda dins del pipeline), atípics (detectar amb IQR, decidir amb criteri de negoci), codificació de categòriques (one-hot per a nominals, ordinal amb ordre explícit, freqüència o target per a moltes categories; mai enters arbitraris), escalat (estandardització per a models de distància i gradient), dates (dia de la setmana, mes, dies des de…), enginyeria de característiques (ratios, agregats per client sense mirar el present, text bàsic), selecció (correlació i importància, que va confirmar que la zona no aporta res) i la fuita d'informació (motiu_devolucio donava un 100 % il·lusori). Tot plegat encapsulat en un Pipeline amb ColumnTransformer que s'ajusta només amb l'entrenament i que és reproduïble i desplegable. La Marta té ara la funció embrutar_comandes per practicar i un pipeline que reutilitzarem a les tres lliçons següents.

Amb les dades preparades, toca obrir la caixa dels algorismes. A la lliçó següent, Algorismes de Machine Learning, veurem què hi ha dins de fit: com la regressió lineal ajusta una recta a la demanda del NovaClean, com la regressió logística converteix una suma ponderada en probabilitat de devolució, com decideixen els k veïns, els arbres, els boscos, les màquines de vectors de suport i k-means, amb la intuïció, una mica de matemàtica i codi per a cadascun, i compararem diversos classificadors sobre aquestes mateixes comandes.

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