In questo tutorial vediamo come fare Undersampling e Oversampling in Python, uno step importante del pre-processing.
Quando abbiamo a che fare con l’apprendimento supervisionato può capitare spesso di avere classi target sbilanciate.
Immaginiamo di dover affrontare un problema di fraud detection, ovvero l’identificazione di transizioni fraudolente. Ci aspettiamo (mi auguro per la banca…) di trovare una percentuale molto bassa di transizioni di questo tipo, e di averne tantissime invece legittime.
Come fare in casi come questi?
I modelli di Machine Learning devono essere opportunamente calibrati in questo senso, altrimenti, con classi troppo sbilanciate, rischiano di prendere delle cantonate.
In questo senso, ci vengono in aiuto due tecniche molto importanti, ovvero l’Undersampling e l’Oversampling che devono essere utilizzate nella fase di pre-processing. Ne abbiamo parlato in modo più approfondito nel nostro articolo Tecniche di undersampling e oversampling (che trovi qui), oggi vediamo come implementarlo in Python.
Per questo tutorial useremo il dataset di Kaggle che trovi qui. In questo dataset le classi sono fortemente sbilanciate, infatti con il codice data[“Class”].value_counts(normalize=True) possiamo vedere come la classe = 1 (ovvero la transizione fraudolenta) rappresenti lo 0.002 % del dataset.

1. UNDERSAMPLING
Con l’undersampling si vanno a levare alcune osservazioni dalla classe più popolata. Nel caso specifico della fraud detection, dalla classe “transizione legittima”.
Le tecniche di undersampling sono preferibili a quelle di oversampling perché non creano dati nuovi e quindi si discostano meno dalla realtà, e di conseguenza sono le più usate.
1.1. Undersampling randomico
Prendo in modo casuale un numero di classe = 0 vicino al numero dei classe = 1. Nel nostro caso, abbiamo 492 classe = 1, quindi prenderò altrettanti valori (volendo anche un po’ di più) di classe = 0.
data_0 = data[data["Class"]==0].sample(n=492,random_state=16)
Ho creato un dataset in cui ho solo i valori class = 0 e ne ho preso un sample di 492 osservazioni. Ho aggiunto anche un random_state per la riproducibilità.
A questo punto filtro i class = 1 e li unisco a data_0. In questo modo avrò il mio nuovo dataset bilanciato 50/50.
data_1 = data[data[“Class”]==1]
new_data = data_0.append(data_1)
Se andiamo a vedere i valori del nostro new_data, avremo quanto segue:

1.2. Undersampling stratificato
L’Undersampling randomico è quello di gran lunga più utilizzato, perché non richiede particolari assunzioni. Spesso però, con classi così sbilanciate, può rischiare di essere anche un’arma a doppio taglio.
Cosa succede se infatti in quei 492 casi che prendo, ci sono diversi casi limite? Naturalmente in questo modo non ho tenuto conto della distribuzione delle variabili, ma ne ho presi un piccolissimo insieme totalmente causale.
L’undersampling stratificato va a prendere le osservazioni in modo più oculato, cercando di mantenere le proporzioni del dataset originario per una o più variabili.
Facciamo l’assunzione che la variabile Amount sia una variabile in questo caso importante. Al posto di prendere osservazioni a caso, prenderò i valori di class = 0 che rispecchiano la distribuzione di questa variabile.
Con data_0[“Amount”].describe() mi studio la sua distribuzione, e poi vado a prendere i nostri 492 casi seguendo i quantili.

Poiché voglio 492 osservazioni e voglio rispettare la distribuzione di questa variabile, ne prenderò 1/4 (123) per ogni intervallo di quantile: 0-5.65, 5.65-22, 22-77.05, 77.05-25691.
data_0_1 = data_0.query("Amount >= 0 & Amount < 5.65").sample(n=123,random_state=16)
data_0_2 = data_0.query("Amount >= 5.65 & Amount < 22").sample(n=123,random_state=16)
data_0_3 = data_0.query("Amount >= 22 & Amount < 77.05").sample(n=123,random_state=16)
data_0_4 = data_0.query("Amount >= 77.05 & Amount < 25691").sample(n=123,random_state=16)
new_data_0 = data_0_1.append(data_0_2).append(data_0_3).append(data_0_4)
Il describe() di questa nuova variabile nel dataset new_data_0 sarà molto simile a quello del dataset originario. Cosa che forse non sarebbe successa nel caso di un campionamento randomico.
A questo punto basterà attaccare insieme questo nuovo dataset (new_data_0) con il nostro data_1.
2. Oversampling
Finora abbiamo lavorato sulla classe = 0, ovvero la classe più popolata.
Con l’oversampling lavoriamo sulla classe = 1, creando ex-novo delle osservazioni del target meno popolato.
E’ una tecnica un po’ rischiosa, perché si tratta di inventare (naturalmente non a caso) delle righe nel nostro dataset.
2.1 SMOTE
La tecnica SMOTE: in questo caso si vanno a prendere le osservazioni più vicine (distanza euclidea) tra quelle delle classe minoritaria, si fa la differenza tra i due vettori di features e si moltiplica questo valore per un numero casuale tra 0 e 1. In altre parole, si va ad applicare un perturbamento alla distanza tra due punti della classe minoritaria. Così facendo si creano osservazioni artificiali che accrescono il patrimonio di dati ma non ne modificano troppo il valore.
Dobbiamo installare e importare la libreria imblearn e in particolare il modulo over_sampling SMOTE.
Poi dovremmo creare le nostre X e y (ovvero il vettore di tutte le features meno il target per la X e i valori del target per la y) e lanciare SMOTE, lui farà il resto.
from imblearn.over_sampling import SMOTE
X = data[['Time', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', 'V11', 'V12', 'V13', 'V14', 'V15', 'V16', 'V17', 'V18', 'V19', 'V20','V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28', 'Amount']].values
y = data["Class"].values
oversample = SMOTE()
X, y = oversample.fit_resample(X, y)
Se andiamo a vedere le quantità delle y, andremo a vedere che ha creato tanti 1 quanti 0.
from collections import Counter
counter = Counter(y)
print(counter)
OUTPUT: Counter({0: 284315, 1: 284315})
Naturalmente, non conviene ricreare così tanti 1, conviene più fare un undersmapling della classe = 0 e poi fare un oversampling degli 1, altrimenti si rischia di discostarci davvero tanto dalla realtà.
conclusioni
In questo tutorial abbiamo visto come applicare delle tecniche di Undersampling e Oversampling in Python sfruttando semplici librerie. Queste due tecniche fanno riferimento al mondo del pre-processing, ovvero quell’insieme di operazioni che devono essere fatte prima di utilizzare un modello di Machine Learning.
Abbiamo visto due tecniche per l’Undersampling (randomico e stratificato) e una tecnica per l’Oversampling (SMOTE).

