Skip to content
Thèmes
Pandas
Pandas Apply: Transform DataFrames with Custom Functions

Pandas Apply : Transformez les DataFrames et les Series avec des fonctions personnalisées

Updated on

La transformation de données est au cœur de chaque flux de travail d'analyse de données. Alors que pandas fournit des centaines de méthodes intégrées pour les opérations courantes, les données du monde réel exigent souvent une logique personnalisée que les fonctions standard ne peuvent pas gérer. Cela crée un dilemme : comment appliquer efficacement des transformations complexes définies par l'utilisateur sur des milliers ou des millions de lignes ?

La méthode apply() résout ce problème en vous permettant d'exécuter n'importe quelle fonction Python sur les colonnes, les lignes ou les éléments de Series d'un DataFrame. Que vous ayez besoin de nettoyer des formats de chaînes incohérents, d'implémenter une logique métier conditionnelle ou de concevoir des fonctionnalités pour des modèles d'apprentissage automatique, apply() offre la flexibilité nécessaire pour gérer les opérations qui sortent de la boîte à outils intégrée de pandas. Cependant, cette puissance s'accompagne de compromis de performance que de nombreux data scientists négligent, conduisant à un code qui s'exécute 10 à 100 fois plus lentement que les alternatives optimisées.

Ce guide révèle comment utiliser apply() efficacement, quand l'éviter complètement et quelles alternatives vectorisées fournissent les mêmes résultats en une fraction du temps.

📚

Comprendre les bases de pandas apply()

La méthode apply() existe sous deux formes : DataFrame.apply() pour les opérations sur des colonnes ou des lignes entières, et Series.apply() pour les transformations élément par élément sur une seule colonne.

Syntaxe de Series.apply()

Lorsque vous travaillez avec une seule colonne (Series), apply() exécute une fonction sur chaque élément :

import pandas as pd
import numpy as np
 
# Créer des données d'exemple
df = pd.DataFrame({
    'price': [29.99, 45.50, 15.75, 89.00],
    'quantity': [2, 1, 5, 3],
    'product': ['widget', 'gadget', 'tool', 'device']
})
 
# Appliquer une fonction à Series
def add_tax(price):
    return price * 1.08
 
df['price_with_tax'] = df['price'].apply(add_tax)
print(df)

Sortie :

   price  quantity product  price_with_tax
0  29.99         2  widget        32.38920
1  45.50         1  gadget        49.14000
2  15.75         5    tool        17.01000
3  89.00         3  device        96.12000

DataFrame.apply() avec le paramètre axis

Le paramètre axis contrôle si apply() traite les colonnes ou les lignes :

  • axis=0 (par défaut) : Appliquer la fonction à chaque colonne (opération verticale)
  • axis=1 : Appliquer la fonction à chaque ligne (opération horizontale)
# axis=0 : Traiter chaque colonne
def get_range(column):
    return column.max() - column.min()
 
ranges = df[['price', 'quantity']].apply(get_range, axis=0)
print(ranges)
# Sortie :
# price       73.25
# quantity     4.00
# axis=1 : Traiter chaque ligne
def calculate_total(row):
    return row['price'] * row['quantity']
 
df['total'] = df.apply(calculate_total, axis=1)
print(df)

Sortie :

   price  quantity product   total
0  29.99         2  widget   59.98
1  45.50         1  gadget   45.50
2  15.75         5    tool   78.75
3  89.00         3  device  267.00

Fonctions Lambda vs. fonctions nommées

Les fonctions lambda fournissent des transformations en ligne concises, tandis que les fonctions nommées offrent une meilleure lisibilité pour une logique complexe.

Fonctions Lambda

Parfaites pour les opérations simples sur une ligne :

# Convertir les noms de produits en majuscules
df['product_upper'] = df['product'].apply(lambda x: x.upper())
 
# Calculer le prix avec remise
df['discounted'] = df['price'].apply(lambda x: x * 0.9 if x > 30 else x)
 
# Combiner plusieurs colonnes
df['description'] = df.apply(
    lambda row: f"{row['quantity']}x {row['product']} @ ${row['price']}",
    axis=1
)
print(df['description'])

Sortie :

0    2x widget @ $29.99
1    1x gadget @ $45.5
2       5x tool @ $15.75
3    3x device @ $89.0

Fonctions nommées

Essentielles pour les transformations en plusieurs étapes et la logique réutilisable :

def categorize_price(price):
    """Catégoriser les produits par niveau de prix"""
    if price < 20:
        return 'Budget'
    elif price < 50:
        return 'Standard'
    else:
        return 'Premium'
 
df['tier'] = df['price'].apply(categorize_price)
 
def validate_order(row):
    """Appliquer les règles métier aux données de commande"""
    if row['quantity'] > 10:
        return 'Bulk Order - Review Required'
    elif row['price'] * row['quantity'] > 200:
        return 'High Value - Priority Shipping'
    else:
        return 'Standard Processing'
 
df['order_status'] = df.apply(validate_order, axis=1)
print(df[['product', 'tier', 'order_status']])

Comprendre le paramètre result_type

Le paramètre result_type (DataFrame.apply() uniquement) contrôle comment apply() formate la sortie lorsque les fonctions renvoient plusieurs valeurs :

result_typeComportementCas d'usage
None (par défaut)Déduit automatiquement le format de sortieUsage général
'expand'Divise les résultats de type liste en colonnes séparéesPlusieurs valeurs de retour
'reduce'Tente de renvoyer une Series au lieu d'un DataFrameOpérations d'agrégation
'broadcast'Renvoie un DataFrame avec la forme originaleTransformations élément par élément
def get_stats(column):
    """Renvoyer plusieurs statistiques"""
    return [column.mean(), column.std(), column.max()]
 
# Comportement par défaut (déduit la structure)
stats = df[['price', 'quantity']].apply(get_stats)
print(stats)
 
# Étendre en lignes séparées
stats_expanded = df[['price', 'quantity']].apply(get_stats, result_type='expand')
print(stats_expanded)

Comparaison des méthodes : apply vs map vs applymap vs transform

Comprendre quand utiliser chaque méthode évite les goulots d'étranglement de performance :

MéthodeOpère surLa fonction d'entrée reçoitType de sortieMeilleur pour
Series.apply()Colonne uniqueChaque élément individuellementSeriesTransformations élément par élément sur une colonne
Series.map()Colonne uniqueChaque élément (accepte aussi dict/Series)SeriesOpérations de substitution/recherche
DataFrame.apply()DataFrame entierColonne complète (axis=0) ou ligne (axis=1)Series/DataFrameOpérations par colonne/ligne
DataFrame.applymap() (obsolète)DataFrame entierChaque élément individuellementDataFrameÉlément par élément sur toutes les colonnes (utilisez map() à la place)
DataFrame.transform()DataFrame/GroupByColonne/groupe completMême forme que l'entréeOpérations préservant la forme du DataFrame
# Series.apply() - fonction personnalisée élément par élément
df['price_rounded'] = df['price'].apply(lambda x: round(x, 0))
 
# Series.map() - mappage de substitution
tier_map = {'Budget': 1, 'Standard': 2, 'Premium': 3}
df['tier_code'] = df['tier'].map(tier_map)
 
# DataFrame.apply() - agrégation par colonne
totals = df[['price', 'quantity']].apply(sum, axis=0)
 
# DataFrame.transform() - préservation de la forme avec groupby
df['price_norm'] = df.groupby('tier')['price'].transform(
    lambda x: (x - x.mean()) / x.std()
)

Performance : Pourquoi apply() est lent

La méthode apply() traite les données dans une boucle Python, contournant les implémentations optimisées C/Cython de pandas. Pour les grands ensembles de données, cela crée des pénalités de performance sévères.

Comparaison de performance

import time
 
# Créer un grand ensemble de données
large_df = pd.DataFrame({
    'values': np.random.randn(100000)
})
 
# Méthode 1 : apply() avec lambda
start = time.time()
result1 = large_df['values'].apply(lambda x: x ** 2)
apply_time = time.time() - start
 
# Méthode 2 : Opération vectorisée
start = time.time()
result2 = large_df['values'] ** 2
vectorized_time = time.time() - start
 
print(f"Temps apply() : {apply_time:.4f}s")
print(f"Temps vectorisé : {vectorized_time:.4f}s")
print(f"Accélération : {apply_time/vectorized_time:.1f}x")

Sortie typique :

Temps apply() : 0.0847s
Temps vectorisé : 0.0012s
Accélération : 70.6x

Quand éviter apply()

Utilisez plutôt des opérations vectorisées quand :

# NE FAITES PAS : Utiliser apply pour l'arithmétique
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
 
# FAITES : Utiliser la multiplication vectorisée
df['total'] = df['price'] * df['quantity']
 
# NE FAITES PAS : Utiliser apply pour les méthodes de chaînes
df['upper'] = df['product'].apply(lambda x: x.upper())
 
# FAITES : Utiliser l'accesseur de chaîne intégré
df['upper'] = df['product'].str.upper()
 
# NE FAITES PAS : Utiliser apply pour les conditions
df['expensive'] = df['price'].apply(lambda x: 'Yes' if x > 50 else 'No')
 
# FAITES : Utiliser np.where ou comparaison directe
df['expensive'] = np.where(df['price'] > 50, 'Yes', 'No')

Alternatives vectorisées

OpérationLent (apply)Rapide (vectorisé)
Arithmétique.apply(lambda x: x * 2)* 2
Conditionnels.apply(lambda x: 'A' if x > 10 else 'B')np.where(condition, 'A', 'B')
Opérations de chaînes.apply(lambda x: x.lower()).str.lower()
Opérations de dates.apply(lambda x: x.year).dt.year
Conditions multiples.apply(complex_if_elif_else)np.select([cond1, cond2], [val1, val2], default)
# Conditionnels complexes avec np.select
conditions = [
    df['price'] < 20,
    (df['price'] >= 20) & (df['price'] < 50),
    df['price'] >= 50
]
choices = ['Budget', 'Standard', 'Premium']
df['tier_fast'] = np.select(conditions, choices, default='Unknown')

Cas d'usage courants pour pandas apply()

Malgré les limitations de performance, apply() reste essentiel pour les opérations qui n'ont pas d'équivalents vectorisés.

1. Nettoyage de chaînes avec une logique complexe

# Exemple de données désordonnées
messy_df = pd.DataFrame({
    'email': ['John.Doe@COMPANY.com', ' jane_smith@test.co.uk ', 'ADMIN@Site.NET']
})
 
def clean_email(email):
    """Standardiser le format d'email"""
    email = email.strip().lower()
    # Supprimer les points supplémentaires
    username, domain = email.split('@')
    username = username.replace('..', '.')
    return f"{username}@{domain}"
 
messy_df['email_clean'] = messy_df['email'].apply(clean_email)
print(messy_df)

2. Logique conditionnelle avec des données externes

# Ajustement de prix basé sur une recherche externe
discount_rules = {
    'widget': 0.10,
    'gadget': 0.15,
    'device': 0.05
}
 
def apply_discount(row):
    """Appliquer une remise spécifique au produit avec logique minimale"""
    base_price = row['price']
    discount = discount_rules.get(row['product'], 0)
    discounted = base_price * (1 - discount)
    # Prix minimum
    return max(discounted, 9.99)
 
df['final_price'] = df.apply(apply_discount, axis=1)

3. Ingénierie de caractéristiques pour ML

# Créer des caractéristiques d'interaction
def create_features(row):
    """Générer des caractéristiques pour la modélisation prédictive"""
    features = {}
    features['price_per_unit'] = row['price'] / row['quantity']
    features['is_bulk'] = 1 if row['quantity'] > 5 else 0
    features['revenue_tier'] = pd.cut(
        [row['price'] * row['quantity']],
        bins=[0, 50, 150, 300],
        labels=['Low', 'Medium', 'High']
    )[0]
    return pd.Series(features)
 
feature_df = df.apply(create_features, axis=1)
df = pd.concat([df, feature_df], axis=1)

4. Appels d'API et recherches externes

def geocode_address(address):
    """Appeler une API externe pour le géocodage"""
    # Espace réservé pour l'appel API réel
    # En pratique : requests.get(f"api.geocode.com?q={address}")
    return {'lat': 40.7128, 'lon': -74.0060}
 
# Appliquer avec limitation de débit
import time
 
def safe_geocode(address):
    time.sleep(0.1)  # Limitation de débit
    return geocode_address(address)
 
# df['coords'] = df['address'].apply(safe_geocode)

Techniques avancées

Passer des arguments supplémentaires

Les fonctions peuvent recevoir des paramètres supplémentaires via args et kwargs :

def apply_markup(price, markup_pct, min_profit):
    """Ajouter une marge en pourcentage avec profit minimum"""
    markup_amount = price * (markup_pct / 100)
    return price + max(markup_amount, min_profit)
 
# Passer des arguments supplémentaires
df['retail_price'] = df['price'].apply(
    apply_markup,
    args=(25,),  # markup_pct=25
    min_profit=5.0  # argument mot-clé
)

Utiliser apply() avec groupby()

Combiner groupby avec apply pour des agrégations complexes :

# Transformations au niveau du groupe
def normalize_group(group):
    """Normalisation Z-score au sein du groupe"""
    return (group - group.mean()) / group.std()
 
df['price_normalized'] = df.groupby('tier')['price'].apply(normalize_group)
 
# Agrégations de groupe personnalisées
def group_summary(group):
    """Créer des statistiques récapitulatives pour le groupe"""
    return pd.Series({
        'total_revenue': (group['price'] * group['quantity']).sum(),
        'avg_price': group['price'].mean(),
        'item_count': len(group)
    })
 
tier_summary = df.groupby('tier').apply(group_summary)
print(tier_summary)

Barres de progression avec tqdm

Surveiller les opérations apply de longue durée :

from tqdm import tqdm
tqdm.pandas()
 
# Utiliser progress_apply au lieu de apply
# df['result'] = df['column'].progress_apply(slow_function)

Gérer les erreurs avec élégance

def safe_transform(value):
    """Appliquer une transformation avec gestion des erreurs"""
    try:
        return complex_operation(value)
    except Exception as e:
        return None  # ou np.nan, ou valeur par défaut
 
df['result'] = df['column'].apply(safe_transform)

Erreurs courantes et débogage

Erreur 1 : Utiliser axis=1 quand la vectorisation est possible

# MAUVAIS : Opération lente par ligne
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
 
# BON : Opération vectorisée rapide
df['total'] = df['price'] * df['quantity']

Erreur 2 : Oublier l'instruction return

# MAUVAIS : Pas de valeur de retour
def broken_function(x):
    x * 2  # return manquant !
 
# BON : Return explicite
def working_function(x):
    return x * 2

Erreur 3 : Modifier le DataFrame à l'intérieur de apply()

# MAUVAIS : Tentative de modifier le DataFrame pendant l'itération
def bad_function(row):
    df.loc[row.name, 'new_col'] = row['price'] * 2  # Dangereux !
    return row['price']
 
# BON : Renvoyer des valeurs et assigner après
def good_function(row):
    return row['price'] * 2
 
df['new_col'] = df.apply(good_function, axis=1)

Déboguer les fonctions apply()

# Tester d'abord la fonction sur une seule ligne/élément
test_row = df.iloc[0]
result = your_function(test_row)
print(f"Résultat du test : {result}")
 
# Ajouter des impressions de débogage dans la fonction
def debug_function(row):
    print(f"Traitement de la ligne {row.name} : {row.to_dict()}")
    result = complex_logic(row)
    print(f"Résultat : {result}")
    return result
 
# Tester sur un petit sous-ensemble
df.head(3).apply(debug_function, axis=1)

Visualiser les résultats avec PyGWalker

Après avoir transformé votre DataFrame avec apply(), la visualisation des résultats aide à valider les transformations et à découvrir des motifs. PyGWalker transforme votre pandas DataFrame en une interface interactive de type Tableau directement dans les notebooks Jupyter.

import pygwalker as pyg
 
# Visualiser les données transformées de manière interactive
walker = pyg.walk(df)

PyGWalker permet l'analyse par glisser-déposer de vos transformations appliquées :

  • Comparez les colonnes originales et transformées avec des graphiques côte à côte
  • Validez la logique conditionnelle par filtrage et regroupement
  • Détectez les valeurs aberrantes dans les champs calculés grâce aux graphiques de distribution
  • Exportez les visualisations pour la documentation

Explorez PyGWalker sur github.com/Kanaries/pygwalker (opens in a new tab) pour transformer l'exploration de données de graphiques statiques en analyse interactive.

FAQ

Comment appliquer une fonction à plusieurs colonnes à la fois ?

Utilisez DataFrame.apply() avec axis=0 pour traiter chaque colonne sélectionnée, ou utilisez des opérations vectorisées directement sur plusieurs colonnes :

# Appliquer à plusieurs colonnes
df[['price', 'quantity']] = df[['price', 'quantity']].apply(lambda x: x * 1.1)
 
# Ou vectorisé (plus rapide)
df[['price', 'quantity']] = df[['price', 'quantity']] * 1.1

Quelle est la différence entre apply() et map() ?

apply() exécute n'importe quelle fonction appelable élément par élément, tandis que map() est optimisé pour la substitution via des dictionnaires, des Series ou des fonctions. Utilisez map() pour les recherches et les remplacements (plus rapide), et apply() pour les transformations personnalisées nécessitant une logique complexe.

Pourquoi ma fonction apply() est-elle si lente ?

apply() s'exécute dans une boucle Python plutôt que du code C compilé, ce qui le rend 10 à 100 fois plus lent que les opérations vectorisées. Vérifiez toujours si pandas a une méthode intégrée (.str, .dt, opérateurs arithmétiques) ou si np.where()/np.select() peut remplacer votre logique avant d'utiliser apply().

Puis-je utiliser apply() avec des fonctions lambda qui accèdent à plusieurs colonnes ?

Oui, avec axis=1 pour traiter les lignes :

df['result'] = df.apply(lambda row: row['col1'] + row['col2'] * 0.5, axis=1)

Cependant, c'est lent pour les grands DataFrames. Préférez : df['result'] = df['col1'] + df['col2'] * 0.5

Comment renvoyer plusieurs colonnes à partir d'un seul appel apply() ?

Renvoyez une pd.Series avec des indices nommés, et pandas l'étendra automatiquement en colonnes :

def multi_output(row):
    return pd.Series({
        'sum': row['a'] + row['b'],
        'product': row['a'] * row['b']
    })
 
df[['sum', 'product']] = df.apply(multi_output, axis=1)

Conclusion

La méthode pandas apply() offre une flexibilité essentielle pour les transformations personnalisées qui sortent des opérations intégrées de pandas. Bien que son implémentation en boucle Python crée des compromis de performance, comprendre quand utiliser apply() par rapport aux alternatives vectorisées sépare les flux de travail de données efficaces de ceux qui s'arrêtent sur les ensembles de données de production.

Points clés : Utilisez apply() uniquement lorsque les méthodes vectorisées, les accesseurs de chaînes ou les fonctions NumPy ne peuvent pas atteindre votre objectif. Testez les fonctions sur des lignes individuelles avant de les appliquer à des DataFrames entiers. Pour les opérations par ligne sur de grands ensembles de données, explorez Cython, la compilation JIT Numba ou le passage à polars pour l'exécution parallèle.

Maîtrisez à la fois la puissance et les limitations de apply(), et votre boîte à outils de transformation de données gérera tous les défis que pandas vous lance.

📚