Colonnes pandas : apply, map, assign, .str & category

Dérivez et nettoyez les colonnes que vous analysez vraiment — ajoutez-les avec assign, réassociez avec map, construisez des colonnes conditionnelles avec np.where, nettoyez le texte avec .str, et convertissez en category — et apprenez quand préférer une expression vectorisée à apply.

Dérivez et nettoyez des colonnes pandas comme l’analyse se fait vraiment : ajoutez des colonnes calculées avec assign, réassociez des valeurs avec map et replace, construisez des colonnes conditionnelles avec np.where et np.select vectorisés au lieu d’un apply lent, nettoyez le texte en masse avec l’accesseur .str, et donnez à une colonne à faible cardinalité le dtype category pour un gros gain de mémoire et un tri ordonné correct — plus quand recourir à apply (en dernier).

Date de publication

17 juillet 2026

Modifié

18 juillet 2026

AstucePoints clés
  • assign ajoute des colonnes calculées sans modifier en place. df.assign(mass_kg=df["body_mass_g"]/1000) renvoie une nouvelle frame, ce qui s’enchaîne proprement dans un pipeline — et sa forme lambda (lambda d: d["x"]/d["y"]) peut référencer des colonnes créées plus tôt dans le même appel.
  • map réassocie une Series élément par élément ; replace corrige des valeurs précises. species.map({"Adelie":"small", ...}) remplace chaque valeur via un dict ou une fonction. map ne travaille que sur une Series — pour exécuter une fonction sur chaque cellule d’une DataFrame entière, utilisez DataFrame.map (l’ancien applymap est déprécié).
  • Une colonne conditionnelle, c’est np.where / np.select, pas apply. np.where(mass>4000, "heavy", "light") est le if/else vectorisé ; np.select gère trois branches ou plus. Les deux s’exécutent en C sur toute la colonne — bien plus rapides et clairs qu’un apply ligne par ligne.
  • Ne recourez à apply qu’en dernier. apply(axis=1) exécute une fonction Python par ligne ; ici, lui et l’expression vectorisée donnent la même réponse (20.777), mais la version vectorielle est plus rapide et se lit mieux. N’utilisez apply que lorsqu’il n’existe vraiment aucun équivalent vectorisé, .str ou np.where.
  • .str nettoie le texte en masse ; astype("category") le compresse et l’ordonne. L’accesseur .str vectorise upper/strip/len/contains/replace/split. Convertir une colonne texte à faible cardinalité en category a réduit sa mémoire d’environ 25× ici (19,256 → 752 octets), et un Categorical ordonné fait suivre au tri, aux comparaisons et aux axes de tracé l’ordre que vous déclarez plutôt que l’ordre alphabétique.

Introduction

Vous n’analysez presque jamais les colonnes qu’on vous donne. La table brute a body_mass_g quand vous voulez des kilogrammes, une chaîne species quand vous voulez une classe de taille, une étiquette texte avec des espaces parasites et une casse incohérente, et une colonne object qui se trierait et se tracerait dans le mauvais ordre. Le vrai travail entre « CSV chargé » et « analyse lancée », c’est dériver et nettoyer des colonnes — et pandas a une trousse à outils réduite et affûtée pour exactement cela : assign, map, apply, np.where/np.select, l’accesseur .str et le dtype category.

Cette leçon parcourt cette trousse dans l’ordre où le travail se fait réellement : ajouter une colonne calculée avec assign, réassocier des valeurs avec map (et corriger des valeurs parasites avec replace), construire une colonne conditionnelle à la façon vectorisée avec np.where/np.select, nettoyer le texte en masse avec .str, et enfin donner à une colonne à faible cardinalité le dtype category pour un gros gain de mémoire et un tri ordonné correct. Au passage, nous réglons l’erreur de performance pandas la plus courante — recourir à apply là où une expression vectorisée fait le même travail plus vite.

Les données sont la table des manchots de Palmer (issue du jeu de données palmerpenguins) — 344 oiseaux avec quelques colonnes texte (species, island, sex) à nettoyer, des catégories à faible cardinalité à compresser et des mesures numériques sur lesquelles calculer. Elle est livrée sous forme d’un petit CSV à côté de cette leçon, donc rien à télécharger et aucun réseau au rendu.

C’est la couche de transformation qui vient au-dessus des DataFrames pandas, qui couvre le chargement, la sélection, le filtrage, groupby et fillna — on s’appuie dessus sans les réexpliquer. C’est aussi l’étape naturelle après les leçons sœurs Combiner : merge & join et Remodeler : pivot & melt : on dérive et nettoie généralement les colonnes juste après une jointure ou un remodelage. Chaque résultat ci-dessous a été produit par le code montré, et les blocs se construisent dans l’ordre. Pour expérimenter, éditez la cellule Essayez en direct à la fin et appuyez sur Run.

La table des manchots

Chargez le fichier et regardez ce que vous avez — les dtypes vous disent quelle colonne appelle quel outil.

import pandas as pd

penguins = pd.read_csv("penguins.csv")

print("shape:", penguins.shape)
print(penguins.dtypes)
print(penguins.head(3))
shape: (344, 7)
species               object
island                object
bill_length_mm       float64
bill_depth_mm        float64
flipper_length_mm    float64
body_mass_g          float64
sex                   object
dtype: object
  species     island  bill_length_mm  bill_depth_mm  flipper_length_mm  \
0  Adelie  Torgersen            39.1           18.7              181.0   
1  Adelie  Torgersen            39.5           17.4              186.0   
2  Adelie  Torgersen            40.3           18.0              195.0   

   body_mass_g     sex  
0       3750.0    Male  
1       3800.0  Female  
2       3250.0  Female  

344 lignes, 7 colonnes. Trois colonnes sont object — le mot de pandas pour texte (species, island, sex) ; ce sont les candidates pour .str et category. Les quatre mesures sont des nombres float64 sur lesquels calculer. (Quelques mesures sont manquantes — body_mass_g a 2 NaN, sex en a 11 — ce qui comptera quand nous nettoierons le texte et construirons des colonnes conditionnelles.) Maintenant, dérivons et nettoyons, un outil à la fois.

assign : ajouter une colonne calculée

La transformation la plus courante consiste à ajouter une colonne dérivée de colonnes existantes — la masse en kilogrammes, un ratio, un indicateur. Vous pouvez écrire df["mass_kg"] = df["body_mass_g"] / 1000, mais cela modifie la frame en place. assign renvoie plutôt une nouvelle frame avec la colonne ajoutée, ce qui lui permet de s’enchaîner proprement dans un pipeline.

penguins = penguins.assign(mass_kg=penguins["body_mass_g"] / 1000)

print(penguins[["species", "body_mass_g", "mass_kg"]].head(3))
print("mean mass_kg:", round(penguins["mass_kg"].mean(), 3))
  species  body_mass_g  mass_kg
0  Adelie       3750.0     3.75
1  Adelie       3800.0     3.80
2  Adelie       3250.0     3.25
mean mass_kg: 4.202

mass_kg est maintenant une colonne, et la masse corporelle moyenne sur les 344 oiseaux est de 4.202 kg (la moyenne ignore les 2 masses manquantes). Deux choses font d’assign plus qu’une commodité :

  • Elle s’enchaîne. Comme elle renvoie une frame, vous pouvez écrire df.merge(...).assign(...).query(...) en un seul pipeline lisible au lieu d’une pile d’affectations en place df["x"] = ....
  • La forme lambda voit la frame en cours de construction. Passez une fonction et elle reçoit la frame courante, de sorte qu’une colonne ultérieure peut s’appuyer sur une précédente dans le même appel :
enriched = penguins.assign(
    mass_kg=lambda d: d["body_mass_g"] / 1000,
    bill_ratio=lambda d: d["bill_length_mm"] / d["bill_depth_mm"],
)
print(enriched[["mass_kg", "bill_ratio"]].head(3).round(3))
   mass_kg  bill_ratio
0     3.75       2.091
1     3.80       2.270
2     3.25       2.239

Les deux nouvelles colonnes se calculent de façon vectorisée sur toute la frame d’un coup — le bill_ratio dérivé (longueur du bec ÷ profondeur) tourne autour de 2.1–2.3 pour ces premiers oiseaux. Pas de boucle, pas d’apply ; juste de l’arithmétique de colonnes.

map : réassocier des valeurs élément par élément

Pour traduire chaque valeur d’une colonne en une autre valeur — species → une classe de taille grossière, un code → une étiquette — utilisez map sur la Series avec un dict (ou une fonction). Chaque valeur est recherchée puis remplacée.

size_class = penguins["species"].map(
    {"Adelie": "small", "Chinstrap": "small", "Gentoo": "large"}
)
print(size_class.value_counts())
species
small    220
large    124
Name: count, dtype: int64

Chaque chaîne d’espèce est devenue sa classe de taille : 220 small (Adelie + Chinstrap) et 124 large (Gentoo). map est l’outil quand vous avez une table claire de → vers. Un proche cousin est replace, qui n’échange que les valeurs que vous nommez et laisse le reste intact — mieux pour corriger quelques mauvaises entrées que pour une réassociation complète :

# replace: fix specific values, keep everything else as-is
tidy_island = penguins["island"].replace({"Torgersen": "Torgersen Island"})
print(tidy_island.value_counts())
island
Biscoe              168
Dream               124
Torgersen Island     52
Name: count, dtype: int64

replace n’a renommé que Torgersen ; Biscoe et Dream sont passés inchangés. Une réserve : map est une méthode de Series — elle travaille sur une colonne à la fois. Pour appliquer une fonction à chaque cellule d’une DataFrame entière, utilisez DataFrame.map (à noter : l’ancien DataFrame.applymap est déprécié et émet désormais un avertissement) :

# DataFrame.map runs a function element-wise across a whole frame
nums = penguins[["bill_length_mm", "flipper_length_mm"]].head(3)
print(nums.map(lambda x: round(x)))
   bill_length_mm  flipper_length_mm
0              39                181
1              40                186
2              40                195

Chaque cellule numérique a été arrondie indépendamment. Pour une seule colonne, préférez Series.map ; ne recourez à DataFrame.map que lorsque vous avez vraiment besoin de la même fonction élément par élément sur plusieurs colonnes.

apply : une fonction ligne par ligne — à réserver en dernier

apply exécute une fonction Python sur une Series (par élément) ou, avec axis=1, sur une DataFrame par ligne — la fonction voit alors toute la ligne et peut combiner plusieurs colonnes. Cette souplesse est réelle, mais elle a un coût : apply(axis=1) appelle votre fonction Python une fois par ligne, ce qui est lent. La leçon à retenir tôt, c’est que la plupart des cas où l’on écrit apply ont un équivalent vectorisé à la fois plus rapide et plus clair.

Voici le même calcul — un ratio masse/nageoire — fait des deux façons :

# per-row Python function
ratio_apply = penguins.apply(
    lambda row: row["body_mass_g"] / row["flipper_length_mm"], axis=1
)

# the SAME result, vectorized — plain column arithmetic, no apply
ratio_vec = penguins["body_mass_g"] / penguins["flipper_length_mm"]

print("apply(axis=1) mean:", round(ratio_apply.mean(), 3))
print("vectorized   mean:", round(ratio_vec.mean(), 3))
apply(axis=1) mean: 20.777
vectorized   mean: 20.777

Les deux donnent 20.777 — des réponses identiques. Mais ratio_vec exécute la division en C sur toute la colonne d’un coup, tandis que ratio_apply parcourt les 344 lignes en Python. Sur un jeu de données réel (des centaines de milliers de lignes), cette différence, ce sont des secondes contre un instant. Ne recourez à apply qu’en dernier — seulement quand il n’existe vraiment aucune expression vectorisée, .str ou np.where pour ce dont vous avez besoin (par exemple appeler une fonction externe qui attend des scalaires). Si vous pouvez écrire la transformation en arithmétique de colonnes, en appel .str ou en np.where, faites-le à la place.

np.where et np.select : la colonne conditionnelle vectorisée

« Ajouter une colonne dont la valeur dépend d’une condition » est l’une des questions pandas les plus posées — et la réponse idiomatique n’est pas un apply ligne par ligne, c’est np.where : un if/else vectorisé sur toute la colonne.

import numpy as np

penguins["size"] = np.where(penguins["body_mass_g"] > 4000, "heavy", "light")
print(penguins["size"].value_counts())
size
light    172
heavy    172
Name: count, dtype: int64

np.where(condition, value_if_true, value_if_false) a donné 172 heavy et 172 light — une répartition nette à 4 kg. (Les 2 masses manquantes atterrissent dans light : NaN > 4000 vaut False, elles prennent donc la branche else — bon à retenir quand un seuil rencontre des données manquantes.) Pour trois branches ou plus, empiler des appels np.where devient vite illisible ; utilisez np.select avec une liste de conditions et de choix correspondants, plus un default :

conditions = [
    penguins["body_mass_g"] < 3500,
    penguins["body_mass_g"] < 4500,
]
choices = ["light", "medium"]
penguins["size3"] = np.select(conditions, choices, default="heavy")
print(penguins["size3"].value_counts())
size3
medium    153
heavy     120
light      71
Name: count, dtype: int64

np.select vérifie les conditions dans l’ordre et prend la première qui correspond — ainsi un oiseau sous 3,500 g est light, un entre 3,500 et 4,500 est medium, et tout le reste (y compris les 2 masses NaN, qui ne correspondent à aucune condition) tombe dans le default heavy : 153 medium, 120 heavy, 71 light. C’est le motif qui passe à l’échelle pour une colonne en bandes/tranches — bien plus clair qu’un nid d’apply et de if/elif.

L’accesseur .str : nettoyer le texte en masse

Les colonnes texte arrivent en désordre — espaces parasites, casse incohérente, valeurs à tester ou à scinder. L’accesseur .str vectorise les méthodes de chaînes de Python sur toute une colonne : upper/lower, strip, len, contains, replace, split, et davantage. D’abord le geste de nettoyage quotidien — supprimer les espaces superflus et normaliser la casse sur une étiquette volontairement en désordre :

messy = pd.Series(["  Adelie ", "GENTOO", "chinstrap "])
print(messy.str.strip().str.title())
0       Adelie
1       Gentoo
2    Chinstrap
dtype: object

.str.strip() a supprimé les espaces autour et .str.title() a normalisé la casse — enchaînés, tous deux vectorisés, sans boucle. Sur la vraie colonne, .str répond directement à des questions courantes :

print("uppercased islands:", penguins["island"].str.upper().unique().tolist())
print("species name lengths (first 3):", penguins["species"].str.len().head(3).tolist())
print("rows whose species contains 'oo':", int(penguins["species"].str.contains("oo").sum()))
uppercased islands: ['TORGERSEN', 'BISCOE', 'DREAM']
species name lengths (first 3): [6, 6, 6]
rows whose species contains 'oo': 124

.str.upper() crie les trois noms d’îles (TORGERSEN, BISCOE, DREAM) ; .str.len() indique le nombre de caractères par valeur (Adelie → 6) ; et .str.contains("oo") renvoie une Series booléenne que l’on somme pour compter les correspondances — 124 lignes, exactement les manchots Gentoo (la seule espèce avec oo). Un piège à noter dès maintenant (et à revoir dans Problèmes fréquents) : .str sur une valeur non-chaîne ou manquante donne NaN, pas une erreur — un NaN dans sex reste donc NaN à travers .str.upper(). Passez na=False à .str.contains(..., na=False) quand vous voulez que les lignes manquantes soient traitées comme « pas de correspondance ».

astype("category") : mémoire et ordre

Une colonne comme species n’a que trois valeurs distinctes répétées 344 fois — stocker 344 chaînes séparées est du gaspillage. Le dtype category stocke chaque étiquette une seule fois et garde un code entier compact par ligne. Convertissez avec astype("category") et mesurez la différence :

species_obj = penguins["species"]
species_cat = penguins["species"].astype("category")

print("object dtype bytes: ", species_obj.memory_usage(deep=True))
print("category dtype bytes:", species_cat.memory_usage(deep=True))
object dtype bytes:  19256
category dtype bytes: 752

La même colonne est passée de 19,256 octets à 752 — environ 25× plus petit — sans aucun changement de ses valeurs. Sur une colonne à faible cardinalité avec beaucoup de lignes, cette économie est grande et gratuite. Mais le plus grand gain est souvent l’ordre. Un object ordinaire (ou une catégorie non ordonnée) se trie et se trace alphabétiquement ; un Categorical ordonné suit l’ordre que vous déclarez, de sorte que les comparaisons (<, >), sort_values et les axes de tracé respectent tous une séquence porteuse de sens. Notre bande size3 est le cas parfait : son ordre porteur de sens est light < medium < heavy, mais alphabétiquement ces étiquettes se trient heavy, light, medium — un fouillis. Déclarez l’ordre que vous entendez :

ordered = pd.Categorical(
    penguins["size3"],
    categories=["light", "medium", "heavy"],  # the order that MEANS something
    ordered=True,
)
print("ordered:", ordered.ordered)
print("declared order:", ordered.categories.tolist())
print("min (first category):", ordered.min())
print("alphabetical order would be:", sorted(penguins["size3"].unique()))
ordered: True
declared order: ['light', 'medium', 'heavy']
min (first category): light
alphabetical order would be: ['heavy', 'light', 'medium']

ordered=True rend les catégories comparables, donc .min() renvoie la première catégorie déclarée, light — pas la plus petite alphabétiquement, qui serait heavy. C’est le signe révélateur : light et heavy divergent, donc cet ordre porte réellement une information qu’une simple colonne texte ne peut pas porter. Déclarez ["heavy", "medium", "light"] à la place et .min() bascule sur heavy. Ce contrôle est ce qui règle le classique problème « mes barres sont dans le mauvais ordre » : fixez l’ordre de la catégorie une fois et chaque tri et chaque tracé lui obéit.

Voici cette catégorie ordonnée qui pilote une figure. Nous convertissons size3 au type ordonné, puis le regroupons avec groupby — comme la catégorie est ordonnée, les groupes (et les barres) sortent light → medium → heavy. Un groupby non ordonné sur la même colonne les trierait alphabétiquement en heavy, light, medium — le fouillis dénué de sens que le type ordonné empêche :

import matplotlib.pyplot as plt

penguins["size3"] = pd.Categorical(
    penguins["size3"],
    categories=["light", "medium", "heavy"],
    ordered=True,
)
mean_flipper = penguins.groupby("size3", observed=True)["flipper_length_mm"].mean()

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.bar(mean_flipper.index.astype(str), mean_flipper.values, color="#3a86d4", width=0.6)

for i, v in enumerate(mean_flipper.values):
    ax.text(i, v + 1.5, f"{v:,.1f}", ha="center", va="bottom", fontsize=9)

ax.set_xlabel("Body-mass band (ordered category)")
ax.set_ylabel("Mean flipper length (mm)")
ax.set_title("Mean flipper length by body-mass band")
ax.set_ylim(0, 240)
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()
Bar chart of mean penguin flipper length in millimetres across three body-mass bands, drawn in azure with the value printed above each bar. The bars follow the declared ordered-category sequence light, medium, heavy — which differs from the alphabetical heavy, light, medium — rising steadily from about 188 mm for light birds to 195 for medium and 216 for heavy, so heavier penguins clearly have longer flippers.
Figure 1

L’axe des x se lit light → medium → heavy parce que c’est l’ordre que nous avons déclaré — et il n’est visiblement pas alphabétique (ce qui mettrait heavy en premier). Les barres montent régulièrement, de 188.0 mm pour les oiseaux légers à 195.1 pour les moyens et 216.3 pour les lourds : les manchots plus lourds ont des nageoires plus longues. Inversez la liste categories= et les barres s’inversent ; c’est tout l’intérêt d’une catégorie ordonnée — vous décidez la séquence, et le reste de pandas l’honore.

Quel outil pour quelle transformation ?

Choisissez l’outil selon la forme de la transformation, pas par habitude :

La transformation dont vous avez besoin Recourez à Exemple
Ajouter une colonne calculée assign df.assign(kg=df["g"]/1000)
Réassocier des valeurs connues map (ou replace pour en corriger quelques-unes) s.map({"A":"small", ...})
Condition → deux issues np.where np.where(m>4000, "heavy", "light")
Condition → trois issues ou plus np.select np.select([c1, c2], [v1, v2], default=...)
Nettoyer / tester / scinder du texte .str s.str.strip().str.upper()
Étiquettes compactes + ordonnées astype("category") pd.Categorical(s, categories=[...], ordered=True)
Une vraie fonction Python ligne par ligne apply(axis=1) (dernier recours) df.apply(f, axis=1)

Règle générale : si la transformation peut s’écrire en arithmétique de colonnes, en appel .str, en map ou en np.where/np.select, écrivez-la ainsi — vectorisée, rapide, lisible. Réservez apply au cas rare qui n’a aucune forme vectorisée.

Essayez en direct

Les blocs ci-dessus se sont exécutés au moment du build. Éditez celui-ci et appuyez sur Run pour dériver et convertir des colonnes vous-même — il s’exécute dans votre navigateur via Pyodide (aucune installation). Le premier Run télécharge pandas une fois (~20 Mo), ensuite c’est mis en cache et instantané.

🟢 Avec un agent IA

Vous fixez une colonne à dériver ou à nettoyer ? Collez un df.head() et demandez à Prova « ajoute une classe de taille à partir de la masse corporelle, nettoie cette colonne texte, et fais de species une catégorie ordonnée » — ou « pourquoi mon apply(axis=1) est-il si lent, et quelle est la version vectorisée ? » Elle écrit les appels assign, np.where/np.select, .str et pd.Categorical(..., ordered=True), et — surtout — repère l’apply que vous pouvez remplacer par de la simple arithmétique de colonnes. Parce que le runtime est juste là, vous l’exécutez et lisez vous-même la colonne dérivée, au lieu de faire confiance à une transformation que vous ne voyez pas. The runtime is the judge. Demander à Prova →

La même idée en R

Vous travaillez en R, ou dans les deux langages ? Chaque geste ici a son jumeau tidyverse. assign, c’est le mutate() de dplyr ; np.where, c’est dplyr::if_else() et np.select, c’est case_when() ; map/replace correspondent à dplyr::recode() / case_match() ; l’accesseur .str, c’est stringr (str_to_upper(), str_trim(), str_detect(), str_length()) ; et un Categorical ordonné, c’est le factor de R avec un ordre de niveaux fixé — géré au mieux avec forcats (fct_relevel, fct_reorder). Les concepts — dériver sans modifier en place, préférer une conditionnelle vectorisée à une boucle sur les lignes, nettoyer le texte en masse et contrôler l’ordre des catégories — se transposent directement. Voir Ajouter une colonne en R et la série plus large Manipulation de données en R pour le côté dplyr.

Problèmes fréquents

Un apply(axis=1) lent là où un vecteur suffirait. apply(axis=1) exécute votre fonction Python une fois par ligne — correct pour quelques centaines de lignes, atrocement lent pour des centaines de milliers. Avant de l’écrire, demandez-vous si la transformation est de la simple arithmétique de colonnes (df["a"] / df["b"]), une conditionnelle (np.where / np.select) ou une opération texte (.str). Si c’est le cas, utilisez la forme vectorisée : même résultat, bien plus rapide. Réservez apply à une logique vraiment ligne par ligne sans équivalent vectorisé.

applymap déclenche un FutureWarning. DataFrame.applymap est déprécié — le remplaçant est DataFrame.map, au comportement identique (df.map(func) exécute func sur chaque cellule). Pour une seule colonne, n’utilisez ni l’un ni l’autre : Series.map est l’outil mono-colonne (df["col"].map(func)). Retenez la nomenclature : Series.map réassocie une colonne ; DataFrame.map s’exécute élément par élément sur une frame entière.

.str sur une valeur non-chaîne ou manquante renvoie NaN, silencieusement. L’accesseur .str n’échoue pas sur NaN ou sur des nombres — il propage NaN, donc un sex manquant reste NaN à travers .str.upper(), et .str.contains(...) renvoie NaN (pas False) pour ces lignes. Ce NaN casse ensuite un masque booléen. Corrigez-le en indiquant à la méthode comment traiter les valeurs manquantes (s.str.contains("x", na=False)) ou en nettoyant d’abord (s.fillna("").str.contains("x") / s.astype("string")).

Une colonne catégorie se trie alphabétiquement au lieu de l’ordre voulu. Un simple astype("category") (ou une colonne object) n’a aucun ordre intrinsèque, donc sort_values et les axes de tracé retombent sur l’alphabétique. Pour contrôler l’ordre, vous devez construire une catégorie ordonnée : pd.Categorical(s, categories=[...dans votre ordre...], ordered=True) (ou s.cat.reorder_categories([...], ordered=True)). Seule une catégorie ordonnée prend en charge les comparaisons </> et .min()/.max() selon votre séquence.

Questions fréquentes

map est une méthode de Series qui substitue chaque valeur via un dict ou une fonction à un argument — idéale pour une réassociation directe valeur-à-valeur (species.map({"Adelie":"small", ...})). apply est plus général : sur une Series, il exécute une fonction par élément, et sur une DataFrame avec axis=1, il exécute une fonction par ligne (en voyant toutes les colonnes à la fois). Utilisez map pour une simple recherche valeur par valeur sur une colonne ; utilisez apply quand la fonction a besoin de plusieurs colonnes ou d’une logique sur mesure — mais vérifiez d’abord si une expression vectorisée (np.where, arithmétique de colonnes, .str) fait le travail plus vite. Pour une fonction élément par élément sur une DataFrame entière, utilisez DataFrame.map (pas le déprécié applymap).

Utilisez le np.where vectorisé pour un choix à deux issues : df["size"] = np.where(df["body_mass_g"] > 4000, "heavy", "light"). Pour trois bandes ou plus, utilisez np.select avec une liste de conditions et de choix correspondants plus un default : np.select([m<3500, m<4500], ["light","medium"], default="heavy") — il prend la première condition qui correspond. Les deux sont bien plus rapides et clairs qu’un apply ligne par ligne avec if/elif. Attention au comportement de NaN : NaN > 4000 vaut False, donc les valeurs manquantes prennent la branche else (np.where) ou le default (np.select).

Sur une Series, s.apply(lambda x: ...) exécute la lambda par élément ; sur une DataFrame, df.apply(lambda row: ..., axis=1) l’exécute par ligne, où row["col"] lit chaque colonne. C’est souple mais lent, car la lambda s’exécute en Python une fois par ligne. Avant d’y recourir, cherchez une alternative vectorisée : df["a"] / df["b"] pour l’arithmétique, np.where(...) pour une conditionnelle, df["c"].str.... pour le texte. Dans cette leçon, le ratio apply(axis=1) et le simple df["a"]/df["b"] vectorisé ont donné la réponse identique (20.777) — préférez donc le vecteur et gardez apply pour une logique qui n’a vraiment aucune forme vectorisée.

Utilisez le contains de l’accesseur .str : df["species"].str.contains("oo") renvoie une Series booléenne (True là où la valeur contient "oo"), que vous pouvez .sum() pour compter les correspondances ou utiliser comme masque de filtre (df[df["species"].str.contains("oo")]). Par défaut, il traite le motif comme une regex, donc échappez les caractères spéciaux ou passez regex=False pour une correspondance littérale, et ajoutez case=False pour une correspondance insensible à la casse. Comme .str.contains renvoie NaN pour les valeurs manquantes, passez na=False pour que ces lignes comptent comme « pas de correspondance » au lieu de casser votre masque.

Deux raisons. Mémoire : une colonne texte à faible cardinalité (peu de valeurs distinctes, beaucoup de lignes) stocke chaque étiquette une fois plus un code entier compact, ce qui a ici réduit species de 19,256 à 752 octets (~25×). Ordre : un Categorical ordonné (pd.Categorical(s, categories=[...], ordered=True)) fait suivre au tri, aux comparaisons </> et aux axes de tracé la séquence que vous déclarez au lieu de l’alphabétique — le correctif propre pour « mes barres sont dans le mauvais ordre ». Certaines bibliothèques (et groupby) tournent aussi plus vite sur des catégories. Passez votre chemin pour les colonnes à forte cardinalité (par ex. texte libre ou identifiants), où le surcoût de la catégorie l’emporte sur l’économie.

Testez vos connaissances

Chargez les manchots et dérivez deux colonnes, toutes deux vectorisées (pas d’apply) :

Tâche 1. Ajoutez une colonne flipper_class avec trois bandes issues de flipper_length_mm : "short" pour moins de 190 mm, "medium" pour 190–210 mm (c.-à-d. < 210), et "long" sinon. Affichez son value_counts().

Tâche 2. Convertissez island en catégorie ordonnée avec l’ordre ["Torgersen", "Biscoe", "Dream"], puis confirmez que .min() renvoie Torgersen.

Pour la tâche 1, utilisez np.select avec deux conditions dans l’ordre (flipper < 190, puis flipper < 210) et default="long"np.select prend la première correspondance, ordonnez donc les conditions de la plus stricte à la plus large. Pour la tâche 2, utilisez pd.Categorical(df["island"], categories=[...], ordered=True) et appelez .min() sur le résultat.

import pandas as pd
import numpy as np

penguins = pd.read_csv("penguins.csv")

# Task 1 — a 3-band column with np.select (vectorized, no apply)
flip = penguins["flipper_length_mm"]
conditions = [flip < 190, flip < 210]
choices = ["short", "medium"]
penguins["flipper_class"] = np.select(conditions, choices, default="long")
print(penguins["flipper_class"].value_counts())

# Task 2 — an ordered categorical
island_cat = pd.Categorical(
    penguins["island"],
    categories=["Torgersen", "Biscoe", "Dream"],
    ordered=True,
)
print("min island:", island_cat.min())

np.select vérifie flipper < 190 d’abord, puis flipper < 210, et envoie tout le reste (y compris les 2 lignes avec une longueur de nageoire manquante) vers default="long". Comme island_cat est ordonnée avec Torgersen en premier, .min() renvoie Torgersen — le plus petit selon l’ordre, pas selon l’alphabet (ce qui serait Biscoe).

Vérification rapide. Vous avez besoin d’une nouvelle colonne qui vaut "heavy" quand body_mass_g > 4000 et "light" sinon, sur une frame de centaines de milliers de lignes. Quel est le bon outil — df.apply(lambda r: "heavy" if r["body_mass_g"] > 4000 else "light", axis=1), ou np.where(df["body_mass_g"] > 4000, "heavy", "light") ?

np.where. Les deux produisent la colonne identique, mais np.where évalue la condition de façon vectorisée en C sur tout le tableau d’un coup, tandis qu’apply(axis=1) appelle une fonction Python par ligne — des ordres de grandeur plus lent à l’échelle. Une colonne conditionnelle à deux issues est exactement ce à quoi sert np.where ; gardez apply pour une logique sans équivalent vectorisé.

Conclusion

Vous pouvez désormais transformer une table brute en les colonnes que vous analysez réellement : ajouter des colonnes calculées avec assign (enchaînable, sans mutation en place), réassocier des valeurs avec map et corriger les valeurs parasites avec replace, construire des colonnes conditionnelles à la façon vectorisée avec np.where et np.select, nettoyer le texte en masse avec l’accesseur .str, et donner à une colonne à faible cardinalité le dtype category pour un gros gain de mémoire et — avec ordered=True — un tri et un ordre de tracé corrects. Et vous connaissez l’habitude qui sépare le pandas rapide du lent : préférez une expression vectorisée, et ne recourez à apply qu’en dernier. Dérivez délibérément, nettoyez en masse, et l’analyse qui suit travaille sur exactement les colonnes dont elle a besoin.

Cela termine la série Manipulation de données en Python — combiner, remodeler, dates et transformer. À partir d’ici, les mêmes données vont vers une figure ou un modèle.

Leçons connexes

Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Manipulation de données en Python — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Colonnes pandas : apply, map, assign, .str \& category},
  date = {2026-07-17},
  url = {https://www.datanovia.com/learn/programming/python-data-wrangling/transform-apply-strings-categoricals},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Colonnes pandas : apply, map, assign, .str & category.” 2026. July 17. https://www.datanovia.com/learn/programming/python-data-wrangling/transform-apply-strings-categoricals.