Il trattamento e l’imputazione dei valori mancanti (lacking values) sono due step molto delicati per ogni progetto di information science. Nonostante esistano numerous strategie per l’imputazione, tutte possono portare a errori perchè si sta introducendo un dato “artificiale”.
Un consiglio che viene dato spesso è, in fase di imputazione di valori mancanti, creare per ogni characteristic che si sta trattando una nuova variabile booleana “ nomeFeature_isMissing” per tracciare quali valori sono reali e quali indotti durante il processo di cleansing.
Solitamente gli step che vengono seguiti in questa fase del preprocessing del dataset sono i seguenti:
- Se la percentuale di valori mancanti è alta (la soglia varia a seconda del contesto) non si può considerare la variabile, quindi viene eliminata una characteristic;
- Non eliminare mai l’intera osservazione a meno che non abbia valori mancanti su ogni characteristic;
- Scegliete una tecnica di imputazione basandovi sul tipo di dato e fenomeno che si sta trattando.
Lo strumento Python più utilizzato per questo compito è il SimpleImputer di scikit-learn che offre quattro strategie di sostituzione:
- sostituire i lacking values con la media;
- sostituire i lacking values con la mediana;
- sostituire i lacking values con la moda;
- sostituire i lacking values con una costante.
C’è una cosa da osservare, nessuna delle strategie descritte si adatta bene allo studio delle serie storiche. Per dimostrare questa affermazione faremo un take a look at utilizzando la serie storica Airpassenger, una sequenza mensile del numero di passeggeri sui voli internazionali tra il 1949 e il 1960.
Tutto il codice di questo articolo è disponibile su un pocket book Google Colab.
Airpassenger non presenta valori mancanti, quindi verranno introdotti in modo completamente casuale.
import pandas as pd
import numpy as np
import matplotlib.pyplot as pltairpassengers = pd.read_csv("airpassenger.csv", index_col = 0 )
airpassengers.columns = ["Passengers"]
airpassengers.head()
Month Passengers
1949-01 112
1949-02 118
1949-03 132
1949-04 129
1949-05 121
Verrano generati 10 indici mediante la funzione randint del modulo random di numpy. In questi dieci indici verrà sostituito il valore originale con NaN.
NOTA. Visto come vengono generati i valori mancanti, il risultato cambierà ogni volta che viene eseguito il codice!
airpassengers_MV = airpassengers.copy()
na_index = np.random.randint(0, airpassengers.form[0]-1, 10)
airpassengers_MV.iloc[na_index,0] = np.nanplt.determine(figsize=(16,8))
plt.plot(vary(airpassengers.form[0]), airpassengers_MV["Passengers"].values, coloration='tab:blue')
plt.gca().set(title="Airpassengers - Lacking Values", xlabel="Time", ylabel="Passengers")
plt.present()
Month Passengers
1949-01 112
1949-02 118
1949-03 132
1949-04 NaN
1949-05 121
Di seguito verranno testate sulla serie tutte le strategie proposte da SimpleImputer di Scikit-learn.
from sklearn.impute import SimpleImputer
SimpleImputer
rispetta la classica interfaccia di sklearn: una volta inizializzato, l’oggetto può essere utilizzando sfruttando i metodi di base, quali match
, remodel
. Nel nostro esempio non avendo interesse verso il forecasting e quindi non dividendo la serie in prepare e take a look at, useremo fit_transform
su tutta la sequenza.
Il metodo non è altro che la concatenazione dei due:
match
calcola il valore da imputare (media, mediana, più frequente) sulla serie;remodel
applica realmente l’imputazione.
In enter necessita di un oggetto 2D (noi passeremo un DataFrame
) ma restituisce un array NumPy.
La strategia di SimpleImputer
viene applicata mediante il parametro technique, come detto in precedenza si può scegliere tra:
imputer_mean = SimpleImputer(technique = "imply") airpassengers_mean = imputer_mean.fit_transform(airpassengers_MV) airpassengers_mean[:5]
array([[112. ], [118. ], [132. ], [282.23880597], [121. ]])
in grassetto il valore imputato
Questo approccio risente molto del pattern, Airpassenger è una serie con pattern lineare crescente e un effetto stagionale moltiplicativo (le onde sono sempre più alte ogni anno), l’approccio dell’imputazione utilizzando la media crea picchi all’inizio della sequenza e valli sul finire, anche se queste ultime sono molto meno evidenti dei primi. Se la serie fosse ancora più lunga questo fenomeno sarebbe sempre più marcato. Di seguito il confronto con l’originale (in rosso).
La mediana è una misura di sintesi robusta agli outlier, quindi si potrebbe pensare che l’effetto pattern venga un po’ mitigato utilizzando questa strategia.
airpassengers_median = imputer_median.fit_transform(airpassengers_MV)
airpassengers_median[:5]
array([[112.],
[118.],
[132.],
[268.],
[121.]])
L’unica cosa che è cambiata è che la mediana è più bassa della media, quindi i picchi sono più bassi, ma si acuisce l’effetto nella parte finale della serie. In conclusione:
Le strategie con media e mediana possono essere chiaramente scartate per l’imputazione di lacking values nelle serie storiche.
I paragrafi successivi servono solo per testare anche le restanti opzioni presenti tra le strategie di SimpleImputer. Solitamente queste vengono utilizzate su variabili categoriche, non numeriche proceed. Quindi sappiamo a priori che non si adatteranno bene alle time sequence.
imputer_mode = SimpleImputer(technique="most_frequent")
airpassengers_mode = imputer_mode.fit_transform(airpassengers_MV)
airpassengers_mode[:5]
array([[112.],
[118.],
[132.],
[229.],
[121.]])
Visto che il pattern cresce linearmente, e la stagionalità con effetto moltiplicativo, la moda è un valore più basso della media, ma anche della mediana. In questo caso sembra dare un andamento quasi naturale alla serie nella parte centrale, ma quando il valore mancante si trova nei primi/ultimi periodi l’errore sistematico è evidente.
N.B. La strategia “Most Frequent” è molto utile se si hanno lacking values in variabili categoriche, come detto in precedenza. Un’altra strategia in questi casi può essere applicare un modello di Machine Studying (es., KNN) usando come goal la variabile di cui dobbiamo imputare i lacking values e come options tutte le altre, creando il coaching set per allenare il modello sui non lacking e applicando il modello sul take a look at, ovvero sulle osservazioni che presentano i dati mancanti.
Il primo problema che ci si pone in questo caso è “ Quale costante utilizzare?”:
- un quantile?
- il minimo/massimo?
- un numero a caso?
Per questo take a look at verranno usati lo zero, il minimo e il massimo.
Sostituzione con zero
Lo zero solitamente è sconsigliatissimo perchè non si capisce se è assenza di valore o presenza pari a zero, crea molta abiguità.
Esempio: presenza advert eventi, se viene effettuata la sostituzione con zero non si riesce più a riconoscere se è un dato imputato o effettivamente c’è presenza zero (in aggiunta al restante errore indotto).
imputer_zero = SimpleImputer(technique="fixed", fill_value=0)
airpassengers_zero = imputer_zero.fit_transform(airpassengers_MV)
Sostituzione con massimo/minimo
Queste due strategie ci daranno time sequence completamente sballate (a cui dedicheremo pochissime righe).
imputer_max = SimpleImputer(technique="fixed", fill_value=airpassengers_MV.Passengers.max())
airpassengers_max = imputer_max.fit_transform(airpassengers_MV)
Analogamente per il minimo:
imputer_min = SimpleImputer(technique="fixed", fill_value=airpassengers_MV.Passengers.min())
airpassengers_min = imputer_min.fit_transform(airpassengers_MV)
La strategia vincente con le serie storiche è quella dell’imputazione per media cell. Esistono diversi tipi di media cell (qui la [pagina Wikipedia] (https://it.wikipedia.org/wiki/Media_mobile)), nella maggior parte dei casi può bastare quella lineare che verrà utilizzata in questo articolo.
Knowledge un serie storica y_t con t=(1, 2, …, T), sia un generico elemento della serie $t$ un valore mancante e information una finestra temporale di dimensione $N$, siano:
- m_1 gli N periodi antecedenti il valore mancante
- m_2 gli N periodi successivi al valore mancante
- theta_i il peso da attribuire all’i-esimo valore osservato. (Per noi sarà pari advert 1 visto che vogliamo una media aritmetica semplice).
Si definisce media cell al tempo t:
Dove okay = {m_1} + {m_2} + 1
NOTA. In questo articolo non useremo la funzione rolling_mean di Pandas per mostrare come funziona la media cell ma verrà costruita un piccolo script ad-hoc.
L’imputazione con questa tecnica permette di imputare al valore mancante la media locale in un vary deciso dall’analista a seconda del fenomeno che si và a studiare.
Il metodo è un pò più esoso in termini di calcolo e presenta alcuni problemi che vanno risolti, advert esempio:
- se manca il valore all’inizio o alla high-quality della serie?
- se nel vary indicato ci sono più valori mancanti?
- se abbiamo lacking values contigui?
Sta al information scientist scegliere quale soluzione applicare advert ogni domanda a seconda del contesto.
Nel nostro caso applicheremo una finestra temporale tra i tre valori prima e i tre valori dopo il lacking worth, gestendo i due casi:
- Il decrease sure è negativo
- L’higher sure supera la lunghezza della serie.
Se nel vary si presentano più lacking values viene semplicemente calcolata la media tra i valori presenti.
Esempio:
[2, NaN, 3] → [2, 2.5, 3]
[2, NaN, NaN] → [2, 2, 2]
La prima cosa che si nota da questo esempio è che cade il +1 finale nel calcolo di okay visto che quel valore per noi è mancante.
airpassengers_MA = airpassengers_MV.copy()
steps = 3for idx in na_index:
decrease = idx - steps
higher = idx + steps + 1
if decrease<0:
decrease=0
if higher>airpassengers_MA.form[0]:
higher=airpassengers_MA.form[0]
airpassengers_MA.iloc[idx,0] = airpassengers_MA.iloc[lower:upper,0].imply()
L’andamento sembra abbastanza naturale, questo è confermato dal confronto con la serie storica originale. Quindi per le serie storiche la media cell è nettamente la strategia vincente.
Imputare un valore mancante introduce sempre errore sistematico, si può solo scegliere la strategia più adatta per minimizzarlo.