L'Importanza del Data Preprocessing
Il feature engineering e il data preprocessing sono le fasi più critiche di qualsiasi progetto di Machine Learning. Una regola non scritta afferma che l'80% del tempo di un data scientist viene speso nella preparazione dei dati, e solo il 20% nella modellazione. Non importa quanto sofisticato sia l'algoritmo: se i dati in input sono sporchi, incompleti o mal rappresentati, il modello produrrà risultati scadenti. Garbage in, garbage out.
Il preprocessing trasforma i dati grezzi in un formato adatto all'algoritmo. Il feature engineering va oltre: crea nuove variabili a partire da quelle esistenti, sfruttando la conoscenza del dominio per catturare relazioni che l'algoritmo da solo non riuscirebbe a trovare. Insieme, queste fasi determinano il successo o il fallimento del progetto ML.
Cosa Imparerai in Questo Articolo
- Tecniche per gestire i valori mancanti
- Encoding delle variabili categoriche
- Scaling e normalizzazione delle feature numeriche
- Rilevamento e gestione degli outlier
- Creazione di nuove feature con domain knowledge
- Pipeline di preprocessing con scikit-learn
Gestire i Valori Mancanti
I dati reali contengono quasi sempre valori mancanti (NaN, null). Le strategie principali sono tre: eliminazione (rimuovere righe o colonne con troppi valori mancanti), imputazione statistica (sostituire con media, mediana o moda) e imputazione predittiva (usare un modello per predire i valori mancanti). La scelta dipende dalla quantità di dati mancanti e dal pattern di mancanza (random o sistematico).
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer, KNNImputer
# Dataset con valori mancanti
data = pd.DataFrame({
'eta': [25, 30, np.nan, 45, 35, np.nan, 28, 50],
'reddito': [30000, np.nan, 45000, 60000, np.nan, 55000, 32000, 70000],
'categoria': ['A', 'B', 'A', np.nan, 'B', 'A', 'B', 'A'],
'target': [0, 1, 0, 1, 1, 0, 1, 0]
})
print("Valori mancanti per colonna:")
print(data.isnull().sum())
print(f"\nPercentuale mancanti:\n{(data.isnull().mean() * 100).round(1)}")
# Strategia 1: Imputazione con media/mediana
imputer_mean = SimpleImputer(strategy='mean')
data['eta_imputed'] = imputer_mean.fit_transform(data[['eta']])
imputer_median = SimpleImputer(strategy='median')
data['reddito_imputed'] = imputer_median.fit_transform(data[['reddito']])
# Strategia 2: Imputazione con KNN (usa i vicini)
knn_imputer = KNNImputer(n_neighbors=3)
numeric_cols = data[['eta', 'reddito']].values
imputed_knn = knn_imputer.fit_transform(numeric_cols)
# Strategia 3: Imputazione categoriche con moda
imputer_mode = SimpleImputer(strategy='most_frequent')
data['categoria_imputed'] = imputer_mode.fit_transform(data[['categoria']])
print("\nDopo imputazione:")
print(data[['eta_imputed', 'reddito_imputed', 'categoria_imputed']].head())
Encoding delle Variabili Categoriche
Gli algoritmi ML lavorano con numeri, non con stringhe. Le variabili categoriche devono essere convertite in formato numerico. Il Label Encoding assegna un intero a ogni categoria (A=0, B=1, C=2): semplice ma introduce un ordine inesistente. Il One-Hot Encoding crea una colonna binaria per ogni categoria: non introduce ordine ma può generare molte colonne con categoriche ad alta cardinalità. Il Target Encoding sostituisce ogni categoria con la media del target per quella categoria: potente ma rischioso per overfitting.
from sklearn.preprocessing import (
LabelEncoder, OneHotEncoder, OrdinalEncoder,
StandardScaler, MinMaxScaler, RobustScaler
)
from sklearn.compose import ColumnTransformer
import pandas as pd
import numpy as np
# Dataset di esempio
df = pd.DataFrame({
'colore': ['rosso', 'blu', 'verde', 'rosso', 'blu'],
'taglia': ['S', 'M', 'L', 'XL', 'M'],
'prezzo': [10.5, 25.0, 45.0, 80.0, 22.0],
'peso': [100, 500, 1200, 3000, 450]
})
# One-Hot per colore (nominale, no ordine)
ohe = OneHotEncoder(sparse_output=False, drop='first')
colore_encoded = ohe.fit_transform(df[['colore']])
print(f"One-Hot colore:\n{colore_encoded}")
# Ordinal per taglia (ordinale, ha un ordine)
oe = OrdinalEncoder(categories=[['S', 'M', 'L', 'XL']])
taglia_encoded = oe.fit_transform(df[['taglia']])
print(f"\nOrdinal taglia: {taglia_encoded.flatten()}")
# --- SCALING ---
# StandardScaler: media=0, std=1 (per distribuzioni normali)
ss = StandardScaler()
prezzo_standard = ss.fit_transform(df[['prezzo']])
# MinMaxScaler: range [0,1] (per distribuzioni non normali)
mms = MinMaxScaler()
prezzo_minmax = mms.fit_transform(df[['prezzo']])
# RobustScaler: usa mediana e IQR (robusto a outlier)
rs = RobustScaler()
peso_robust = rs.fit_transform(df[['peso']])
print(f"\nStandard: {prezzo_standard.flatten().round(2)}")
print(f"MinMax: {prezzo_minmax.flatten().round(2)}")
print(f"Robust: {peso_robust.flatten().round(2)}")
Rilevamento Outlier
Gli outlier sono valori anomali che si discostano significativamente dal resto dei dati. Possono essere errori di misura, dati corrotti o genuini valori estremi. Il metodo IQR (Interquartile Range) identifica outlier come punti oltre 1.5 volte l'IQR dal primo o terzo quartile. Il metodo Z-score identifica punti con valore standardizzato oltre una soglia (tipicamente 3). L'Isolation Forest è un approccio ML che isola gli outlier con alberi decisionali random.
Feature Selection
Non tutte le feature contribuiscono positivamente al modello. Feature irrilevanti o ridondanti possono peggiorare le performance e rallentare il training. La feature selection identifica le variabili più informative. I metodi includono: correlazione (rimuovi feature altamente correlate tra loro), variance threshold (rimuovi feature a bassa varianza), SelectKBest (seleziona le K migliori secondo un test statistico) e la feature importance di Random Forest.
Pipeline di Preprocessing con scikit-learn
Le Pipeline di scikit-learn concatenano passi di preprocessing e modellazione in un unico oggetto. Questo previene il data leakage (quando informazioni del test set contaminano il training) e semplifica la cross-validation e il deployment. Il ColumnTransformer permette di applicare trasformazioni diverse a colonne diverse.
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import pandas as pd
import numpy as np
# Dataset realistico
np.random.seed(42)
n = 200
df = pd.DataFrame({
'eta': np.random.randint(18, 70, n).astype(float),
'reddito': np.random.normal(40000, 15000, n),
'esperienza': np.random.randint(0, 30, n).astype(float),
'citta': np.random.choice(['Milano', 'Roma', 'Napoli', 'Torino'], n),
'titolo_studio': np.random.choice(['Diploma', 'Laurea', 'Master'], n),
'target': np.random.randint(0, 2, n)
})
# Inserire valori mancanti random
for col in ['eta', 'reddito', 'esperienza']:
mask = np.random.random(n) < 0.1
df.loc[mask, col] = np.nan
# Definire colonne per tipo
numeric_features = ['eta', 'reddito', 'esperienza']
categorical_features = ['citta', 'titolo_studio']
# Preprocessing per numeriche: imputa + scala
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# Preprocessing per categoriche: imputa + one-hot
categorical_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(drop='first', handle_unknown='ignore'))
])
# ColumnTransformer combina tutto
preprocessor = ColumnTransformer(transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# Pipeline completa: preprocessing + modello
full_pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])
# Cross-validation (il preprocessing avviene DENTRO ogni fold)
X = df.drop('target', axis=1)
y = df['target']
scores = cross_val_score(full_pipeline, X, y, cv=5, scoring='accuracy')
print(f"Accuracy: {scores.mean():.3f} (+/- {scores.std():.3f})")
Data Leakage: Il preprocessing (scaling, imputazione) deve avvenire dopo il train/test split, mai prima. Se scali sull'intero dataset, il test set influenza i parametri dello scaler. La Pipeline di scikit-learn previene automaticamente questo problema applicando fit_transform solo sul training set e transform sul test set.
Punti Chiave
- Il preprocessing è la fase più critica: l'80% del tempo va nella preparazione dei dati
- Valori mancanti: eliminazione, imputazione statistica o predittiva a seconda del contesto
- Encoding: One-Hot per nominali, Ordinal per ordinali, Target Encoding con cautela
- Scaling: StandardScaler per distribuzioni normali, RobustScaler con outlier
- Pipeline + ColumnTransformer prevengono il data leakage e semplificano il codice
- Il feature engineering con domain knowledge spesso fa più differenza della scelta dell'algoritmo







