2015-12-29 16 views
7

Uso i dati di una sfida passata del kaggle in base ai dati del pannello in un certo numero di negozi e un periodo di 2,5 anni. Ogni osservazione include il numero di clienti per una determinata data di negozio. Per ogni data di negozio, il mio obiettivo è calcolare il numero medio di clienti che hanno visitato questo negozio negli ultimi 60 giorni.Accelerazione della media degli ultimi 60 giorni nei panda

Di seguito è riportato il codice che fa esattamente ciò di cui ho bisogno. Tuttavia, dura per sempre: ci vorrebbe una notte per elaborare le righe di c.800k. Sto cercando un modo intelligente per raggiungere lo stesso obiettivo più velocemente.

Ho incluso 5 osservazioni del set di dati iniziale con le variabili rilevanti: store id (Store), Data e numero di clienti ("Clienti").

Nota:

  • Per ogni riga nella iterazione, che finiscono scrivere i risultati utilizzando .loc invece di esempio row ["Lagged No of customers"] perché "row" non scrive nulla nelle celle. Mi chiedo perché sia ​​così.
  • Normalmente compro le nuove colonne usando "apply, axis = 1", quindi apprezzerei davvero qualsiasi soluzione basata su questo. Ho scoperto che "apply" funziona bene quando per ogni riga, il calcolo viene eseguito su colonne utilizzando valori allo stesso livello di riga. Tuttavia, non so come una funzione "applica" possa coinvolgere diverse righe, che è ciò che questo problema richiede. l'unica eccezione che ho visto finora è "diff", che non è utile qui.

Grazie.


dati del campione:

pd.DataFrame({ 
    'Store': {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}, 
    'Customers': {0: 668, 1: 578, 2: 619, 3: 635, 4: 785}, 
    'Date': { 
    0: pd.Timestamp('2013-01-02 00:00:00'), 
    1: pd.Timestamp('2013-01-03 00:00:00'), 
    2: pd.Timestamp('2013-01-04 00:00:00'), 
    3: pd.Timestamp('2013-01-05 00:00:00'), 
    4: pd.Timestamp('2013-01-07 00:00:00') 
    } 
}) 

codice che funziona, ma è incredibilmente lento:

import pandas as pd 
import numpy as np 
data = pd.read_csv("Rossman - no of cust/dataset.csv") 
data.Date = pd.to_datetime(data.Date) 
data.Customers = data.Customers.astype(int) 

for index, row in data.iterrows(): 
    d = row["Date"] 
    store = row["Store"] 
    time_condition = (d - data["Date"]<np.timedelta64(60, 'D')) & (d > data["Date"]) 

    sub_df = data.loc[ time_condition & (data["Store"] == store), :] 

    data.loc[ (data["Date"]==d) & (data["Store"] == store), "Lagged No customers"] = sub_df["Customers"].sum() 
    data.loc[ (data["Date"]==d) & (data["Store"] == store), "No of days"] = len(sub_df["Customers"]) 
    if len(sub_df["Customers"]) > 0: 
     data.loc[ (data["Date"]==d) & (data["Store"] == store), "Av No of customers"] = int(sub_df["Customers"].sum()/len(sub_df["Customers"])) 
+0

Perché si prende le prime 100 righe di 'data'? ('data [: 100]') –

+0

@Alexander 'time_condition' è solo una maschera che seleziona la finestra temporale corretta, che viene successivamente utilizzata per creare sub_df; @David ZI prende 100 righe per non passare la notte in attesa dell'output, ma l'obiettivo è di avere l'output per l'intero set di dati –

+0

In questo caso penso che sia meglio lasciare il '[: 100]' del codice di esempio in la tua domanda. Dopotutto, il tuo codice di esempio dovrebbe illustrare il problema che stai riscontrando - in questo caso, essere troppo lento. Non è un grosso problema, ma se ti ritrovi a modificare di nuovo per qualche altro motivo, potresti anche prenderlo in considerazione. –

risposta

6

Dato i tuoi dati di esempio piccolo, ho usato una media mobile due giorni invece di 60 giorni.

>>> (pd.rolling_mean(data.pivot(columns='Store', index='Date', values='Customers'), window=2) 
    .stack('Store')) 
Date  Store 
2013-01-03 1  623.0 
2013-01-04 1  598.5 
2013-01-05 1  627.0 
2013-01-07 1  710.0 
dtype: float64 

Prendendo un perno dei dati con date come l'indice e memorizza come le colonne, si può semplicemente prendere una media mobile. È quindi necessario impilare i negozi per riportare i dati nella forma corretta.

Ecco alcuni esempio di output dei dati originali prima della pila finale:

Store   1  2  3 
Date       
2015-07-29 541.5 686.5 767.0 
2015-07-30 534.5 664.0 769.5 
2015-07-31 550.5 613.0 822.0 

Dopo .stack('Store'), questo diventa:

Date  Store 
2015-07-29 1  541.5 
      2  686.5 
      3  767.0 
2015-07-30 1  534.5 
      2  664.0 
      3  769.5 
2015-07-31 1  550.5 
      2  613.0 
      3  822.0 
dtype: float64 

Supponendo che quanto sopra è chiamato df, è possibile quindi unire torna ai tuoi dati originali come segue:

data.merge(df.reset_index(), 
      how='left', 
      on=['Date', 'Store']) 

EDIT: Nei dati è presente un motivo stagionale chiaro per il quale è possibile effettuare le regolazioni. In ogni caso, probabilmente vorrai che la tua media mobile sia in multipli di sette per rappresentare anche settimane. Ho usato una finestra temporale di 63 giorni nell'esempio seguente (9 settimane).

Per evitare di perdere dati nei negozi che si aprono (e quelli all'inizio del periodo di tempo), è possibile specificare min_periods=1 nella funzione di media mobile. Questo vi darà il valore medio su tutte le osservazioni disponibili per il vostro data finestra di tempo

df = data.loc[data.Customers > 0, ['Date', 'Store', 'Customers']] 
result = (pd.rolling_mean(df.pivot(columns='Store', index='Date', values='Customers'), 
      window=63, min_periods=1) 
     .stack('Store')) 
result.name = 'Customers_63d_mvg_avg' 
df = df.merge(result.reset_index(), on=['Store', 'Date'], how='left') 

>>> df.sort_values(['Store', 'Date']).head(8) 
       Date Store Customers Customers_63d_mvg_avg 
843212 2013-01-02  1  668    668.000000 
842103 2013-01-03  1  578    623.000000 
840995 2013-01-04  1  619    621.666667 
839888 2013-01-05  1  635    625.000000 
838763 2013-01-07  1  785    657.000000 
837658 2013-01-08  1  654    656.500000 
836553 2013-01-09  1  626    652.142857 
835448 2013-01-10  1  615    647.500000 

Per vedere più chiaramente cosa sta succedendo, qui è un esempio di giocattolo:

s = pd.Series([1,2,3,4,5] + [np.NaN] * 2 + [6]) 
>>> pd.concat([s, pd.rolling_mean(s, window=4, min_periods=1)], axis=1) 
    0 1 
0 1 1.0 
1 2 1.5 
2 3 2.0 
3 4 2.5 
4 5 3.5 
5 NaN 4.0 
6 NaN 4.5 
7 6 5.5 

La finestra è di quattro osservazioni , ma nota che il valore finale di 5.5 è uguale a (5 + 6)/2. I valori 4.0 e 4.5 sono (3 + 4 + 5)/3 e (4 + 5)/2, rispettivamente.

Nel nostro esempio, le righe NaN della tabella pivot non vengono riunite in df perché abbiamo fatto un join di sinistra e tutte le righe in df hanno uno o più clienti.

È possibile visualizzare un grafico dei dati di laminazione come segue:

df.set_index(['Date', 'Store']).unstack('Store').plot(legend=False) 

enter image description here

+0

Grazie non l'avevo mai visto prima :) rolling_sum potrebbe anche essere utile per altri problemi simili che ho avuto in passato. Lo proverò APPENA POSSIBILE sul set di dati completo per testare la velocità. –

+0

L'ho appena testato sul file di dati di allenamento 5.66mb. Meno di mezzo secondo. È quindi necessario unirlo indietro. – Alexander

+4

Stavo lavorando su una soluzione come questa, ma non sembra fare esattamente la stessa cosa dell'implementazione di riferimento nella domanda. Quel codice, quando prende la media, si divide per il numero di date negli ultimi 60 giorni per cui ci sono effettivamente dati nel set di dati - un numero che è tipicamente inferiore a 60. (Questo numero è posto nel "Numero di date" "column)" rolling_mean' sembra duplicare effettivamente i dati nelle righe vuote e quindi dividere per 60 o qualcosa del genere. In ogni caso, i miei test dimostrano che i risultati non sono esattamente gli stessi. –