En tancar el mòdul anterior vam prometre posar l'estadística a treballar sobre dades reals i imperfectes, i aquí complim aquesta promesa. Els datasets de MercaFresh —com els de qualsevol empresa real— arriben amb duplicats, errors de captura, valors impossibles i categories escrites de tres maneres diferents. Cap algorisme del mòdul 4 no pot compensar una dada d'entrada corrupta: si entrenem el model de churn amb clients duplicats o edats negatives, aprendrà patrons que no existeixen. En aquesta lliçó aprendràs a auditar un dataset, detectar-ne els problemes i corregir-los de manera sistemàtica amb pandas. És, segons la majoria de professionals, la fase on s'inverteix més temps d'un projecte de ML: fer-la bé marca la diferència entre un model útil i un d'enganyós.

Contingut

  1. Per què les dades reals estan brutes
  2. El dataset de clients de MercaFresh
  3. Auditoria inicial: conèixer el terreny
  4. Duplicats: exactes i lògics
  5. Errors de captura i valors impossibles
  6. Inconsistències de text i categories
  7. Tipus de dades incorrectes
  8. Outliers: detectar-los i decidir què fer-ne

Per què les dades reals estan brutes

Els datasets dels tutorials arriben nets; els de les empreses, no. Les causes són sempre semblants:

  • Entrada manual: un teleoperador de MercaFresh tecleja "BCN" un dia i "Barcelona" l'endemà.
  • Integració de sistemes: la web, l'app mòbil i el call center desen les dades amb formats diferents (dates 2025-03-14 vs 14/03/2025).
  • Errors de programari: un bug va registrar durant dues setmanes imports de comanda a 0 €.
  • Migracions històriques: en canviar de CRM el 2023, alguns clients es van importar dues vegades.
  • Valors sentinella: algú va fer servir -1 o 999 per dir "no ho sé", i ara semblen dades reals.

La conseqüència pràctica es resumeix en la frase clàssica garbage in, garbage out: un model entrenat amb escombraries produeix prediccions escombraries, però amb aparença de precisió matemàtica. Per això la neteja no és un tràmit, sinó part de l'anàlisi.

flowchart LR
    A[Dades crues] --> B[Auditoria]
    B --> C[Duplicats]
    C --> D[Valors impossibles]
    D --> E[Text i categories]
    E --> F[Tipus de dades]
    F --> G[Outliers]
    G --> H[Dataset net]

Aquest és l'ordre que seguirem: primer entendre, després corregir del més obvi al més subtil.

El dataset de clients de MercaFresh

Treballarem amb un extracte fictici però realista de la taula de clients, exportada del CRM per al projecte de churn. El construirem en codi perquè puguis reproduir tot el que segueix:

import pandas as pd
import numpy as np

dades = {
    "id_client": [101, 102, 102, 104, 105, 106, 107, 108, 109, 110],
    "nom": ["Ana Ruiz", "Luis Gil", "Luis Gil", "Marta Vega", "Joan Pons",
            "Sara Mora", "Pau Serra", "Eva Lima", "Leo Cano", "Iris Bou"],
    "edat": [34, 29, 29, -5, 41, 38, 127, 45, 31, 27],
    "ciutat": ["Barcelona", "BCN", "BCN", " barcelona ", "Valencia",
               "VALENCIA", "Madrid", "madrid", "Sevilla", "Barcelona"],
    "despesa_total": ["1250.50", "890.00", "890.00", "0", "2100.75",
                      "310.20", "15400.00", "670.40", "0", "980.10"],
    "data_alta": ["2023-05-10", "2024-01-15", "2024-01-15", "2023-11-02",
                  "2022-07-30", "2027-03-01", "2021-02-14", "2023-09-19",
                  "2024-06-05", "2023-12-28"],
    "num_comandes": [18, 12, 12, 0, 25, 4, 210, 9, 0, 14],
}

df = pd.DataFrame(dades)
print(df)

A simple vista ja s'hi intueixen problemes: el client 102 apareix dues vegades, hi ha una edat de -5 i una altra de 127, la ciutat "Barcelona" està escrita de quatre maneres, la despesa és text i hi ha una data d'alta el 2027 (al futur!). En un dataset de 10 files els veus d'un cop d'ull; en un de 200.000 necessites mètode.

Auditoria inicial: conèixer el terreny

Abans de tocar res, cal radiografiar el dataset. Tres funcions de pandas cobreixen el 80 % de l'auditoria inicial:

# 1) Estructura: columnes, tipus i valors no nuls
df.info()

info() respon tres preguntes: quantes files hi ha?, de quin tipus és cada columna?, quants valors no nuls té cadascuna? Aquí descobrim el primer problema silenciós: despesa_total és de tipus object (text), no numèric. Un object on esperaves nombres és una alarma vermella.

# 2) Resum estadístic de les columnes numèriques
print(df.describe())

describe() ens dona mitjana, desviació, mínim, màxim i quartils — exactament els estadístics del mòdul 2 (lliçó 02-01). La seva gran utilitat en neteja és mirar min i max: una edat mínima de -5 i màxima de 127 no necessiten més anàlisi per saber que alguna cosa falla.

# 3) Cardinalitat: quants valors diferents té cada columna
print(df.nunique())
print(df["ciutat"].unique())

nunique() compta valors diferents per columna. Si sabem que MercaFresh opera en 4 ciutats però ciutat té 7 valors únics, hi ha inconsistències d'escriptura. unique() ens les mostra: ['Barcelona' 'BCN' ' barcelona ' 'Valencia' 'VALENCIA' 'Madrid' 'madrid' 'Sevilla'].

Funció Pregunta que respon Problema que destapa
info() Quins tipus i quants nuls? Tipus incorrectes, columnes incompletes
describe() Quins rangs prenen els nombres? Valors impossibles, escales estranyes
nunique() / unique() Quantes categories diferents? Categories duplicades per escriptura

Duplicats: exactes i lògics

Duplicats exactes

Un duplicat exacte és una fila idèntica a una altra en totes les columnes. A MercaFresh, el client 102 (Luis Gil) es va importar dues vegades durant la migració del CRM:

# Quantes files duplicades exactes hi ha?
print(df.duplicated().sum())        # 1

# Veure-les (keep=False mostra TOTES les copies, no nomes la segona)
print(df[df.duplicated(keep=False)])

# Eliminar-les conservant la primera aparicio
df = df.drop_duplicates()
print(len(df))                      # 9 files

Punts clau de drop_duplicates:

  • keep="first" (per defecte) conserva la primera còpia; keep="last", l'última; keep=False les elimina totes.
  • Retorna un DataFrame nou: cal reassignar-lo (df = df.drop_duplicates()).

Per què importa per al model de churn? Un client duplicat pesa el doble en l'entrenament: el model "veurà" dues vegades els seus patrons i esbiaixarà les seves conclusions cap a aquell perfil.

Duplicats lògics

Més perillosos són els duplicats lògics: files que representen la mateixa entitat però no són idèntiques byte a byte. Per exemple, el mateix client donat d'alta dues vegades amb la despesa repartida entre totes dues fitxes, o la mateixa comanda registrada per la web i per l'app amb ids diferents. Per caçar-los, es busca duplicitat només en les columnes que defineixen la identitat:

# Hi ha ids de client repetits encara que la resta de columnes difereixi?
print(df.duplicated(subset=["id_client"]).sum())

# O identitat per nom + data d'alta
sospitosos = df[df.duplicated(subset=["nom", "data_alta"], keep=False)]

Amb duplicats lògics, esborrar sense mirar és arriscat: de vegades el correcte és fusionar (sumar les comandes de totes dues fitxes) en lloc d'eliminar. És una decisió de negoci, no només tècnica: pregunta sempre què representa cada fila.

Errors de captura i valors impossibles

Un valor impossible és aquell que viola les regles del món real o del negoci. La tècnica consisteix a definir regles de validesa explícites i comprovar quantes files les incompleixen:

# Regla 1: l'edat ha d'estar entre 18 i 100 (MercaFresh exigeix majoria d'edat)
edats_invalides = df[(df["edat"] < 18) | (df["edat"] > 100)]
print(edats_invalides[["id_client", "edat"]])
# 104 -> -5  (error de signe o de tecleig)
# 107 -> 127 (van teclejar 12 i 7 junts? es 27?)

# Regla 2: un client amb comandes ha de tenir despesa > 0
# (primer convertirem despesa_total a nombre; ho veiem a l'apartat de tipus)

# Regla 3: la data d'alta no pot ser futura
dates = pd.to_datetime(df["data_alta"])
futures = df[dates > pd.Timestamp("2026-08-24")]
print(futures[["id_client", "data_alta"]])   # 106 -> 2027-03-01

I què en fem? Hi ha tres opcions, en ordre de preferència:

  1. Corregir, si coneixem l'error: una edat de -5 amb el signe canviat pot ser 5 (impossible, menor d'edat) o un error de tecleig; si el CRM original té la dada bona, es recupera d'allà.
  2. Marcar com a absent (np.nan), si no podem saber el valor real: és honest reconèixer que no ho sabem, i a la propera lliçó veurem com tractar aquests nuls.
  3. Eliminar la fila, només si el registre sencer és irrecuperable i són pocs casos.
# Opcio 2 aplicada: convertir impossibles en NaN
df.loc[(df["edat"] < 18) | (df["edat"] > 100), "edat"] = np.nan
df.loc[pd.to_datetime(df["data_alta"]) > pd.Timestamp("2026-08-24"),
       "data_alta"] = np.nan

Fixa't en df.loc[condicio, columna] = valor: és la manera segura en pandas de modificar un subconjunt (evita el famós SettingWithCopyWarning).

Els imports a 0 mereixen una reflexió a part: una despesa_total de 0 amb num_comandes de 0 pot ser legítima (client registrat que mai no ha comprat — molt rellevant per al churn!), mentre que despesa 0 amb 12 comandes és un error. La mateixa xifra pot ser dada vàlida o error segons el context: valida combinacions de columnes, no columnes aïllades.

Inconsistències de text i categories

La columna ciutat n'és l'exemple perfecte: "Barcelona", "BCN" i " barcelona " són la mateixa ciutat per a un humà, però tres categories diferents per a pandas i per a qualsevol model. Si no ho arreglem, el futur one-hot encoding (lliçó 03-04) crearà tres columnes on n'hi hauria d'haver una.

La normalització segueix una recepta en tres passos amb els mètodes de cadena str.*:

# Pas 1: treure espais sobrers al principi i al final
df["ciutat"] = df["ciutat"].str.strip()

# Pas 2: unificar majuscules/minuscules
df["ciutat"] = df["ciutat"].str.lower()
print(df["ciutat"].unique())
# ['barcelona' 'bcn' 'valencia' 'madrid' 'sevilla']

# Pas 3: mapar alies i abreviatures a un valor canonic
alies = {"bcn": "barcelona", "vlc": "valencia", "mad": "madrid"}
df["ciutat"] = df["ciutat"].replace(alies)

# Toc final estetic: posar majuscula inicial
df["ciutat"] = df["ciutat"].str.title()
print(df["ciutat"].value_counts())
# Barcelona    3
# Madrid       2
# Valencia     2
# Sevilla      1

Detall important: str.strip() i str.lower() transformen el text caràcter a caràcter, mentre que replace() substitueix valors complets fent servir un diccionari. Per a substitucions dins del text (per exemple, treure els punts de "S.L.") hi ha str.replace("...", "..."), que és un altre mètode diferent — confondre'ls és un clàssic.

Per descobrir àlies que no coneixies, value_counts() és el teu aliat: ordena les categories per freqüència i les variants rares apareixen al final de la llista amb recomptes d'1 o 2.

Tipus de dades incorrectes

Que un nombre estigui emmagatzemat com a text és un problema invisible fins que esclata: no pots calcular mitjanes, les ordenacions són alfabètiques ("15400" < "890" perquè "1" < "8") i describe() l'ignora.

# despesa_total va arribar com a text: convertir-la a float
df["despesa_total"] = df["despesa_total"].astype(float)

# Si hi hagues valors no convertibles ("N/D", "error"), astype fallaria.
# to_numeric amb errors="coerce" els converteix en NaN en lloc de trencar:
df["despesa_total"] = pd.to_numeric(df["despesa_total"], errors="coerce")

# Les dates en text es converteixen amb to_datetime
df["data_alta"] = pd.to_datetime(df["data_alta"], errors="coerce")
print(df.dtypes)
Situació Eina Comportament davant d'errors
Text numèric net astype(float) / astype(int) Llança una excepció
Text numèric amb brutícia pd.to_numeric(..., errors="coerce") Converteix la brutícia en NaN
Dates en text pd.to_datetime(..., errors="coerce") Converteix la brutícia en NaT
Format de data ambigu pd.to_datetime(..., format="%d/%m/%Y") Exigeix el format indicat

El paràmetre format mereix atenció: "14/03/2025" pot ser 14 de març o (en convenció americana) un error. Especificar format="%d/%m/%Y" elimina l'ambigüitat. Convertir les dates a datetime real, a més, desbloqueja operacions que farem servir a la lliçó 03-03: extreure el mes, calcular l'antiguitat, restar dates.

Outliers: detectar-los i decidir què fer-ne

Els outliers ja els coneixes del mòdul 2: a 02-01 els vas veure treure el cap als boxplots d'imports de comanda, i a 02-02 vas aprendre que un z-score alt assenyala una raresa. Ara els apliquem com a eines de neteja sobre despesa_total.

Detecció amb z-score

despesa = df["despesa_total"].dropna()
z = (despesa - despesa.mean()) / despesa.std()
print(df.loc[z.abs() > 3, ["id_client", "despesa_total"]])

Recordatori de 02-02: si les dades fossin normals, només un 0,3 % dels valors cauria més enllà de |z| > 3. El client 107, amb 15.400 € de despesa i 210 comandes, es dispara. El z-score té una feblesa: la mitjana i la desviació que fa servir es contaminen pels mateixos outliers, així que en mostres petites o molt esbiaixades pot fallar.

Detecció amb IQR

El mètode del rang interquartílic (02-01) és més robust perquè els quartils gairebé no s'immuten davant de valors extrems:

q1 = despesa.quantile(0.25)
q3 = despesa.quantile(0.75)
iqr = q3 - q1
limit_inf = q1 - 1.5 * iqr
limit_sup = q3 + 1.5 * iqr
outliers = df[(df["despesa_total"] < limit_inf) | (df["despesa_total"] > limit_sup)]
print(outliers[["id_client", "despesa_total"]])

És exactament la regla que dibuixa els "bigotis" del boxplot que ja coneixes.

La decisió: error o realitat?

Aquí hi ha el matís que separa la neteja mecànica de la bona neteja: un outlier no és necessàriament un error. El client 107 podria ser un restaurant que compra cada dia a MercaFresh — un client valuosíssim, no una dada corrupta. Les opcions:

Situació Acció recomanada
Outlier clarament erroni (edat 127) Corregir o convertir en NaN
Outlier real però atípic (client-restaurant) Conservar; potser marcar-lo amb una columna es_empresa
Outlier real que distorsiona el model Acotar (capping/winsorizing: retallar al percentil 99) o transformar la variable (lliçó 03-03)
Molts outliers estructurals Detectar-los és en si mateix un objectiu de negoci (anomalies, mòdul 5)
# Exemple de capping al percentil 99
p99 = df["despesa_total"].quantile(0.99)
df["despesa_total"] = df["despesa_total"].clip(upper=p99)

La regla d'or: no eliminis mai un outlier sense poder explicar per què. Documenta cada decisió — el teu jo del futur (i el model de churn) t'ho agrairan.

Errors Comuns i Consells

  • Netejar sense auditar primer. Executar drop_duplicates "per si de cas" abans d'entendre les dades pot esborrar registres legítims. Primer info(), describe(), nunique(); després, actuar.
  • Oblidar reassignar. df.drop_duplicates() sense df = no canvia res: la majoria d'operacions de pandas retornen una còpia.
  • Confondre replace() amb str.replace(). El primer substitueix valors complets de la Serie; el segon, subcadenes dins de cada text.
  • Eliminar outliers automàticament amb |z| > 3. És un detector, no un jutge: cada outlier mereix un diagnòstic (error o client excepcional?).
  • Validar columnes de manera aïllada. Despesa 0 pot ser vàlida o no segons num_comandes: les regles de validesa potents creuen columnes.
  • No documentar els canvis. Guarda el nombre de files eliminades i les regles aplicades; en un projecte real et demanaran comptes de cada registre descartat.
  • Consell: escriu la neteja com una funció reproduïble (def netejar_clients(df): ...) en lloc de cel·les soltes. Quan arribi l'exportació del mes següent, l'aplicaràs en segons.

Exercicis

Exercici 1

Amb el DataFrame df original de la lliçó (abans de netejar), escriu el codi que: (a) compti els duplicats exactes, (b) els elimini conservant la primera aparició i (c) comprovi si queda cap id_client repetit.

Exercici 2

L'equip de MercaFresh et passa aquesta columna d'una nova exportació: ["Madrid", "MAD", " madrid", "Màdrid", "madriz"]. Normalitza-la perquè tots els valors acabin sent "Madrid". Pista: necessitaràs strip, lower i un diccionari d'àlies que inclogui els errors tipogràfics.

Exercici 3

La taula de comandes té una columna import_comanda amb aquests valors: [22.5, 19.0, 24.3, 21.7, 480.0, 23.1, 20.9, 18.4]. Detecta outliers amb el mètode IQR i raona (en un comentari) si el valor detectat s'hauria d'eliminar, conservar o investigar, sabent que MercaFresh també serveix petits restaurants.

Solucions

Exercici 1

# (a) Comptar duplicats exactes
print(df.duplicated().sum())            # 1

# (b) Eliminar conservant la primera aparicio
df = df.drop_duplicates(keep="first")

# (c) Queden ids repetits? (duplicats logics)
print(df.duplicated(subset=["id_client"]).sum())   # 0 en aquest cas

Si (c) retornés més de 0, caldria inspeccionar aquelles files amb keep=False i decidir si fusionar o eliminar.

Exercici 2

s = pd.Series(["Madrid", "MAD", " madrid", "Màdrid", "madriz"])

s = s.str.strip().str.lower()           # 'madrid', 'mad', 'madrid', 'màdrid', 'madriz'
alies = {"mad": "madrid", "màdrid": "madrid", "madriz": "madrid"}
s = s.replace(alies)
s = s.str.title()
print(s.unique())                       # ['Madrid']

Els errors tipogràfics (madriz) no s'arreglen amb regles generals: cal descobrir-los amb value_counts() i afegir-los al diccionari d'àlies un a un.

Exercici 3

imports_comanda = pd.Series([22.5, 19.0, 24.3, 21.7, 480.0, 23.1, 20.9, 18.4])

q1, q3 = imports_comanda.quantile(0.25), imports_comanda.quantile(0.75)
iqr = q3 - q1
lim_inf, lim_sup = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print(imports_comanda[(imports_comanda < lim_inf) | (imports_comanda > lim_sup)])   # 480.0

# Raonament: 480 EUR es un outlier estadistic clar, pero MercaFresh serveix
# restaurants: podria ser una comanda majorista legitima. Abans d'eliminar,
# investigar: el client te historial de comandes grans? Coincideix amb
# un client de tipus empresa? Si es legitima, es conserva (i potser es marca
# amb una columna 'comanda_majorista'); nomes s'elimina si es confirma l'error.

Conclusió

En aquesta lliçó has après a convertir un dataset caòtic en un de fiable: auditar amb info(), describe() i nunique(); eliminar duplicats exactes i diagnosticar els lògics amb drop_duplicates; definir regles de validesa per caçar valors impossibles; unificar categories amb str.strip/lower i replace; corregir tipus amb astype, to_numeric i to_datetime; i detectar outliers amb z-score i IQR sabent que detectar-los no és sentenciar-los. La idea central: la neteja és anàlisi, no tràmit — cada correcció és una decisió que s'ha de poder explicar.

Hauràs notat que diverses vegades la solució honesta va ser convertir un valor sospitós en NaN: hem anat acumulant forats deliberadament. A la propera lliçó abordem precisament això: les dades mancants de MercaFresh, per què falten (que no sempre és atzar) i les estratègies per eliminar-les o imputar-les sense enganyar el model.

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