Hai un bel dataset di annunci immobiliari, lanci la regressione e scikit-learn ti risponde ValueError: could not convert string to float: 'Milano'? È il rito di passaggio di chiunque lavori con dati veri: i modelli di machine learning, sotto il cofano, fanno moltiplicazioni e somme — e non puoi moltiplicare «Milano» per un coefficiente. Prima o poi ogni colonna di testo va tradotta in numeri. La traduzione sembra banale (Milano=0, Roma=1, Napoli=2, fatto!) e invece è una delle scelte di preprocessing dove è più facile sbagliare senza che nulla si rompa: il codice gira, il modello si addestra, e intanto gli hai insegnato una bugia. Vediamo quale, e come evitarla.
Perché «Milano=0, Roma=1, Napoli=2» è una bugia
Assegnare un intero a ogni categoria si chiama ordinal encoding (o label encoding quando lo fai sulla variabile target). Il problema è che i numeri, per un modello, non sono etichette: sono quantità. Se codifichi Napoli=2 e Milano=0, una regressione lineare capirà che Napoli è «il doppio di qualcosa» rispetto a Roma=1, e che la distanza Milano–Napoli è il doppio della distanza Milano–Roma. Hai inventato un ordine e delle proporzioni che nei tuoi dati non esistono.
Pensa ai numeri di maglia dei calciatori: il numero 10 non è «il doppio» del numero 5, e calcolare la media dei numeri di maglia di una squadra non ti dice niente. Sono numeri usati come nomi. L’ordinal encoding applicato a categorie senza ordine fa esattamente questo: trasforma nomi in quantità, e i modelli che ragionano su distanze e proporzioni — regressioni, SVM, k-NN, reti neurali — ci cascano in pieno.
Quando invece l’ordine esiste davvero, l’ordinal encoding è la scelta giusta: le taglie S < M < L, i titoli di studio, le fasce «basso/medio/alto». Lì i numeri 0, 1, 2 raccontano una verità: la M sta tra la S e la L.
One-hot: una colonna per categoria, nessun ordine inventato
Per le categorie senza ordine (dette nominali) la soluzione standard è il one-hot encoding: al posto della colonna «città» crei una colonna per ogni città, con 1 dove la categoria è presente e 0 altrove. Ogni riga ha esattamente un 1 acceso — da cui il nome, «uno caldo».
L’analogia più fedele è il citofono di un condominio: non c’è un pulsante «quantità di Rossi», c’è un pulsante per famiglia, e ne premi uno solo. Nessun pulsante è «maggiore» di un altro, nessuna distanza è implicita: ogni categoria ha la sua dimensione indipendente, e il modello impara un coefficiente per ciascuna. Niente ordini inventati, niente proporzioni fantasma.
Il prezzo da pagare è la larghezza: una colonna con 3 città diventa 3 colonne, una con 5.000 codici prodotto diventa 5.000 colonne quasi tutte piene di zeri. Tienilo a mente, ci torniamo tra un attimo.
La stessa colonna nominale codificata in due modi: con l’ordinal encoding un solo numero per riga (comodo, ma l’ordine 0<1<2 è un’invenzione), con il one-hot una colonna binaria per categoria e nessuna gerarchia implicita.
In pratica: OneHotEncoder e OrdinalEncoder (e chi è LabelEncoder)
Prepara l’ambiente: Python 3.12 in un venv (o con uv), editor a scelta tra VS Code e PyCharm, e le solite due librerie. Piccolo chiarimento di nomenclatura che confonde tutti: in scikit-learn LabelEncoder serve solo per la colonna target (le etichette da predire), mentre per le feature si usano OrdinalEncoder e OneHotEncoder. Se in un vecchio tutorial vedi LabelEncoder applicato alle feature, è codice d’altri tempi.
# pip install pandas scikit-learn (dentro il venv)
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
df = pd.DataFrame({
"citta": ["Milano", "Roma", "Napoli", "Roma", "Milano", "Napoli"],
"taglia": ["S", "L", "M", "M", "S", "L"], # ordine VERO: S < M < L
"mq": [55, 80, 65, 70, 48, 90],
"prezzo": [1250, 980, 700, 890, 1100, 820], # affitto mensile, target
})
# One-hot per la categoria nominale (nessun ordine)
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
citta_oh = ohe.fit_transform(df[["citta"]])
print(ohe.get_feature_names_out()) # ['citta_Milano' 'citta_Napoli' 'citta_Roma']
print(citta_oh)
# Ordinal per la categoria ORDINATA: l'ordine lo dichiari tu,
# altrimenti scikit-learn usa quello alfabetico (L<M<S: sbagliato!)
oe = OrdinalEncoder(categories=[["S", "M", "L"]])
print(oe.fit_transform(df[["taglia"]]).ravel()) # [0. 2. 1. 1. 0. 2.]
Due dettagli del codice che in produzione valgono oro. handle_unknown="ignore" dice al one-hot cosa fare se in inferenza arriva una città mai vista in training («Torino»): tutte le colonne a zero invece di un crash. E l’argomento categories di OrdinalEncoder ti obbliga a dichiarare l’ordine giusto: senza, l’ordinamento alfabetico farebbe L < M < S — una taglia media più grande della large.
Forse conosci anche pd.get_dummies: fa il one-hot in una riga ed è comodissimo per esplorare. Ma non ha memoria: se il batch di domani non contiene nessun annuncio di Napoli, produce una tabella con una colonna in meno e il modello va in errore. Per l’esplorazione va bene; per un modello che deve vivere oltre il notebook, usa OneHotEncoder dentro una pipeline.
Tutto insieme: ColumnTransformer dentro una Pipeline
Nel mondo reale le colonne categoriche convivono con quelle numeriche, e ognuna vuole il suo trattamento. ColumnTransformer è lo smistatore che applica il trasformatore giusto a ogni gruppo di colonne:
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
X = df[["citta", "taglia", "mq"]]
y = df["prezzo"]
pre = ColumnTransformer([
# (nome, trasformatore, colonne a cui applicarlo)
("nominali", OneHotEncoder(handle_unknown="ignore"), ["citta"]),
("ordinali", OrdinalEncoder(categories=[["S", "M", "L"]]), ["taglia"]),
("numeriche", StandardScaler(), ["mq"]),
])
pipe = Pipeline([("preprocessing", pre), ("modello", Ridge())])
# La pipeline impara le categorie SOLO dal train di ogni fold:
# niente sorprese tra training e produzione
print(cross_val_score(pipe, X, y, cv=3, scoring="r2"))
Se non ricordi perché lo StandardScaler è lì, ti basta sapere che porta le colonne numeriche su una scala comparabile — ne abbiamo parlato nel tutorial su train/test split e scaling. Il punto importante è un altro: l’encoder dentro la pipeline impara le categorie solo dal train set, esattamente come l’imputer dei dati mancanti. Stessa regola d’oro, stesso motivo: niente informazioni dal futuro.
Quando il one-hot NON serve (onestà sulle alte cardinalità)
Codificare le categorie non è un dettaglio tecnico: è dire al modello quali distanze esistono tra i tuoi dati. Sbaglia la codifica, e il modello imparerà con impegno una geografia che non esiste.
Sarebbe disonesto chiudere senza i limiti. Primo: gli alberi di decisione e i loro ensemble (random forest, gradient boosting) ragionano per soglie, non per distanze — con un ordinal encoding se la cavano spesso bene anche su categorie nominali, e il one-hot può perfino peggiorarli frammentando le feature. Secondo: con migliaia di categorie (codici prodotto, ID utente) il one-hot esplode in migliaia di colonne sparse; lì si passa a tecniche come il target encoding (nella libreria category_encoders) o, nel deep learning, agli embedding. Terzo: per i modelli lineari senza regolarizzazione può servire drop="first" per evitare colonne perfettamente ridondanti. Il one-hot è il default sensato per categorie nominali con poche decine di valori e modelli basati su distanze — non un dogma universale.
Prova tu
Prendi il dataset del codice sopra e aggiungi una colonna riscaldamento con i valori «assente», «centralizzato», «autonomo». Domanda non banale: è nominale o ordinale? Prova entrambe le codifiche nel ColumnTransformer e confronta gli score in cross-validation. Poi fai il test cattivo: passa alla pipeline un annuncio con citta="Torino" e verifica con i tuoi occhi cosa succede con e senza handle_unknown="ignore".
Per approfondire: la sezione Encoding categorical features della guida ufficiale scikit-learn (scikit-learn.org, capitolo Preprocessing) copre tutti i parametri visti qui; per le alte cardinalità, la documentazione della libreria category_encoders è il passo successivo naturale.

I commenti sono riservati agli iscritti.
Accedi per commentare