apply, la comoda funzione di cui non hai mai avuto bisogno
Iniziamo affrontando le domande nel PO, una per una.
" Se l' applicazione è così negativa, perché è nell'API? "
DataFrame.applye Series.applysono funzioni utili definite rispettivamente sull'oggetto DataFrame e Series. applyaccetta qualsiasi funzione definita dall'utente che applica una trasformazione / aggregazione su un DataFrame. applyè effettivamente un proiettile d'argento che fa tutto ciò che qualsiasi funzione esistente dei panda non può fare.
Alcune cose applypossono fare:
- Esegui qualsiasi funzione definita dall'utente su un DataFrame o una serie
- Applicare una funzione per riga (
axis=1) o per colonna ( axis=0) su un DataFrame
- Eseguire l'allineamento dell'indice durante l'applicazione della funzione
- Eseguire l'aggregazione con funzioni definite dall'utente (tuttavia, di solito si preferisce
aggo transformin questi casi)
- Esegui trasformazioni in base agli elementi
- Trasmetti i risultati aggregati alle righe originali (vedi l'
result_typeargomento).
- Accetta argomenti posizionali / parola chiave da passare alle funzioni definite dall'utente.
...Tra gli altri. Per ulteriori informazioni, vedere Applicazione di funzioni per riga o colonna nella documentazione.
Quindi, con tutte queste caratteristiche, perché è applycattivo? È perché applyè lento . Panda non fa supposizioni sulla natura della tua funzione, quindi applica in modo iterativo la tua funzione a ciascuna riga / colonna, se necessario. Inoltre, gestire tutte le situazioni di cui sopra significa applyincorrere in un notevole sovraccarico ad ogni iterazione. Inoltre, applyconsuma molta più memoria, il che rappresenta una sfida per le applicazioni limitate dalla memoria.
Ci sono pochissime situazioni in cui applyè opportuno utilizzare (più su quello sotto). Se non sei sicuro se dovresti usare apply, probabilmente non dovresti.
Affrontiamo la prossima domanda.
" Come e quando devo fare il mio codice di applicare -free? "
Per riformulare, ecco alcune situazioni comuni in cui vorresti sbarazzarti di qualsiasi chiamata a apply.
Dati numerici
Se stai lavorando con dati numerici, probabilmente esiste già una funzione cython vettorizzata che fa esattamente quello che stai cercando di fare (in caso contrario, fai una domanda su Stack Overflow o apri una richiesta di funzionalità su GitHub).
Contrasta le prestazioni di applyper una semplice operazione di aggiunta.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
Dal punto di vista delle prestazioni, non c'è paragone, l'equivalente citato è molto più veloce. Non c'è bisogno di un grafico, perché la differenza è evidente anche per i dati dei giocattoli.
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Anche se abiliti il passaggio di array non elaborati con l' rawargomento, è ancora due volte più lento.
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Un altro esempio:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In generale, cerca alternative vettorializzate se possibile.
String / Regex
Pandas fornisce funzioni stringa "vettorializzate" nella maggior parte delle situazioni, ma ci sono rari casi in cui quelle funzioni non ... "si applicano", per così dire.
Un problema comune è controllare se un valore in una colonna è presente in un'altra colonna della stessa riga.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
Questo dovrebbe restituire la seconda e la terza riga, poiché "donald" e "minnie" sono presenti nelle rispettive colonne "Title".
Usando applica, questo sarebbe fatto usando
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
Tuttavia, esiste una soluzione migliore utilizzando le liste di comprensione.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
La cosa da notare qui è che le routine iterative sono più veloci di apply, a causa dell'overhead inferiore. Se hai bisogno di gestire NaN e dtype non validi, puoi costruire su questo utilizzando una funzione personalizzata che puoi quindi chiamare con argomenti all'interno della comprensione dell'elenco.
Per ulteriori informazioni su quando la comprensione degli elenchi dovrebbe essere considerata una buona opzione, vedere il mio articolo: For loops with pandas - When should I care? .
Nota Anche le
operazioni data e data / ora hanno versioni vettoriali. Così, per esempio, si dovrebbe preferire pd.to_datetime(df['date']), oltre, dire df['date'].apply(pd.to_datetime).
Per saperne di più sui
documenti .
Una trappola comune: colonne di elenchi che esplodono
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
Le persone sono tentate di usare apply(pd.Series). Questo è orribile in termini di prestazioni.
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
Un'opzione migliore è elencare la colonna e passarla a pd.DataFrame.
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Infine,
" Ci sono situazioni in cui apply va bene? "
Applica è una funzione comoda, quindi ci sono situazioni in cui il sovraccarico è abbastanza trascurabile da perdonare. Dipende davvero da quante volte viene chiamata la funzione.
Funzioni che sono vettorizzate per le serie, ma non per i DataFrame
E se si desidera applicare un'operazione di stringa su più colonne? E se volessi convertire più colonne in datetime? Queste funzioni sono vettorializzate solo per le serie, quindi devono essere applicate su ogni colonna su cui si desidera convertire / operare.
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
Questo è un caso ammissibile per apply:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
Nota che avrebbe anche senso stack, o semplicemente usare un ciclo esplicito. Tutte queste opzioni sono leggermente più veloci dell'uso apply, ma la differenza è abbastanza piccola da perdonare.
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
È possibile creare un caso simile per altre operazioni come le operazioni sulle stringhe o la conversione in categoria.
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
E così via...
Conversione di serie in str: astypeversusapply
Sembra un'idiosincrasia dell'API. L'utilizzo applyper convertire interi in una serie in stringa è paragonabile (e talvolta più veloce) rispetto all'utilizzo astype.
Il grafico è stato tracciato utilizzando la perfplotlibreria.
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
Con i galleggianti, vedo che astypeè costantemente veloce come, o leggermente più veloce di apply. Quindi questo ha a che fare con il fatto che i dati nel test sono di tipo intero.
GroupBy operazioni con trasformazioni concatenate
GroupBy.applynon è stata discussa fino ad ora, ma GroupBy.applyè anche una comoda funzione iterativa per gestire tutto ciò che le GroupByfunzioni esistenti non fanno.
Un requisito comune è eseguire un GroupBy e quindi due operazioni principali come un "cumsum ritardato":
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
Avresti bisogno di due successive chiamate di gruppo qui:
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Usando apply, puoi abbreviare questo a una singola chiamata.
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
È molto difficile quantificare le prestazioni perché dipende dai dati. Ma in generale, applyè una soluzione accettabile se l'obiettivo è ridurre una groupbychiamata (perché groupbyè anche piuttosto costosa).
Altri avvertimenti
A parte gli avvertimenti sopra menzionati, vale anche la pena ricordare che applyopera due volte sulla prima riga (o colonna). Questo viene fatto per determinare se la funzione ha effetti collaterali. In caso contrario, applypotrebbe essere in grado di utilizzare un percorso veloce per valutare il risultato, altrimenti ricade su un'implementazione lenta.
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
Questo comportamento si vede anche nelle GroupBy.applyversioni di Panda <0.25 (è stato corretto per 0.25, vedi qui per maggiori informazioni .)
returns.add(1).apply(np.log)vs.np.log(returns.add(1)è un caso in cuiapplysarà generalmente leggermente più veloce, che è la casella verde in basso a destra nel diagramma di jpp sotto.