J'ai vu de nombreuses réponses postées à des questions sur Stack Overflow impliquant l'utilisation de la méthode Pandas apply
. J'ai également vu des utilisateurs commenter sous eux en disant que " apply
c'est lent et devrait être évité".
J'ai lu de nombreux articles sur le thème de la performance qui expliquent la apply
lenteur. J'ai également vu une clause de non-responsabilité dans la documentation sur la façon dont apply
est simplement une fonction pratique pour passer des UDF ( je ne peux pas le trouver maintenant). Donc, le consensus général est que cela apply
devrait être évité si possible. Cependant, cela soulève les questions suivantes:
- Si
apply
c'est si mauvais, alors pourquoi est-ce dans l'API? - Comment et quand dois-je rendre mon code sans code
apply
? - Y a-t-il déjà des situations où
apply
c'est bon (mieux que d'autres solutions possibles)?
returns.add(1).apply(np.log)
vsnp.log(returns.add(1)
est un cas oùapply
sera généralement légèrement plus rapide, qui est la case verte en bas à droite dans le diagramme de jpp ci-dessous.Réponses:
apply
, la fonction pratique dont vous n'avez jamais eu besoinNous commençons par aborder les questions du PO, une par une.
DataFrame.apply
etSeries.apply
sont des fonctions de commodité définies respectivement sur les objets DataFrame et Series.apply
accepte toute fonction définie par l'utilisateur qui applique une transformation / agrégation sur un DataFrame.apply
est en fait une solution miracle qui fait tout ce que les pandas existants ne peuvent pas faire.Certaines des choses
apply
peuvent faire:axis=1
) ou colonne (axis=0
) sur un DataFrameagg
outransform
dans ces cas)result_type
argument).... Entre autres. Pour plus d'informations, consultez Application de fonction par ligne ou par colonne dans la documentation.
Alors, avec toutes ces fonctionnalités, pourquoi est-ce
apply
mauvais? C'est parce queapply
c'est lent . Pandas ne fait aucune hypothèse sur la nature de votre fonction, et donc applique itérativement votre fonction à chaque ligne / colonne si nécessaire. De plus, la gestion de toutes les situations ci-dessusapply
entraîne une surcharge importante à chaque itération. De plus,apply
consomme beaucoup plus de mémoire, ce qui est un défi pour les applications limitées en mémoire.Il y a très peu de situations où il
apply
est approprié d'utiliser (plus de détails ci-dessous). Si vous ne savez pas si vous devriez utiliserapply
, vous ne devriez probablement pas.Abordons la question suivante.
Pour reformuler, voici quelques situations courantes dans lesquelles vous voudrez vous débarrasser de tout appel à
apply
.Données numériques
Si vous travaillez avec des données numériques, il existe probablement déjà une fonction cython vectorisée qui fait exactement ce que vous essayez de faire (sinon, posez une question sur Stack Overflow ou ouvrez une demande de fonctionnalité sur GitHub).
Comparez les performances de
apply
pour une opération d'addition simple.En termes de performances, il n'y a pas de comparaison, l'équivalent cythonisé est beaucoup plus rapide. Il n'y a pas besoin de graphique, car la différence est évidente même pour les données sur les jouets.
Même si vous activez le passage de tableaux bruts avec l'
raw
argument, c'est toujours deux fois plus lent.Un autre exemple:
En général, recherchez des alternatives vectorisées si possible.
Chaîne / expression régulière
Pandas fournit des fonctions de chaîne "vectorisées" dans la plupart des situations, mais il y a de rares cas où ces fonctions ne "s'appliquent", pour ainsi dire.
Un problème courant consiste à vérifier si une valeur dans une colonne est présente dans une autre colonne de la même ligne.
Cela devrait renvoyer la deuxième et la troisième ligne, puisque "donald" et "minnie" sont présents dans leurs colonnes "Titre" respectives.
En utilisant apply, cela serait fait en utilisant
Cependant, une meilleure solution existe en utilisant la compréhension de liste.
La chose à noter ici est que les routines itératives sont plus rapides que
apply
, en raison de la réduction des frais généraux. Si vous avez besoin de gérer des NaN et des dtypes non valides, vous pouvez utiliser une fonction personnalisée que vous pouvez ensuite appeler avec des arguments dans la compréhension de la liste.Pour plus d'informations sur le moment où la compréhension de liste doit être considérée comme une bonne option, consultez mon article: Pour les boucles avec des pandas - Quand dois-je m'en soucier? .
Un écueil courant: des colonnes de listes qui explosent
Les gens sont tentés d'utiliser
apply(pd.Series)
. C'est horrible en termes de performances.Une meilleure option consiste à lister la colonne et à la transmettre à pd.DataFrame.
Enfin,
Appliquer est une fonction pratique, il existe donc des situations où la surcharge est suffisamment négligeable pour pardonner. Cela dépend vraiment du nombre d'appels de la fonction.
Fonctions vectorisées pour les séries, mais pas pour les DataFrames
Que faire si vous souhaitez appliquer une opération de chaîne sur plusieurs colonnes? Que faire si vous souhaitez convertir plusieurs colonnes en datetime? Ces fonctions sont vectorisées pour les séries uniquement, elles doivent donc être appliquées sur chaque colonne que vous souhaitez convertir / opérer.
C'est un cas recevable pour
apply
:Notez qu'il serait également judicieux d'
stack
utiliser ou simplement d'utiliser une boucle explicite. Toutes ces options sont légèrement plus rapides que l'utilisationapply
, mais la différence est suffisamment petite pour pardonner.Vous pouvez faire un cas similaire pour d'autres opérations telles que les opérations de chaîne ou la conversion en catégorie.
contre
Etc...
Conversion de séries en
str
:astype
versusapply
Cela semble être une idiosyncrasie de l'API. L'utilisation
apply
pour convertir des entiers d'une série en chaîne est comparable (et parfois plus rapide) que l'utilisationastype
.Le graphique a été tracé à l'aide de la
perfplot
bibliothèque.Avec les flotteurs, je vois que le
astype
est toujours aussi rapide ou légèrement plus rapide queapply
. Cela a donc à voir avec le fait que les données du test sont de type entier.GroupBy
opérations avec transformations chaînéesGroupBy.apply
n'a pas été discuté jusqu'à présent, maisGroupBy.apply
c'est aussi une fonction de commodité itérative pour gérer tout ce que lesGroupBy
fonctions existantes ne font pas.Une exigence courante consiste à effectuer un GroupBy, puis deux opérations principales telles qu'un «retard cumulé»:
Vous auriez besoin de deux appels groupby successifs ici:
En utilisant
apply
, vous pouvez raccourcir cela en un seul appel.Il est très difficile de quantifier les performances car elles dépendent des données. Mais en général,
apply
c'est une solution acceptable si le but est de réduire ungroupby
appel (cargroupby
c'est aussi assez cher).Autres mises en garde
Outre les mises en garde mentionnées ci-dessus, il convient également de mentionner que
apply
fonctionne deux fois sur la première ligne (ou colonne). Ceci est fait pour déterminer si la fonction a des effets secondaires. Sinon,apply
vous pourrez peut-être utiliser un chemin rapide pour évaluer le résultat, sinon cela revient à une implémentation lente.Ce comportement est également observé dans
GroupBy.apply
les versions sur pandas <0.25 (il a été corrigé pour 0.25, voir ici pour plus d'informations .)la source
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
sûrement après la première itération, ce sera beaucoup plus rapide puisque vous vous convertissezdatetime
en ...datetime
?to_datetime
sur des chaînes est aussi rapide que sur ... desdatetime
objets" .. vraiment? J'ai inclus la création de dataframe (coût fixe) dans les timingsapply
vsfor
loop et la différence est beaucoup plus petite.Tous ne
apply
se ressemblent pasLe tableau ci-dessous indique quand considérer
apply
1 . Le vert signifie peut-être efficace; rouge éviter.Une partie de cela est intuitive: il
pd.Series.apply
s'agit d'une boucle de ligne par ligne au niveau de Python, idempd.DataFrame.apply
row-wise (axis=1
). Les abus de ceux-ci sont nombreux et variés. L'autre article les traite plus en profondeur. Les solutions populaires consistent à utiliser des méthodes vectorisées, des compréhensions de listes (suppose des données propres) ou des outils efficaces comme lepd.DataFrame
constructeur (par exemple à éviterapply(pd.Series)
).Si vous utilisez par
pd.DataFrame.apply
ligne, la spécificationraw=True
(si possible) est souvent bénéfique. À ce stade,numba
est généralement un meilleur choix.GroupBy.apply
: généralement favoriséLa répétition des
groupby
opérations à éviterapply
nuira aux performances.GroupBy.apply
c'est généralement bien ici, à condition que les méthodes que vous utilisez dans votre fonction personnalisée soient elles-mêmes vectorisées. Parfois, il n'y a pas de méthode Pandas native pour une agrégation de groupe que vous souhaitez appliquer. Dans ce cas, pour un petit nombre de groupesapply
avec une fonction personnalisée peut encore offrir des performances raisonnables.pd.DataFrame.apply
en colonne: un sac mélangépd.DataFrame.apply
column -wise (axis=0
) est un cas intéressant. Pour un petit nombre de lignes par rapport à un grand nombre de colonnes, c'est presque toujours cher. Pour un grand nombre de lignes par rapport aux colonnes, cas le plus courant, vous pouvez parfois constater des améliorations significatives des performances en utilisantapply
:1 Il existe des exceptions, mais celles-ci sont généralement marginales ou rares. Quelques exemples:
df['col'].apply(str)
peut légèrement surpasserdf['col'].astype(str)
.df.apply(pd.to_datetime)
travailler sur des chaînes ne s'adapte pas bien aux lignes par rapport à unefor
boucle normale .la source
apply
par ligne est beaucoup plus rapide que ma solution avecany
. Des pensées à ce sujet?any
c'est environ 100 fois plus rapide queapply
. Il a fait mes premiers tests avec 2000 lignes x 1000 cols et iciapply
c'était deux fois plus rapide queany
Pour
axis=1
(c'est-à-dire les fonctions par ligne), vous pouvez simplement utiliser la fonction suivante à la place deapply
. Je me demande pourquoi ce n'est pas lepandas
comportement. (Non testé avec des index composés, mais il semble être beaucoup plus rapide queapply
)la source
zip(df, row[1:])
suffit ici; vraiment, à ce stade, considéreznumba
si func est un calcul numérique. Voir cette réponse pour une explication.numba
c'est plus rapide,faster_df_apply
est destiné aux personnes qui veulent juste quelque chose d'équivalent, mais plus rapide,DataFrame.apply
(qui est étrangement lent).Y a-t-il déjà des situations où
apply
c'est bien? Oui, parfois.Tâche: décoder les chaînes Unicode.
Mise à jour
Je ne préconisais en aucun cas l'utilisation de
apply
, je pensais juste que puisque leNumPy
ne peut pas faire face à la situation ci-dessus, cela aurait pu être un bon candidat pourpandas apply
. Mais j'oubliais la simple compréhension de la liste grâce au rappel de @jpp.la source
[unidecode.unidecode(x) for x in s]
oulist(map(unidecode.unidecode, s))
?apply
, je pensais juste que cela aurait pu être un bon cas d'utilisation.