La méthode pandas apply devient utile dès qu’une transformation ne rentre plus proprement dans une opération vectorisée. Elle permet d’exécuter une fonction personnalisée sur chaque ligne ou chaque colonne d’un DataFrame, ce qui est très pratique quand la logique métier dépend de plusieurs champs à la fois. Je vais montrer comment la paramétrer correctement, comment lire ce que reçoit la fonction, quand renvoyer une seule valeur ou plusieurs, et surtout dans quels cas je préfère une autre approche.
Les points essentiels à garder en tête
- `apply` sert surtout pour une logique personnalisée sur des lignes ou des colonnes, pas pour du calcul simple.
- `axis=0` travaille colonne par colonne, tandis que `axis=1` travaille ligne par ligne.
- `raw=True` transmet un tableau NumPy au lieu d’une `Series`, ce qui peut accélérer certaines fonctions numériques.
- Si une fonction renvoie plusieurs valeurs, il faut penser à la forme du résultat et, au besoin, à `result_type="expand"`.
- Pour une transformation élément par élément, `map` ou une expression vectorisée est souvent plus lisible et plus rapide.
- Sur de gros volumes, `apply` devient vite le mauvais réflexe si une alternative native existe.
Quand `apply` est le bon outil
Je réserve `apply` aux cas où une seule expression vectorisée ne suffit pas. Concrètement, cela couvre les règles métier qui combinent plusieurs colonnes, les conditions imbriquées, les calculs qui changent selon le contexte, ou les transformations dont la logique serait pénible à écrire en une seule ligne.
- catégoriser une ligne selon plusieurs champs
- extraire ou reformater une valeur à partir de plusieurs colonnes
- produire un score, un libellé, ou plusieurs indicateurs à partir d’une règle maison
En revanche, si je peux écrire la même chose avec une opération sur colonnes, je le fais presque toujours. La suite logique, c’est donc de comprendre précisément ce que Pandas envoie à la fonction selon l’axe choisi.

Comprendre ce que la fonction reçoit selon l’axe
Le point qui piège le plus souvent, c’est l’axe. Avec `axis=0`, Pandas applique la fonction à chaque colonne ; avec `axis=1`, il passe ligne par ligne. Dans les deux cas, l’objet reçu est en général une `Series`, sauf si l’on active `raw=True`, auquel cas on reçoit un tableau NumPy plus brut.
| Paramètre | Ce que cela change | Quand je l’utilise |
|---|---|---|
| `axis=0` | La fonction reçoit chaque colonne | Réductions, statistiques, nettoyage colonne par colonne |
| `axis=1` | La fonction reçoit chaque ligne | Règles métier qui combinent plusieurs champs |
| `raw=True` | La fonction reçoit un `ndarray` au lieu d’une `Series` | Calculs numériques simples où les labels ne servent pas |
| `result_type="expand"` | Les retours de type liste deviennent des colonnes | Sorties multiples avec `axis=1` |
| `result_type="broadcast"` | Le résultat garde la forme d’origine | Quand chaque ligne doit produire une structure de même taille |
| `by_row="compat"` | Pandas essaie de traduire certaines fonctions vers ses méthodes natives | Quand on passe une liste ou un dictionnaire de fonctions |
Une fois ce cadrage clair, l’écriture des fonctions devient beaucoup plus simple, y compris quand on veut produire une colonne entière à partir d’une règle plus riche.
Appliquer une logique aux lignes sans se tromper
Quand je traite les lignes, je nomme systématiquement le paramètre `row` pour ne pas me tromper sur le sens de lecture. C’est surtout utile quand la règle dépend de deux ou trois colonnes, par exemple pour calculer un prix net, attribuer un segment client ou valider un statut.
import pandas as pd
df = pd.DataFrame({
"produit": ["A", "B", "C"],
"prix_ht": [120, 80, 200],
"remise_pct": [10, 0, 15],
})
def prix_net(row):
remise = row["prix_ht"] * row["remise_pct"] / 100
return row["prix_ht"] - remise
df["prix_net"] = df.apply(prix_net, axis=1)Ici, `apply` est pertinent parce que le calcul combine plusieurs colonnes et une petite règle métier. Si je devais faire exactement la même chose sur un million de lignes avec une formule simple, je chercherais d’abord une version vectorisée.
Retourner plusieurs valeurs sans bricoler
Une autre force de cette méthode, c’est qu’elle peut renvoyer plus qu’un simple scalaire. Si la fonction produit plusieurs indicateurs, je peux retourner une `Series` et laisser Pandas créer plusieurs colonnes d’un coup, ce qui garde le code lisible et évite des passages répétés sur le même DataFrame.
df = pd.DataFrame({
"produit": ["A", "B", "C"],
"prix_ht": [120, 80, 200],
"remise_pct": [10, 0, 15],
"cout": [70, 55, 140],
})
def indicateurs(row):
ca_net = row["prix_ht"] * (1 - row["remise_pct"] / 100)
marge = ca_net - row["cout"]
return pd.Series({
"ca_net": ca_net,
"marge": marge,
})
df[["ca_net", "marge"]] = df.apply(indicateurs, axis=1)Quand la fonction renvoie une liste ou un tuple, `result_type="expand"` devient utile si l’on veut transformer ces valeurs en colonnes. En pratique, j’utilise cette option quand la sortie a une forme stable et que je veux éviter du post-traitement manuel.
Ce que j’utilise à la place quand c’est plus simple
Dans beaucoup de cas, `apply` n’est pas l’outil le plus direct. Je compare rapidement l’intention de départ avec le bon niveau d’abstraction, et je choisis la méthode la plus lisible avant même de penser à la performance.
| Situation | Méthode à privilégier | Pourquoi |
|---|---|---|
| Une opération mathématique ou logique sur une colonne entière | Expression vectorisée / NumPy | Plus lisible, plus rapide, souvent une seule ligne |
| Une transformation cellule par cellule sur une `Series` | `Series.map` | Plus explicite qu’un `apply` si l’entrée et la sortie sont scalaires |
| Une transformation cellule par cellule sur tout le DataFrame | `DataFrame.map` | La méthode élément par élément remplace l’ancien usage de `applymap` |
| Une agrégation | `agg` | Conçu pour réduire les données, pas pour parcourir ligne par ligne |
| Une transformation qui doit garder la même forme | `transform` | Utile quand chaque valeur doit être remplacée sans changer la structure |
| Une logique métier qui dépend de plusieurs colonnes | `apply(axis=1)` | Flexible, mais à réserver aux cas où les alternatives sont moins claires |
Je garde aussi en tête que `DataFrame.applymap` n’est plus le bon nom à retenir : la méthode élément par élément s’appelle maintenant `DataFrame.map`. Ce détail compte, parce qu’il évite des habitudes héritées d’anciennes versions et force à choisir l’outil qui correspond vraiment à la tâche.
Les erreurs qui coûtent le plus de temps
La plupart des problèmes que je vois autour de `apply` ne viennent pas de la syntaxe, mais d’un mauvais cadrage du besoin. Les erreurs suivantes reviennent souvent, et elles peuvent faire perdre beaucoup de temps sur un pipeline pourtant simple.
- Oublier `axis=1` quand on veut traiter des lignes, puis se retrouver avec une logique appliquée colonne par colonne.
- Muter l’objet reçu dans la fonction : Pandas ne garantit pas un comportement fiable si la fonction modifie la `Series` passée en argument.
- Retourner des types incohérents selon les lignes, ce qui conduit à des colonnes en `object` difficiles à exploiter ensuite.
- Utiliser `apply` pour une formule triviale alors qu’une expression vectorisée ferait le travail plus vite et plus clairement.
- Passer `raw=True` sans adapter la fonction : si la fonction attend des noms de colonnes, elle cassera, car elle reçoit alors un tableau brut.
- Appeler une API ou une base de données ligne par ligne : dans ce cas, le problème n’est plus Pandas mais l’architecture du traitement.
La documentation officielle de Pandas montre un exemple parlant : sur une petite comparaison, une version vectorisée prend 0,0043 seconde contre 5,6435 secondes pour une UDF avec `apply`. Le chiffre n’est pas universel, mais le message est constant : dès qu’une opération peut être poussée dans NumPy, elle gagne presque toujours. Pour certaines fonctions numériques stables, `engine="numba"` peut aider, mais seulement si le code reste compatible et les types ne changent pas en cours de route.
Une fois ces pièges écartés, la méthode redevient un outil utile plutôt qu’un réflexe par défaut.
Le réflexe que j’applique avant de garder `apply`
Avant de valider une transformation, je me pose trois questions simples : est-ce que je peux écrire la même logique en vectorisé, est-ce qu’une version `map`, `agg` ou `transform` ferait le travail, et ai-je vraiment besoin d’une fonction personnalisée ligne par ligne ? Si la réponse à la première question est oui, je n’hésite pas longtemps.
Quand la réponse est non, `apply` reste un bon outil pour clarifier une règle métier ou construire plusieurs colonnes à partir d’une même logique. Je l’utilise alors avec une fonction courte, un axe explicite, et une vérification rapide sur un petit échantillon avant de l’envoyer sur de gros volumes.