Trattare i valori mancanti nelle serie storiche


Il trattamento e l'imputazione dei valori mancanti (missing values) sono due step molto delicati per ogni progetto di data science. Nonostante esistano diverse 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 feature che si sta trattando una nuova variabile booleana "nomeFeature_isMissing" per tracciare quali valori sono reali e quali indotti durante il processo di cleaning.

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 feature;
  • Non eliminare mai l'intera osservazione a meno che non abbia valori mancanti su ogni feature;
  • 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:

  1. sostituire i missing values con la media;
  2. sostituire i missing values con la mediana;
  3. sostituire i missing values con la moda;
  4. sostituire i missing 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 test utilizzando la serie storica Airpassenger, una sequenza mensile del numero di passeggeri sui voli internazionali tra il 1949 e il 1960.

airpassenger

Tutto il codice di questo articolo è disponibile su un notebook Google Colab.

Creazione dei Missing Values

Airpassenger non presenta valori mancanti, quindi verranno introdotti in modo completamente casuale.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

airpassengers = 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.shape[0]-1, 10)
airpassengers_MV.iloc[na_index,0] = np.nan

plt.figure(figsize=(16,8))
plt.plot(range(airpassengers.shape[0]), airpassengers_MV["Passengers"].values, color='tab:blue')
plt.gca().set(title="Airpassengers - Missing Values", xlabel="Time", ylabel="Passengers")
plt.show()
Month Passengers
1949-01 112
1949-02 118
1949-03 132
1949-04 NaN
1949-05 121
TS_with_missing

Imputazione dei Missing Values

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 fit, transform. Nel nostro esempio non avendo interesse verso il forecasting e quindi non dividendo la serie in train e test, useremo fit_transform su tutta la sequenza.

Il metodo non è altro che la concatenazione dei due:

  • fit calcola il valore da imputare (media, mediana, più frequente) sulla serie;
  • transform applica realmente l'imputazione.

In input necessita di un oggetto 2D (noi passeremo un DataFrame) ma restituisce un array NumPy.

Sostituzione con la media

La strategia di SimpleImputer viene applicata mediante il parametro strategy, come detto in precedenza si può scegliere tra:

  • "mean" default;
  • "median";
  • "most_frequent";
  • "costant".
imputer_mean = SimpleImputer(strategy = "mean")
airpassengers_mean = imputer_mean.fit_transform(airpassengers_MV)

airpassengers_mean[:5]

array([[112. ], [118. ], [132. ], [282.23880597], [121. ]])

in grassetto il valore imputato

missing_mean

Questo approccio risente molto del trend, Airpassenger è una serie con trend 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).

confronto_mean

Sostituzione con la mediana

La mediana è una misura di sintesi robusta agli outlier, quindi si potrebbe pensare che l'effetto trend venga un po' mitigato utilizzando questa strategia.

airpassengers_median = imputer_median.fit_transform(airpassengers_MV)
airpassengers_median[:5]

array([[112.], [118.], [132.], [268.], [121.]])

airpassenger_median
confronto_median

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 missing 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 continue. Quindi sappiamo a priori che non si adatteranno bene alle time series.

Sostituzione con la moda

imputer_mode = SimpleImputer(strategy="most_frequent")
airpassengers_mode = imputer_mode.fit_transform(airpassengers_MV)
airpassengers_mode[:5]

array([[112.], [118.], [132.], [229.], [121.]])

Visto che il trend 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.

airpassenger_mode

N.B. La strategia "Most Frequent" è molto utile se si hanno missing values in variabili categoriche, come detto in precedenza. Un'altra strategia in questi casi può essere applicare un modello di Machine Learning (es., KNN) usando come target la variabile di cui dobbiamo imputare i missing values e come features tutte le altre, creando il training set per allenare il modello sui non missing e applicando il modello sul test, ovvero sulle osservazioni che presentano i dati mancanti.

Sostituzione con una costante

Il primo problema che ci si pone in questo caso è "Quale costante utilizzare?":

  • un quantile?
  • il minimo/massimo?
  • un numero a caso?

Per questo test 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 ad 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(strategy="constant", fill_value=0)
airpassengers_zero = imputer_zero.fit_transform(airpassengers_MV)
airpassenger_zero

Sostituzione con massimo/minimo

Queste due strategie ci daranno time series completamente sballate (a cui dedicheremo pochissime righe).

imputer_max = SimpleImputer(strategy="constant", fill_value=airpassengers_MV.Passengers.max())
airpassengers_max = imputer_max.fit_transform(airpassengers_MV)
imputer_max

Analogamente per il minimo:

imputer_min = SimpleImputer(strategy="constant", fill_value=airpassengers_MV.Passengers.min())
airpassengers_min = imputer_min.fit_transform(airpassengers_MV)
imputer_min

Imputazione per Media Mobile

La strategia vincente con le serie storiche è quella dell'imputazione per media mobile. Esistono diversi tipi di media mobile (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.

Data un serie storica ${y_t}$ con $t=(1, 2, ..., T)$, sia un generico elemento della serie $t$ un valore mancante e data 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 ad 1 visto che vogliamo una media aritmetica semplice).

Si definisce media mobile al tempo $t$:

$mm_i$ = $\frac{1}{k}$ $\sum_{i=-m_1}^{m_2}$ $\theta_i$ $y_{t+1}$

Dove $k = {m_1} + {m_2} + 1$

NOTA. In questo articolo non useremo la funzione rolling_mean di Pandas per mostrare come funziona la media mobile ma verrà costruita un piccolo script ad-hoc.

L'imputazione con questa tecnica permette di imputare al valore mancante la media locale in un range 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, ad esempio:

  • se manca il valore all'inizio o alla fine della serie?
  • se nel range indicato ci sono più valori mancanti?
  • se abbiamo missing values contigui?

Sta al data scientist scegliere quale soluzione applicare ad ogni domanda a seconda del contesto.

Nel nostro caso applicheremo una finestra temporale tra i tre valori prima e i tre valori dopo il missing value, gestendo i due casi:

  1. Il lower bound è negativo
  2. L'upper bound supera la lunghezza della serie.

Se nel range si presentano più missing 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 k visto che quel valore per noi è mancante.

airpassengers_MA = airpassengers_MV.copy()
steps = 3

for idx in na_index:
    lower = idx - steps
    upper = idx + steps + 1
    if lower<0:
        lower=0
    if upper>airpassengers_MA.shape[0]:
        upper=airpassengers_MA.shape[0]

    airpassengers_MA.iloc[idx,0] = airpassengers_MA.iloc[lower:upper,0].mean()
interpolazione

L'andamento sembra abbastanza naturale, questo è confermato dal confronto con la serie storica originale. Quindi per le serie storiche la media mobile è nettamente la strategia vincente.

confronto_interpolazione

Imputare un valore mancante introduce sempre errore sistematico, si può solo scegliere la strategia più adatta per minimizzarlo.


Se questo articolo ti è piaciuto e vuoi tenerti aggiornato sulle nostre attività, ricordati che l'iscrizione all'Italian Association for Machine Learning è gratuita! Puoi seguirci su Facebook, LinkedIn, e Twitter.

Previous Post Next Post