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.apply
e Series.apply
sono funzioni utili definite rispettivamente sull'oggetto DataFrame e Series. apply
accetta 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 apply
possono 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
agg
o transform
in questi casi)
- Esegui trasformazioni in base agli elementi
- Trasmetti i risultati aggregati alle righe originali (vedi l'
result_type
argomento).
- 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é è apply
cattivo? È 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 apply
incorrere in un notevole sovraccarico ad ogni iterazione. Inoltre, apply
consuma 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 apply
per 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' raw
argomento, è 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
: astype
versusapply
Sembra un'idiosincrasia dell'API. L'utilizzo apply
per convertire interi in una serie in stringa è paragonabile (e talvolta più veloce) rispetto all'utilizzo astype
.
Il grafico è stato tracciato utilizzando la perfplot
libreria.
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.apply
non è stata discussa fino ad ora, ma GroupBy.apply
è anche una comoda funzione iterativa per gestire tutto ciò che le GroupBy
funzioni 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 groupby
chiamata (perché groupby
è anche piuttosto costosa).
Altri avvertimenti
A parte gli avvertimenti sopra menzionati, vale anche la pena ricordare che apply
opera due volte sulla prima riga (o colonna). Questo viene fatto per determinare se la funzione ha effetti collaterali. In caso contrario, apply
potrebbe 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.apply
versioni 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 cuiapply
sarà generalmente leggermente più veloce, che è la casella verde in basso a destra nel diagramma di jpp sotto.