Comment remplacer les boucles pandas lentes en DataFrame ?

Je les remplace par des opérations vectorisées, np.where, np.select, map, str, apply quand il faut, et groupby.transform. Le vrai sujet, c’est de choisir le bon outil selon la transformation. C’est là que pandas devient rapide, lisible, et franchement moins pénible.

Pourquoi les boucles pandas ralentissent tout ?

Les boucles pandas ralentissent parce qu’elles traitent les lignes une par une et contournent une bonne partie des optimisations de NumPy et pandas.

Pandas est rapide quand on lui demande de travailler par colonnes. C’est son terrain naturel. Une colonne, c’est un bloc de données que NumPy peut traiter d’un coup, avec du code optimisé en C sous le capot. Une ligne, par contre, devient vite un petit objet Python manipulé isolément. Et Python est beaucoup moins bon quand il doit répéter 100 000 fois une mini opération toute simple.

Je vois souvent le piège avec iterrows. Ça a l’air pratique, presque évident. On lit chaque ligne, on calcule, on écrit le résultat. Le souci, c’est que iterrows renvoie une Series pandas, pas une vraie ligne brute optimisée. Une Series, c’est flexible, mais c’est coûteux. Et parfois, pandas convertit les types au passage. Un entier peut devenir un float, une date peut perdre son format attendu. Rien de dramatique sur 10 lignes. Très pénible sur un vrai fichier.

Pour garder un exemple concret, je vais partir d’un DataFrame e-commerce de 100 000 lignes. Ce n’est pas énorme. Justement. Si une boucle rame déjà là-dessus dans un notebook, elle va exploser dès qu’on passera à plusieurs millions de lignes ou à un traitement lancé tous les matins en production.

order_id Identifiant de commande
customer_age Âge du client
product_category Catégorie du produit
region Région de vente
price Prix unitaire
quantity Quantité achetée
days_to_ship Délai d’expédition en jours

La mauvaise approche classique ressemble à ça.

df["revenue"] = 0.0

for index, row in df.iterrows():
    df.loc[index, "revenue"] = row["price"] * row["quantity"]

Le calcul est simple. Le problème, c’est la mécanique autour. À chaque tour, pandas construit une ligne, Python lit les valeurs, pandas réécrit une cellule. On paye le coût de coordination 100 000 fois.

Le bon réflexe, ce n’est pas de réécrire toute votre stack. C’est de remplacer cette habitude par une carte mentale claire des bons outils pandas : opérations vectorisées, conditions par colonnes, groupby, map, where, assign. C’est souvent là qu’on gagne 10x, 50x, parfois plus, sans rendre le notebook illisible.

Quand utiliser les opérations vectorisées ?

Les opérations vectorisées sont mon premier réflexe dès qu’on fait du calcul arithmétique, une comparaison simple ou une transformation colonne à colonne.

Dans pandas, l’idée est simple : au lieu de passer ligne par ligne avec une boucle Python, je demande à pandas de travailler sur des colonnes entières. Derrière, pandas délègue une grosse partie du boulot à NumPy, la brique Python optimisée pour manipuler des tableaux de données numériques. Et NumPy fait ça beaucoup plus vite, parce qu’il évite le coût de Python à chaque ligne.

Le cas typique sur un jeu de données e-commerce, c’est le calcul du chiffre d’affaires par ligne.

df['revenue'] = df['price'] * df['quantity']

C’est court, lisible, et surtout beaucoup plus rapide qu’une boucle qui ferait le calcul commande par commande. Franchement, quand je vois encore du for row in df pour ce genre de calcul, je sais déjà où on va gagner du temps.

Le même principe marche très bien pour des transformations simples. Par exemple, marquer les grosses commandes quand la quantité est supérieure ou égale à 5.

df['is_large_order'] = df['quantity'] >= 5

Ou calculer un prix TTC avec une taxe à 20 %.

df['price_with_tax'] = df['price'] * 1.2

La règle que j’utilise souvent avec mes clients est assez directe : si la logique tient en une expression colonne à colonne, je ne sors pas l’artillerie lourde. Pas besoin de apply, pas besoin de boucle, pas besoin de fonction custom pour faire une multiplication ou une comparaison.

Ça rend aussi le code plus facile à relire. Trois semaines plus tard, vous comprenez encore ce que fait la ligne. Une boucle avec des conditions partout, c’est rarement le cas.

Cas Mauvais réflexe Bon réflexe
Calcul arithmétique Boucler sur chaque ligne pour multiplier le prix par la quantité.
df['revenue'] = df['price'] * df['quantity']
Comparaison simple Tester chaque commande une par une avec une boucle.
df['is_large_order'] = df['quantity'] >= 5
Combinaison de colonnes Créer une fonction juste pour assembler deux colonnes simples. Utiliser directement une expression entre colonnes pandas.

Comment gérer les conditions sans boucle ?

Pour les conditions, j’utilise np.where quand il n’y a que deux sorties, np.select quand il y a plusieurs cas, et apply seulement quand la logique devient trop spécifique.

Le cas le plus simple, c’est la condition binaire. Oui ou non. Vrai ou faux. Client senior ou pas senior.

import numpy as np

df['senior_discount'] = np.where(
    df['customer_age'] >= 60,
    True,
    False
)

Ici, np.where applique la condition sur toute la colonne d’un coup. C’est vectorisé, ça veut dire que Pandas et NumPy travaillent sur des blocs de données plutôt que ligne par ligne en Python. C’est exactement ce qu’on veut pour remplacer une boucle lente.

Dans ce cas précis, je pourrais même écrire plus court.

df['senior_discount'] = df['customer_age'] >= 60

Ça retourne déjà une colonne de True ou False. Mais np.where devient pratique quand les deux valeurs de sortie sont plus explicites, par exemple « Discount » et « No discount », ou 0.15 et 0.

Quand il y a plusieurs cas, je passe à np.select. Exemple classique : un taux de taxe selon la région.

conditions = [
    df['region'] == 'EU',
    df['region'] == 'US',
    df['region'] == 'UK'
]

tax_rates = [
    0.20,
    0.08,
    0.15
]

df['tax_rate'] = np.select(conditions, tax_rates, default=0)

df['tax_amount'] = df['price'] * df['tax_rate']

Là, le code reste lisible. On a une liste de conditions, une liste de valeurs, et une valeur par défaut. C’est beaucoup plus propre qu’une pyramide de if elif imbriqués dans une boucle. J’ai vu ça chez un client sur des règles tarifaires, le fichier faisait peur alors que la logique tenait en dix lignes avec np.select.

Il reste apply. Je ne le mets pas en premier, parce que ce n’est pas magique. Ça reste du Python appliqué plusieurs fois, souvent ligne par ligne ou valeur par valeur. Donc niveau performance, ce n’est pas au même niveau que du vrai vectorisé.

def shipping_label(days_to_ship):
    if days_to_ship <= 1:
        return 'Express'
    if days_to_ship <= 4:
        return 'Standard'
    return 'Economy'

df['shipping_label'] = df['days_to_ship'].apply(shipping_label)

Mais apply a un vrai avantage : c’est lisible, facile à tester, facile à déboguer. Quand np.where ou np.select rendent le code illisible, je préfère parfois perdre un peu en performance et garder une logique métier claire.

Ma règle simple : deux cas égale np.where, plusieurs cas égale np.select, logique métier tordue égale apply, mais toujours avec un œil sur les performances.

Comment mapper les valeurs et nettoyer le texte ?

Pour traduire des valeurs ou manipuler du texte, j’évite les boucles et j’utilise map ou l’accessor str de pandas.

Quand j’ai une correspondance simple, par exemple une catégorie produit à transformer en code département, map est souvent le bon outil. C’est exactement le cas d’une petite table de traduction en dictionnaire.

category_codes = {
    'Electronics': 'ELEC',
    'Clothing': 'CLO',
    'Home': 'HOME'
}

df['dept_code'] = df['product_category'].map(category_codes)

Ce code dit simplement : pour chaque valeur de product_category, va chercher le code correspondant dans category_codes. Pas besoin de boucle for, pas besoin de apply lambda. Pandas sait faire ça sur toute la colonne.

Il y a juste un piège à connaître. Si une catégorie n’existe pas dans le dictionnaire, map renvoie une valeur manquante. En pandas, on parle souvent de NaN, c’est-à-dire une valeur vide ou inconnue. Sur de vraies données e-commerce, ça arrive tout le temps. Un libellé change, quelqu’un ajoute “Electronic”, “Electronics & Gadgets”, ou une catégorie arrive depuis un flux externe. Donc je vérifie toujours.

df[df['dept_code'].isna()]['product_category'].unique()

Pour le texte, j’utilise .str. C’est l’accessor pandas prévu pour appliquer des méthodes de chaînes de caractères à toute une colonne. Par exemple, si je veux créer un slug simple à partir du premier mot de la catégorie :

df['category_slug'] = df['product_category'].str.split().str[0].str.lower()

Ce genre de code est lisible et rapide. .str.split() découpe le texte, .str[0] prend le premier élément, .str.lower() met en minuscules. Et surtout, ça évite les apply lambda inutiles pour des opérations standard comme lower, contains, split ou replace. J’ai vu pas mal de pipelines ralentir juste à cause de ça, alors que la correction tient en une ligne.

Besoin Outil
Traduire une valeur map
Normaliser du texte str.lower
Extraire une partie str.split
Détecter un motif str.contains

Que faire pour les calculs par groupes ?

Pour les calculs par groupes, groupby avec transform évite de boucler sur chaque région, chaque catégorie ou chaque client.

Beaucoup de boucles pandas viennent d’un besoin très classique : calculer une moyenne par catégorie, puis remettre ce résultat au niveau de chaque ligne. On voit souvent ça avec une boucle sur les catégories, un filtre, un calcul, puis une affectation. Ça marche, mais c’est lent, verbeux, et franchement fragile dès que le volume monte.

C’est exactement le genre de cas où groupby.transform est plus adapté qu’une boucle manuelle. groupby sert à créer des groupes dans le DataFrame. transform applique un calcul à chaque groupe, puis renvoie un résultat aligné avec les lignes d’origine.

df['avg_price_by_category'] = df.groupby('product_category')['price'].transform('mean')

Ici, chaque ligne récupère la moyenne de prix de sa catégorie produit. Pas besoin de parcourir les catégories une par une. Pandas fait le travail proprement, en interne, et beaucoup plus vite dans la plupart des cas.

Même logique si on veut calculer la part de chiffre d’affaires d’une ligne dans sa région. Je pars du principe que revenue a déjà été calculé dans le chapitre sur les opérations vectorisées, par exemple avec une multiplication entre quantité et prix.

df['revenue_share_in_region'] = df['revenue'] / df.groupby('region')['revenue'].transform('sum')

J’aime bien cet exemple parce qu’il parle à tout le monde. On divise le revenu de chaque ligne par le revenu total de sa région. Sans boucle. Sans DataFrame temporaire compliqué. Sans merge inutile.

Méthode Ce qu’elle fait Résultat
agg Résume chaque groupe avec un calcul comme mean, sum ou count Réduit le nombre de lignes
transform Calcule par groupe puis remet la valeur sur chaque ligne d’origine Garde le même nombre de lignes

agg est très bien pour produire un tableau résumé. transform est souvent ce qu’on veut pour enrichir une table avant une analyse, un dashboard ou un modèle de machine learning.

Voici la carte mentale que je garde en tête quand je remplace des boucles pandas lentes :

  • Vectorisation : Pour les calculs simples entre colonnes.
  • np.where : Pour une condition binaire, oui ou non.
  • np.select : Pour plusieurs conditions propres.
  • apply : Pour une logique métier spéciale, quand rien d’autre ne colle.
  • map : Pour remplacer des valeurs avec une table de correspondance.
  • str : Pour traiter du texte sans boucle.
  • groupby.transform : Pour les calculs par groupes à remettre ligne par ligne.

Le bon choix n’est pas forcément le plus sophistiqué. C’est celui qui colle à la transformation.

Et si on arrêtait de traiter pandas comme une boucle ?

Les boucles ligne par ligne donnent l’impression de contrôler le traitement, mais elles coûtent cher dès que le DataFrame grossit. Mon réflexe, c’est de partir des colonnes : calcul vectorisé quand c’est simple, np.where ou np.select pour les conditions, map pour les correspondances, str pour le texte, groupby.transform pour les calculs par groupes. apply reste utile, mais je le garde pour les cas où la logique métier ne rentre pas proprement ailleurs. Avec cette grille de lecture, vous écrivez moins de code, vous le déboguez plus vite, et surtout vous gagnez du temps sur vos traitements pandas.

FAQ

  • Pourquoi les boucles sont lentes dans pandas ?
    Parce qu’elles traitent les données ligne par ligne côté Python. pandas est beaucoup plus efficace quand il travaille sur des colonnes entières, avec des opérations vectorisées qui s’appuient sur NumPy et des fonctions optimisées.
  • Est-ce que apply est toujours une mauvaise idée ?
    Pas toujours. Je l’utilise quand la logique métier devient trop spécifique pour np.where ou np.select. Par contre, je ne le prends pas comme premier réflexe. Pour un calcul simple, une condition binaire ou une correspondance, il y a souvent plus rapide et plus clair.
  • Quelle différence entre np.where et np.select ?
    np.where est parfait pour une condition avec deux sorties possibles. np.select sert quand vous avez plusieurs conditions et plusieurs valeurs à assigner. Ça évite les if elif imbriqués et ça garde le code lisible sur un DataFrame.
  • Quand utiliser map dans pandas ?
    map est adapté quand vous voulez traduire une valeur en une autre avec un dictionnaire. Par exemple transformer une catégorie produit en code interne. C’est propre, rapide, et très lisible, à condition de vérifier les valeurs qui ne sont pas présentes dans le dictionnaire.
  • Comment faire des calculs par groupe sans boucle ?
    J’utilise groupby avec transform quand je veux calculer une métrique par groupe tout en gardant le même nombre de lignes dans le DataFrame. Par exemple une moyenne de prix par catégorie ou une part de revenue par région.

 

 

A propos de l’auteur

Je suis Franck Scandolera, expert et formateur en Tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, intégration de l’IA en entreprise et SEO/GEO. Avec mon agence webAnalyste et l’organisme Formations Analytics, j’accompagne des équipes qui manipulent de la data au quotidien, souvent entre pandas, pipelines, dashboards et automatisations. J’ai travaillé pour des clients comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Si vous voulez fiabiliser vos traitements data ou accélérer vos workflows, contactez-moi.

Retour en haut