Cours
De nombreuses personnes se tournent vers .apply() pour « éviter les boucles » et s'attendent à ce que ce soit rapide et simple. En pratique, l'.apply(axis=1) e ligne par ligne reste une boucle au niveau Python. Il peut être lent avec des données volumineuses et renvoie parfois des formes inattendues. La solution est simple : utilisez les opérations vectorisées pandas/NumPy pour les tâches courantes et réservez l' .apply() e pour la logique qui nécessite réellement plusieurs colonnes.
Ce guide explique comment .apply() fonctionne aujourd'hui, met en évidence les pièges courants et fournit des modèles prêts à l'emploi plus rapides et plus clairs.
Qu'est-ce que DataFrame.apply() et Series.apply() ?
DataFrame.apply(func, axis=0) appelle par défautfunc sur chaque colonne. Avec` axis=1`, il appelle ` func ` sur chaque ligne. `Series.apply(func) ` appelle `func ` sur chaque élément (ou sur l'ensemble ` Series`, selon les signatures dans les versions récentes).
- Par ligne ou par colonne :
axis=1signifie « par ligne ».axis=0(par défaut) signifie « par colonne ».
- Règles de forme de retour (pandas ≥0,23) : Si votre fonction renvoie un scalaire par ligne/colonne, vous obtenez une erreur de type «
Series». Si elle renvoie unSeriesou undict, pandas développe ces clés en unDataFrame. Si la fonction renvoie une liste/un tableau, vous obtiendrez une série de listes, à moins que vous ne définissiezresult_type='expand'.
- Erreurs plus strictes (pandas ≥2.0) : Les longueurs de listes incompatibles génèrent désormais des erreurs au lieu de produire silencieusement des résultats incohérents.
Premier choix recommandé : Opérations vectorisées
La plupart des tâches impliquant la combinaison ou la transformation de colonnes sont plus rapides et plus claires avec des expressions vectorisées ou des méthodes intégrées. L'exemple suivant calcule la différence de points d'une équipe de baseball sans utiliser d' .apply().
import pandas as pd
team_stats = pd.DataFrame({
"Team": ["ARI", "ATL", "BAL"],
"League": ["NL", "NL", "AL"],
"Year": [2012, 2012, 2012],
"RunsScored": [734, 700, 712],
"RunsAllowed": [688, 600, 705],
"Wins": [81, 94, 93],
"Games": [162, 162, 162],
"Playoffs": [0, 1, 1],
})
# Vectorized: fast and idiomatic
team_stats["RunDiff"] = team_stats["RunsScored"] - team_stats["RunsAllowed"]
print(team_stats[["Team", "RunsScored", "RunsAllowed", "RunDiff"]])
Utilisation de .apply() ligne par ligne lorsque cela est nécessaire
Utilisez cette méthode lorsque votre logique s'étend réellement sur plusieurs .apply(axis=1) lorsque votre logique s'étend réellement sur plusieurs colonnes et n'est pas facilement vectorisable (par exemple, des règles conditionnelles qui dépendent de plusieurs champs).
calculer une valeur dérivée à partir de plusieurs colonnes
Ce modèle calcule une valeur par ligne à l'aide de plusieurs entrées. L'approche vectorisée ci-dessus reste préférable, mais celle-ci montre l'utilisation correcte ligne par ligne et les options qui affectent la vitesse et la sortie.
def compute_run_diff(row):
# Treat the row as read-only; return a scalar
return row["RunsScored"] - row["RunsAllowed"]
# Row-wise apply (Python-level loop; can be slow on large data)
team_stats["RunDiff_apply"] = team_stats.apply(compute_run_diff, axis=1)
Lorsque la fonction est numérique et ne nécessite que des tableaux bruts, le fait de passer raw=True permet d'éviter une partie de la surcharge de pandas en fournissant un tableau NumPy à votre fonction.
import numpy as np
def compute_run_diff_raw(values):
# values is a NumPy array when raw=True
# Order matches the column order we select
rs, ra = values
return rs - ra
team_stats["RunDiff_raw"] = team_stats[["RunsScored", "RunsAllowed"]].apply(
compute_run_diff_raw, axis=1, raw=True
)
Veuillez vérifier une fois la saisie de la fonction.
Lorsque je connecte une nouvelle fonction de ligne, j'imprime une ligne une fois pour vérifier ce qui est transmis.
def debug_row(row):
# Print the first row only
if row.name == team_stats.index[0]:
print("Example row:", row.to_dict())
return 0
_ = team_stats.apply(debug_row, axis=1)
Étude de cas : Sommes, totaux saisonniers et indicateurs textuels
Compte tenu des statistiques annuelles de Rays, privilégiez les méthodes intégrées et les outils élémentaires lorsque cela est possible. .apply() dans la mesure du possible.
rays_by_year = pd.DataFrame(
{
"Year": [2012, 2011, 2010, 2009, 2008],
"RunsScored": [697, 707, 802, 803, 774],
"RunsAllowed": [577, 614, 649, 754, 671],
"Wins": [90, 91, 96, 84, 97],
"Playoffs": [0, 1, 1, 0, 1],
}
).set_index("Year")
obtenir efficacement les sommes des colonnes
Veuillez utiliser DataFrame.sum() au lieu de .apply(sum). C'est plus rapide et plus clair.
# Preferred
totals = rays_by_year.sum(axis=0)
print(totals)
# If you must use apply (not recommended here)
totals_apply = rays_by_year.apply(sum, axis=0)
calculer le nombre total de points marqués au cours d'une saison
Utilisez l'arithmétique vectorisée sur les colonnes ; aucune .apply() nécessaire.
rays_by_year["TotalRuns"] = rays_by_year["RunsScored"] + rays_by_year["RunsAllowed"]
print(rays_by_year[["RunsScored", "RunsAllowed", "TotalRuns"]].head())
convertir un indicateur 0/1 en texte
Veuillez privilégier Series.map() ou replace() pour les transformations élémentaires sur une colonne.
# Using map with a dict
rays_by_year["PlayoffsText"] = rays_by_year["Playoffs"].map({0: "No", 1: "Yes"})
# Equivalent with replace
rays_by_year["PlayoffsText2"] = rays_by_year["Playoffs"].replace({0: "No", 1: "Yes"})
Contrôler la forme de sortie à partir de .apply()
Lorsque votre fonction renvoie plusieurs valeurs par ligne, veuillez indiquer explicitement la forme. Cela permet d'éviter les surprises et les problèmes de compatibilité avec les versions futures.
renvoyer une liste et la développer en colonnes
Utilisez result_type='expand' pour diviser une liste/un tableau en plusieurs colonnes.
def wins_losses(row):
# Derive wins and losses as two outputs
wins = row["Wins"]
losses = row["Games"] - row["Wins"] if "Games" in row else np.nan
return [wins, losses]
# Example using team_stats, which has "Wins" and "Games"
expanded = team_stats.apply(wins_losses, axis=1, result_type="expand")
expanded.columns = ["WinsOut", "LossesOut"]
team_stats = pd.concat([team_stats, expanded], axis=1)
Renvoyer une série ou un dictionnaire pour nommer automatiquement les colonnes
Retourner un Series ou dict de chaque ligne produit un DataFrame avec des étiquettes de colonnes correspondantes.
def summary_row(row):
return pd.Series(
{
"IsWinningSeason": row["Wins"] >= 90,
"RunRatio": (row["RunsScored"] / row["RunsAllowed"]),
}
)
summary = team_stats.apply(summary_row, axis=1)
team_stats = pd.concat([team_stats, summary], axis=1)
Conseils de performance pertinents
Le traitement ligne par ligne est pratique, mais peut être lent sur les grands cadres, car .apply(axis=1) est pratique mais peut être lent sur les grands cadres car il appelle votre fonction Python une fois par ligne. Ces modèles permettent d'éviter ce goulot d'étranglement.
- Veuillez privilégier les opérations vectorisées pandas/NumPy et les méthodes intégrées telles que
sum,mean,clip,where,astype, et les méthodes de chaîne/accesseur.
- Pour les opérations numériques sur les lignes/colonnes qui acceptent les tableaux, veuillez transmettre
raw=Truepour obtenir un tableau NumPy et réduire la surcharge pandas.
- Pour effectuer des opérations élémentaires sur une seule colonne, veuillez utiliser les méthodes
Series.map()ou des méthodes vectorisées à la place deDataFrame.apply(axis=1).
- Veuillez éviter de modifier la ligne/colonne fournie dans votre fonction ; veuillez plutôt renvoyer une nouvelle valeur.
Notes importantes concernant la version (pandas 2.x)
Les versions récentes de pandas ont renforcé le comportement autour de .apply() afin que les résultats soient plus prévisibles.
- Rétournements de type liste : Dans la version 2.0 et les versions ultérieures, les longueurs de liste incompatibles génèrent une erreur. Veuillez utiliser des longueurs cohérentes et définir l'
result_type='expand'lorsque vous souhaitez plusieurs colonnes.
- Augmentation de la production : Le renvoi d'un objet
Seriesoudictse développe en un DataFrame avec ces clés comme colonnes (stable depuis la version 0.23).
Series.apply()Modifications : L'argument d'convert_dtypeest obsolète. Si vous avez besoin de types mixtes, effectuez d'abord une conversion versobject(par exemple,s.astype("object").apply(fn)). Les nouvelles signatures permettent de contrôler sifnreçoit des scalaires ou une série dans davantage de contextes (par exemple,by_row).
.apply() par rapport à .map() par rapport à .applymap() par rapport à .agg()
Veuillez sélectionner l'API qui correspond à la forme de votre transformation.
Series.map(func_or_dict): Transformation élémentaire sur une colonne. Idéal pour les recherches ou les fonctions simples.
DataFrame.apply(func, axis=1): Logique par ligne nécessitant plusieurs colonnes.
DataFrame.apply(func, axis=0): Logique par colonne (chaque entrée est une série représentant une colonne).
DataFrame.applymap(func): Élément par élément sur chaque cellule. Veuillez l'utiliser avec modération ; les méthodes vectorisées sont plus rapides.
.agg()/.transform(): Agrégations et transformations par groupe ; privilégiez ces dernières dans les pipelines groupby.
Erreurs courantes et solutions rapides
Ce sont les problèmes qui entraînent le plus souvent des résultats incorrects ou des ralentissements, avec les moyens de les corriger.
- Oublier axis=1 pour la logique par ligne : Si votre fonction reçoit soudainement des colonnes au lieu de lignes, veuillez ajouter
axis=1.
- Série inattendue de listes : Lorsque vous renvoyez une liste/un tableau à partir de chaque ligne, définissez
result_type='expand'pour diviser en colonnes.
- Code lent ligne par ligne : Veuillez remplacer par des expressions vectorisées ou des fonctions intégrées ; si cela n'est pas possible, envisagez l'utilisation de l'
raw=Truee afin de réduire la charge.
- Modification de la ligne d'entrée : Traitez les entrées comme étant en lecture seule et renvoyez de nouvelles valeurs.
- Déviation du type de données : Si votre fonction renvoie parfois des valeurs non entières, les colonnes entières peuvent être converties en flottantes ou en objets. Veuillez ensuite couler si nécessaire.
astypesi nécessaire.
Conclusion
.apply() est un outil flexible, mais ce n'est pas un raccourci vers la performance. Utilisez des opérations vectorisées pour les transformations arithmétiques, d'agrégation et élémentaires sur une seule colonne. N'utilisez l'DataFrame.apply(axis=1) que lorsque votre logique nécessite réellement plusieurs colonnes par ligne. Lorsque vous l'utilisez, contrôlez la forme de la sortie avec result_type, envisagez raw=True pour les fonctions numériques et surveillez dtypes. Ces modèles produisent des résultats prévisibles sur les pandas modernes et s'adaptent mieux à mesure que vos données augmentent.
