A la lliçó anterior vam acabar amb uns quants NaN sembrats a propòsit: vam convertir edats impossibles i dates futures en valors absents perquè era més honest que inventar-nos un nombre. Ara toca decidir què fem amb aquests forats, i amb tots els que ja venien d'origen en les dades de MercaFresh. Les dades mancants són inevitables en qualsevol projecte real, i la manera de tractar-les pot canviar del tot les conclusions d'un model: eliminar files pot esbiaixar la mostra, imputar malament pot fabricar patrons falsos, i fer-ho en el moment equivocat pot filtrar informació que el model no hauria de tenir. En aquesta lliçó aprendràs a diagnosticar per què falten les dades, a visualitzar-ne els patrons i a triar entre eliminar i imputar amb criteri.

Contingut

  1. Per què falten dades: MCAR, MAR i MNAR
  2. Detecció i visualització de nuls
  3. Eliminació: files i columnes
  4. Imputació simple: mitjana, mediana, moda i constants
  5. SimpleImputer de scikit-learn
  6. Imputació avançada: KNNImputer i indicadors d'absència
  7. El risc silenciós: fuita d'informació

Per què falten dades: MCAR, MAR i MNAR

Abans d'omplir un forat cal preguntar-se per què és buit. L'estadística distingeix tres mecanismes d'absència, i la diferència no és acadèmica: determina quines estratègies són vàlides.

MCAR: Missing Completely At Random

L'absència no depèn de res: és pur atzar. A MercaFresh, una fallada intermitent del servidor va perdre el camp temps_lliurament d'un 2 % de comandes triades a l'atzar. Cap tipus de comanda no tenia més probabilitat de perdre'l que un altre.

  • Conseqüència: les dades que queden continuen sent representatives. Eliminar aquestes files no esbiaixa; només redueix la mostra.

MAR: Missing At Random (condicionat a allò observat)

L'absència depèn d'altres columnes que sí que tenim. A MercaFresh, l'edat falta molt més entre els clients que es van registrar per l'app mòbil (on el camp era opcional) que entre els del formulari web (on era obligatori). Sabent el canal de registre, l'absència ja no aporta informació extra.

  • Conseqüència: eliminar files esbiaixa la mostra (perdríem usuaris d'app, que potser tenen un altre perfil de churn), però es pot imputar bé fent servir les columnes relacionades.

MNAR: Missing Not At Random

L'absència depèn del mateix valor que falta. Exemple clàssic a MercaFresh: la valoració post-lliurament (satisfaccio) falta sobretot entre els clients descontents, que no es molesten a respondre l'enquesta. El forat és informació: qui no respon tendeix a estar insatisfet.

  • Conseqüència: és el cas més perillós. Imputar la mitjana fabricaria clients "mitjanament satisfets" que en realitat estaven emprenyats — justament els que faran churn. Aquí els indicadors d'absència (els veurem més avall) són gairebé obligatoris.
Mecanisme De què depèn l'absència? Exemple MercaFresh Estratègia raonable
MCAR De res (atzar pur) Fallada aleatòria del servidor Eliminar o imputar: totes dues segures
MAR D'altres columnes observades L'edat falta segons el canal de registre Imputar fent servir aquestes columnes
MNAR Del mateix valor absent No responen els insatisfets Indicador d'absència + cautela

A la pràctica no hi ha cap test definitiu per distingir-los: es raona amb coneixement del negoci. La pregunta útil és sempre: qui o què va decidir no omplir aquest camp, i té això relació amb el que vull predir?

Detecció i visualització de nuls

Reconstruïm un extracte de clients de MercaFresh, ja net de duplicats i valors impossibles (herència de 03-01), però amb els seus nuls:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "id_client": [101, 104, 105, 106, 107, 108, 109, 110, 111, 112],
    "edat": [34, np.nan, 41, 38, np.nan, 45, 31, 27, np.nan, 52],
    "canal_registre": ["web", "app", "web", "app", "app", "web", "app", "web", "app", "web"],
    "despesa_total": [1250.5, 0.0, 2100.75, 310.2, 15400.0, 670.4, 0.0, 980.1, 45.9, 1530.0],
    "satisfaccio": [4.5, np.nan, 4.8, np.nan, 4.2, 3.9, np.nan, 4.6, np.nan, 4.1],
    "temps_lliurament_min": [42, 55, np.nan, 61, 38, 47, 52, np.nan, 66, 44],
})

El recompte bàsic:

# Nuls per columna, en absolut i en percentatge
print(df.isna().sum())
print((df.isna().mean() * 100).round(1))
# edat                   30.0 %
# satisfaccio            40.0 %
# temps_lliurament_min   20.0 %

isna() retorna un DataFrame de booleans (True on hi ha nul); sum() els compta i mean() dona directament la proporció. Però els totals no ho expliquen tot: importa el patró. Falten sempre a les mateixes files? Falta satisfaccio en els mateixos clients que edat?

# Quantes columnes falten per fila?
print(df.isna().sum(axis=1).value_counts())

# Coincideixen els nuls de dues columnes?
print(pd.crosstab(df["edat"].isna(), df["satisfaccio"].isna()))

# Pista sobre el mecanisme: l'absencia d'edat depen del canal?
print(df.groupby("canal_registre")["edat"].apply(lambda s: s.isna().mean()))
# app    0.6   <- falta el 60 % a l'app i el 0 % al web: fa olor de MAR!
# web    0.0

Aquest darrer groupby és l'eina detectiu: creua l'absència amb altres columnes per intuir el mecanisme. Per a datasets grans, un mapa visual ajuda:

import matplotlib.pyplot as plt

plt.imshow(df.isna(), aspect="auto", cmap="gray_r")
plt.xticks(range(len(df.columns)), df.columns, rotation=45)
plt.ylabel("Fila")
plt.title("Mapa de nuls (negre = absent)")
plt.tight_layout()
plt.show()

Cada cel·la negra és un forat: bandes verticals indiquen columnes problemàtiques; patrons alineats entre columnes, absències correlacionades. (La llibreria missingno automatitza aquests gràfics, però amb imshow en tens prou amb les eines que ja tens.)

Eliminació: files i columnes

L'opció més simple és esborrar. dropna la implementa amb diversos matisos:

# Eliminar tota fila amb ALMENYS un nul (agressiu: aqui perdriem 7 de 10)
df_sense_nuls = df.dropna()

# Eliminar nomes files on falti una columna critica concreta
df_edat = df.dropna(subset=["edat"])

# Eliminar files amb mes de 2 nuls (thresh = minim de valors NO nuls exigit)
df_thresh = df.dropna(thresh=len(df.columns) - 2)

# Eliminar una COLUMNA sencera
df_sense_satisf = df.drop(columns=["satisfaccio"])

Quan és acceptable eliminar?

  • Files: quan els nuls són pocs (regla orientativa: < 5 % de les files) i el mecanisme és MCAR. Si és MAR o MNAR, eliminar files distorsiona la mostra: en el nostre exemple, esborrar les files sense edat eliminaria sobretot clients d'app.
  • Columnes: quan en falta una proporció enorme (> 50-60 %) i la columna no és crítica per al negoci. Compte amb l'excepció MNAR: satisfaccio falta un 40 %, però que falti pot predir churn — abans de llençar-la, considera conservar-ne l'indicador d'absència.
  • Mai no eliminis la fila si el nul és a la variable objectiu d'una dada de producció que vols predir; només en entrenament té sentit descartar files sense etiqueta.

Imputació simple: mitjana, mediana, moda i constants

Imputar és omplir el forat amb un valor estimat. Les receptes bàsiques amb pandas:

# Mitjana: per a numeriques simetriques
df["edat"] = df["edat"].fillna(df["edat"].mean())

# Mediana: per a numeriques amb outliers o asimetria (recorda 02-01!)
df["temps_lliurament_min"] = df["temps_lliurament_min"].fillna(
    df["temps_lliurament_min"].median())

# Moda: per a categoriques ([0] perque mode() pot retornar diversos empats)
df["canal_registre"] = df["canal_registre"].fillna(
    df["canal_registre"].mode()[0])

# Constant amb significat de negoci
df["satisfaccio"] = df["satisfaccio"].fillna(-1)   # -1 = "no va respondre"

Mitjana o mediana? El criteri ve directe del mòdul 2: la mitjana és sensible als outliers, la mediana no. Amb el client-restaurant de 15.400 € al dataset, imputar despesa_total amb la mitjana inflaria tots els forats; la mediana és la tria robusta per defecte.

Una imputació per grups aprofita l'estructura MAR: si l'edat falta segons el canal, imputa amb la mediana de cada canal:

df["edat"] = df["edat"].fillna(
    df.groupby("canal_registre")["edat"].transform("median"))

Cost ocult de tota imputació simple: redueix artificialment la variància. Cada forat omplert amb la mediana és un punt clavat al centre de la distribució; si imputes el 30 % d'una columna, el seu histograma tindrà un pic artificial i les seves correlacions (02-03) s'afebliran. Imputar molt no surt gratis.

SimpleImputer de scikit-learn

Per què fer servir scikit-learn si fillna ja funciona? Perquè SimpleImputer memoritza el valor amb què imputa (l'aprèn a fit) i el reaplica idèntic a dades noves (transform). Aquesta separació fit/transform és la clau de l'apartat final d'aquesta lliçó, i el patró general de tota la llibreria.

from sklearn.impute import SimpleImputer

# Imputador per a columnes numeriques, amb estrategia mediana
imp_num = SimpleImputer(strategy="median")

cols_num = ["edat", "temps_lliurament_min"]
df[cols_num] = imp_num.fit_transform(df[cols_num])

# El valor apres queda guardat, a punt per aplicar-se a dades futures:
print(imp_num.statistics_)    # [37.0, 49.5] p. ex.: medianes de cada columna

Estratègies disponibles: "mean", "median", "most_frequent" (serveix per a categòriques) i "constant" (amb fill_value=). El mateix objecte, ja ajustat, s'aplica després amb imp_num.transform(dades_noves) — sense recalcular res.

Imputació avançada: KNNImputer i indicadors d'absència

KNNImputer

La imputació simple ignora el context de la fila: a un client jove de despesa alta i a un jubilat de compra ocasional els assigna la mateixa edat mediana. KNNImputer és més fi: per a cada forat busca les k files més semblants en les altres columnes (els seus "veïns") i en fa la mitjana dels valors.

from sklearn.impute import KNNImputer

imp_knn = KNNImputer(n_neighbors=3)
df[["edat", "despesa_total", "temps_lliurament_min"]] = imp_knn.fit_transform(
    df[["edat", "despesa_total", "temps_lliurament_min"]])

La mecànica interna de "veïns més propers" és exactament l'algorisme K-NN que estudiarem a fons a la lliçó 04-05; de moment n'hi ha prou amb la intuïció: omple cada forat copiant de les files que més s'hi assemblen. Dues advertències pràctiques: només treballa amb columnes numèriques, i com que mesura distàncies, les columnes amb escales grans dominen el càlcul — un problema que resoldrem a la lliçó 03-05.

Indicadors d'absència

Quan l'absència és informativa (MNAR), imputar i callar destrueix el senyal. La solució: afegir una columna booleana que recordi on hi havia un forat, i després imputar tranquil·lament.

# 1) Guardar l'empremta de l'absencia ABANS d'imputar
df["satisfaccio_absent"] = df["satisfaccio"].isna().astype(int)

# 2) Ara si, imputar la columna original
df["satisfaccio"] = df["satisfaccio"].fillna(df["satisfaccio"].median())

Així el model de churn pot aprendre el patró real: "els clients que no responen l'enquesta es donen de baixa més". SimpleImputer(add_indicator=True) genera aquests indicadors automàticament. És una tècnica barata i sorprenentment potent: en molts datasets, l'indicador acaba sent millor predictor que la columna imputada.

El risc silenciós: fuita d'informació

Queda l'advertència més important de la lliçó. Al mòdul 6 (lliçó 06-01) veurem que les dades es divideixen en un conjunt d'entrenament i un altre de prova, i que aquest últim ha de simular dades del futur que el model no ha vist mai. Doncs bé: si calcules la mediana per imputar fent servir tot el dataset i després divideixes, les dades de prova hauran influït en aquell valor. El model haurà "espiat" el futur. És el que s'anomena fuita d'informació (data leakage).

# MALAMENT: la mediana es calcula amb TOTES les dades, incloses les de prova
df["edat"] = df["edat"].fillna(df["edat"].median())
# ... i despres es divideix en train/test

# BE (esquema conceptual, es desenvolupa a 06-01):
# 1. dividir primer en train i test
# 2. imp = SimpleImputer(strategy="median"); imp.fit(X_train)  <- apren NOMES de train
# 3. X_train = imp.transform(X_train)
# 4. X_test  = imp.transform(X_test)   <- aplica el valor apres a train

Aquí és on la parella fit/transform de scikit-learn deixa de ser una mania estilística i es converteix en la barrera contra la fuita: s'aprèn de train, s'aplica a tot. L'efecte de fer-ho malament és traïdor perquè no dona cap error: senzillament la teva avaluació sortirà millor del que el model mereix, i el desengany arribarà en producció. Guarda aquesta idea: reapareixerà amb l'escalat (03-05), amb el target encoding (03-04) i en general amb qualsevol pas que "aprengui" alguna cosa de les dades.

Errors Comuns i Consells

  • Imputar sense preguntar-se el mecanisme. Omplir satisfaccio (MNAR) amb la mitjana fabrica clients contents que no existeixen. Primer diagnostica (MCAR/MAR/MNAR), després actua.
  • Fer servir la mitjana en columnes amb outliers. Amb el client de 15.400 € a dins, la mitjana de despesa no representa ningú. Mediana per defecte; mitjana només en distribucions raonablement simètriques.
  • Eliminar files alegrement. Un dropna() sense més pot buidar-te mig dataset i esbiaixar la resta. Mira sempre quantes files perds i qui són.
  • Imputar abans de dividir train/test. La fuita d'informació no avisa: infla les teves mètriques en silenci. Aprèn els valors d'imputació només amb dades d'entrenament.
  • Oblidar l'indicador d'absència. Si sospites MNAR, crea la columna _absent abans d'imputar; després ja no la podràs reconstruir.
  • Confondre nuls amb sentinelles. Un 999 o un -1 heretats del sistema origen no són NaN per a pandas: converteix-los primer (df.replace(999, np.nan)) o els teus recomptes de nuls mentiran.
  • Consell: deixa registrat al codi quants nuls hi havia, quina estratègia vas aplicar a cada columna i per què. La imputació és una decisió de modelatge, no una feina de lampisteria.

Exercicis

Exercici 1

Per a cada escenari de MercaFresh, classifica el mecanisme (MCAR, MAR o MNAR) i justifica-ho en una frase: (a) el pes de la comanda falta als registres d'una setmana en què la bàscula del magatzem va estar espatllada; (b) el codi postal falta més en comandes fetes per telèfon, perquè l'operador de vegades l'omet; (c) els ingressos declarats falten sobretot entre els clients de rendes altes, que prefereixen no dir-los.

Exercici 2

Amb el DataFrame df de la lliçó (versió amb nuls), imputa temps_lliurament_min amb la mediana per ciutat sabent que existeix una columna ciutat. Escriu el codi amb groupby + transform i explica per què pot ser millor que la mediana global.

Exercici 3

Crea un SimpleImputer amb estratègia mediana i add_indicator=True, ajusta'l sobre les columnes ["edat", "satisfaccio"] i mostra la forma (shape) del resultat. Explica per què surten més columnes de les que van entrar.

Solucions

Exercici 1

  • (a) MCAR: l'avaria no té relació amb el tipus de comanda ni amb el valor del pes; és atzar (temporal) pur.
  • (b) MAR: l'absència depèn del canal (columna observada), no del valor del mateix codi postal; coneixent el canal, es pot imputar sense biaix.
  • (c) MNAR: l'absència depèn del valor que falta (les rendes altes s'amaguen més); imputar la mitjana subestimaria els ingressos absents, i convé un indicador d'absència.

Exercici 2

df["temps_lliurament_min"] = df["temps_lliurament_min"].fillna(
    df.groupby("ciutat")["temps_lliurament_min"].transform("median"))

transform("median") retorna una Serie de la mateixa mida que df amb la mediana del grup de cada fila, així que fillna omple cada forat amb la mediana de la seva ciutat. És millor que la global si el temps de lliurament varia sistemàticament per ciutat (repartir a Barcelona ≠ repartir a Sevilla): estem explotant estructura MAR, igual que amb l'edat per canal.

Exercici 3

from sklearn.impute import SimpleImputer

imp = SimpleImputer(strategy="median", add_indicator=True)
resultat = imp.fit_transform(df[["edat", "satisfaccio"]])
print(resultat.shape)   # (10, 4)

Van entrar 2 columnes i en surten 4: les dues originals imputades més un indicador binari per cada columna que tenia nuls (1 = el valor era absent). Així es conserva la informació d'absència, essencial si el mecanisme és MNAR com sospitem de satisfaccio.

Conclusió

Ja saps tractar els forats amb criteri: diagnosticar el mecanisme (MCAR, MAR, MNAR) abans d'actuar, quantificar i visualitzar els patrons de nuls, eliminar només quan és segur, imputar amb mitjana/mediana/moda o per grups, escalar a SimpleImputer i KNNImputer quan cal, conservar el senyal de l'absència amb indicadors, i —sobretot— aprendre els valors d'imputació únicament de les dades d'entrenament per no filtrar informació. El dataset de churn de MercaFresh ja està complet: sense duplicats, sense valors impossibles i sense forats.

Però complet no vol dir a punt: diverses de les seves variables tenen distribucions molt asimètriques (la despesa, amb la seva llarga cua de clients-restaurant), les dates continuen sent poc útils tal com estan, i les comandes encara no estan resumides per client. A la propera lliçó, transformació de dades, remodelarem aquestes variables —logaritmes, discretització, components de data, agregacions— i coneixerem els pipelines de scikit-learn per encadenar tots aquests passos de manera ordenada.

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