2016-05-02 38 views
7

Ho un dataframe panda e voglio calcolare la media mobile di una colonna (dopo una clausola groupby). Tuttavia, voglio escludere i NaN.panda groupby e rolling_apply ignorando NaNs

Ad esempio, se il groupby restituisce [2, NaN, 1], il risultato dovrebbe essere 1,5 mentre attualmente restituisce NaN.

Ho provato quanto segue, ma non sembra funzionare:

df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 3, lambda x: np.mean([i for i in x if i is not np.nan and i!='NaN'])) 

Se ho nemmeno provato questo:

df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 3, lambda x: 1) 

sto ottenendo NaN nell'output così deve essere qualcosa a che fare con il modo in cui i panda lavorano in background.

Qualche idea?

EDIT: Ecco un esempio di codice con quello che sto cercando di fare:

import pandas as pd 
import numpy as np 

df = pd.DataFrame({'var1' : ['a', 'b', 'a', 'b', 'a', 'b', 'a', 'b'], 'value' : [1, 2, 3, np.nan, 2, 3, 4, 1] }) 
print df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 2, lambda x: np.mean([i for i in x if i is not np.nan and i!='NaN'])) 

Il risultato è:

0 NaN 
1 NaN 
2 2.0 
3 NaN 
4 2.5 
5 NaN 
6 3.0 
7 2.0 

mentre io volevo avere la seguente:

0 NaN 
1 NaN 
2 2.0 
3 2.0 
4 2.5 
5 3.0 
6 3.0 
7 2.0 
+1

Si prega di fornire un piccolo insieme riproducibile di codice tale che io possa giocare con informazioni simili che possiedi senza dovermi inventare da solo. – piRSquared

+0

@piRSquared Ho appena aggiunto un esempio di codice. Grazie – Stergios

risposta

1

Questo risultato può essere all'altezza delle vostre aspettative? Ho leggermente modificato la soluzione con il parametro min_periods e il filtro destro per nan.

In [164]: df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 2, lambda x: np.mean([i for i in x if not np.isnan(i)]), min_periods=1) 
Out[164]: 
0 1.0 
1 2.0 
2 2.0 
3 2.0 
4 2.5 
5 3.0 
6 3.0 
7 2.0 
dtype: float64 
+0

Questo è un uso intelligente di 'min_period'! – IanS

1

Ecco un'implementazione alternativa senza comprensione lista, ma riesce anche a popolare il primo ingresso dell'uscita con np.nan

means = df.groupby('var1')['value'].apply(
    lambda gp: gp.rolling(2, min_periods=1).apply(np.nanmean)) 
8

Come sempre in panda, attenersi a metodi vettorializzate (cioè evitando apply) è essenziale per prestazioni e scalabilità.

L'operazione che si desidera eseguire è un po 'complicata poiché le operazioni di rotazione su oggetti groupby non sono al momento consapevoli di NaN (versione 0.18.1). Come tale, avremo bisogno di un paio di brevi righe di codice:

g1 = df.groupby(['var1'])['value']    # group values 
g2 = df.fillna(0).groupby(['var1'])['value'] # fillna, then group values 

s = g2.rolling(2).sum()/g1.rolling(2).count() # the actual computation 

s.reset_index(level=0, drop=True).sort_index() # drop/sort index 

L'idea è quella di sommare i valori nella finestra (usando sum), contare i valori NaN (utilizzando count) e poi dividere per trovare il significare. Questo codice ha pronunciato la seguente uscita che corrisponde al risultato desiderato:

0 NaN 
1 NaN 
2 2.0 
3 2.0 
4 2.5 
5 3.0 
6 3.0 
7 2.0 
Name: value, dtype: float64 

prove su un dataframe più grande (circa 100.000 righe), il run-time era sotto 100ms, significativamente più veloce di qualsiasi metodo di applicare basati ho provato.

Può valere la pena testare i diversi approcci sui dati effettivi poiché i tempi possono essere influenzati da altri fattori come il numero di gruppi. È abbastanza certo che i calcoli vettorializzati vinceranno comunque.


L'approccio mostrato sopra funziona bene per calcoli semplici, come la media rotolante.Funzionerà per calcoli più complicati (come lo scostamento della deviazione standard), sebbene l'implementazione sia maggiormente coinvolta.

L'idea generale è esaminare ciascuna routine semplice che è veloce in panda (ad esempio sum) e quindi riempire qualsiasi valore null con un elemento di identità (ad esempio 0). Puoi quindi utilizzare groubpy ed eseguire l'operazione di rotazione (ad esempio .rolling(2).sum()). L'output è quindi combinato con l'output (s) di altre operazioni.

Ad esempio, per implementare in base alla variazione rolling di NaN-aware (di cui la deviazione standard è la radice quadrata) dobbiamo trovare "la media dei quadrati meno il quadrato della media". Ecco uno schizzo di ciò che questo potrebbe sembrare:

def rolling_nanvar(df, window): 
    """ 
    Group df by 'var1' values and then calculate rolling variance, 
    adjusting for the number of NaN values in the window. 

    Note: user may wish to edit this function to control degrees of 
    freedom (n), depending on their overall aim. 
    """ 
    g1 = df.groupby(['var1'])['value'] 
    g2 = df.fillna(0).groupby(['var1'])['value'] 
    # fill missing values with 0, square values and groupby 
    g3 = df['value'].fillna(0).pow(2).groupby(df['var1']) 

    n = g1.rolling(window).count() 

    mean_of_squares = g3.rolling(window).sum()/n 
    square_of_mean = (g2.rolling(window).sum()/n)**2 
    variance = mean_of_squares - square_of_mean 
    return variance.reset_index(level=0, drop=True).sort_index() 

Si noti che questa funzione non può essere numericamente stabile (squadratura potrebbe portare a overflow). panda usa Welford's algorithm internamente per mitigare questo problema.

In ogni caso, questa funzione, sebbene utilizzi diverse operazioni, è ancora molto veloce. Ecco un confronto con il metodo di applicazione basata su più concisa suggerito da Yakym Pirozhenko:

>>> df2 = pd.concat([df]*10000, ignore_index=True) # 80000 rows 
>>> %timeit df2.groupby('var1')['value'].apply(\ 
     lambda gp: gp.rolling(7, min_periods=1).apply(np.nanvar)) 
1 loops, best of 3: 11 s per loop 

>>> %timeit rolling_nanvar(df2, 7) 
10 loops, best of 3: 110 ms per loop 

Vettorizzazione è 100 volte più veloce in questo caso. Ovviamente, a seconda della quantità di dati che hai, potresti voler continuare a usare apply poiché ti consente generalità/brevità a scapito delle prestazioni.

+0

Si noti che questo usa il metodo 'rolling', che è disponibile solo in panda 18, mentre OP usa' pd.rolling_apply', quindi molto probabilmente ha installato pandas 17 o inferiore. – IanS

+0

@ajcr Questo risolve il mio problema corrente ma richiede una completa riscrittura se voglio applicare funzioni diverse dalla media (ad esempio rolling deviazione standard). C'è un modo per farlo funzionare anche per altre funzioni? – Stergios

+0

@Stergios: Darò il problema ancora un po 'di riflessione e aggiungerò altre linee guida/suggerimenti a questa risposta più tardi oggi/domani. È certamente vero che l'uso di 'apply' è a volte l'opzione più conveniente e fornisce la soluzione più generale (non tutte le funzioni sono facili da imitare con le routine di panda accelerate). –