22 astuces Pandas qui rendront ton travail plus efficace

couverture 22 astuces pandas

Pandas est la bibliothèque Python la plus utilisée pour les données structurées. Il existe de nombreux tutoriels, mais j’aimerais présenter ici des astuces Pandas que tu ne connais peut-être pas encore, ou que tu connais mais sans savoir qu’elles s’utilisent de cette manière.

En bref : ces 22 astuces Pandas couvrent la lecture de fichiers, la manipulation de colonnes, le filtrage, le groupby, les dates et la performance. Trois d’entre elles changent vraiment le temps de calcul : remplacer iterrows() par itertuples() (ou mieux, par une opération vectorisée), utiliser nlargest() plutôt que sort_values().head(), et typer les colonnes à la lecture (usecols, dtype, category).

Tout le code a été testé en octobre 2026 avec Pandas 2.3.3 et Pandas 3.0.5 : les résultats sont identiques, sauf mention contraire. Les temps d’exécution donnés sont mesurés sur ma machine : les tiens seront différents, mais les ordres de grandeur resteront les mêmes.

Tu peux aussi exécuter toutes les astuces dans ce notebook Google Colab : chaque extrait est dans sa propre cellule, prêt à être modifié.

22 astuces Pandas pour la data science en Python

Le jeu de données utilisé dans l’article

Un DataFrame est un tableau de données à deux dimensions (des lignes et des colonnes), l’objet central de Pandas. La plupart des exemples utilisent ce petit DataFrame de dix élèves, avec leur ville, leur niveau (1, 2 ou 3) et leurs notes en maths et en sciences. Quand une astuce a besoin d’autres données (un gros fichier, des dates, des valeurs manquantes), je les crée dans l’extrait lui-même.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'ID': ['A001', 'B014', 'C022', 'D031', 'E047', 'F058', 'G063', 'H070', 'I082', 'J095'],
    'ville': ['Lisbonne', 'Paris', 'Zurich', 'Lisbonne', 'Paris', 'Lisbonne', 'Zurich', 'Paris', 'Lisbonne', 'Zurich'],
    'niveau': [1, 2, 3, 1, 2, 3, 1, 2, 3, 1],
    'maths': [35, 72, 58, 35, 90, 64, 47, 81, 35, 66],
    'sciences': [40, 65, 70, 30, 88, 55, 52, 79, 45, 71],
})
print(df)
Python
     ID     ville  niveau  maths  sciences
0  A001  Lisbonne       1     35        40
1  B014     Paris       2     72        65
2  C022    Zurich       3     58        70
3  D031  Lisbonne       1     35        30
4  E047     Paris       2     90        88
5  F058  Lisbonne       3     64        55
6  G063    Zurich       1     47        52
7  H070     Paris       2     81        79
8  I082  Lisbonne       3     35        45
9  J095    Zurich       1     66        71

Lecture et chargement de données

1. Comment lire rapidement un gros fichier CSV avec read_csv ?

Pour lire vite un gros fichier CSV, lis d’abord quelques lignes avec nrows=5, puis charge seulement les colonnes utiles (usecols) avec des types légers (dtype). Tout le monde connaît read_csv, qui lit un fichier CSV dans un DataFrame. Mais si le fichier est volumineux, commence par ne lire que quelques lignes avec l’argument nrows=5 : tu vérifies que le séparateur et les colonnes sont les bons avant de charger toute la table.

Pour la démonstration, je crée d’abord un fichier de 200 000 lignes dont le séparateur est un point-virgule (très courant dans les fichiers exportés depuis Excel en français) :

import pandas as pd
import numpy as np

rng = np.random.default_rng(0)
n = 200_000
pd.DataFrame({
    'id': np.arange(n),
    'ville': rng.choice(['Lisbonne', 'Paris', 'Zurich'], n),
    'montant': rng.normal(100, 20, n).round(2),
    'quantite': rng.integers(1, 10, n),
    'remise': rng.random(n).round(3),
}).to_csv('ventes.csv', sep=';', index=False)
Python

Si tu oublies de préciser le séparateur, tu le vois tout de suite : tout se retrouve dans une seule colonne.

import pandas as pd

apercu = pd.read_csv('ventes.csv', nrows=5)
print(apercu)
Python
  id;ville;montant;quantite;remise
0            0;Zurich;83.1;8;0.249
1           1;Paris;115.04;1;0.023
2            2;Paris;105.41;7;0.02
3         3;Lisbonne;99.03;7;0.604
4        4;Lisbonne;116.77;6;0.997

Avec le bon séparateur (sep=';'), les colonnes apparaissent correctement. columns.tolist() te donne la liste des noms de colonnes à copier.

import pandas as pd

apercu = pd.read_csv('ventes.csv', sep=';', nrows=5)
print(apercu)
print(apercu.columns.tolist())
Python
   id     ville  montant  quantite  remise
0   0    Zurich    83.10         8   0.249
1   1     Paris   115.04         1   0.023
2   2     Paris   105.41         7   0.020
3   3  Lisbonne    99.03         7   0.604
4   4  Lisbonne   116.77         6   0.997
['id', 'ville', 'montant', 'quantite', 'remise']

(Tu peux aussi regarder les premières lignes de n’importe quel fichier texte depuis le terminal, avec head -n 5 data.txt sous Linux et macOS.)

Ensuite, charge uniquement ce dont tu as besoin. usecols limite les colonnes lues. dtype évite à Pandas de deviner le type de chaque colonne, et te permet de choisir un type plus léger (ici int32, float32 et category). Autre avantage : si une colonne mélange du texte et des chiffres (des identifiants comme « 007 » ou « A12 »), déclare-la en str pour éviter les erreurs quand tu fusionneras des tableaux avec cette colonne comme clé.

import pandas as pd

df_ventes = pd.read_csv(
    'ventes.csv',
    sep=';',
    usecols=['id', 'ville', 'montant'],
    dtype={'id': 'int32', 'ville': 'category', 'montant': 'float32'},
)
print(df_ventes.dtypes)
print(df_ventes.shape)
Python
id            int32
ville      category
montant     float32
dtype: object
(200000, 3)

L’effet sur la mémoire est considérable :

import pandas as pd

complet = pd.read_csv('ventes.csv', sep=';')
cible = pd.read_csv('ventes.csv', sep=';', usecols=['id', 'ville', 'montant'],
                    dtype={'id': 'int32', 'ville': 'category', 'montant': 'float32'})

print(f"Lecture complète : {complet.memory_usage(deep=True).sum() / 1e6:.1f} Mo")
print(f"Lecture ciblée   : {cible.memory_usage(deep=True).sum() / 1e6:.1f} Mo")
Python
Lecture complète : 17.5 Mo
Lecture ciblée   : 1.8 Mo

2. Comment sélectionner les colonnes selon leur type avec select_dtypes ?

La méthode select_dtypes sélectionne les colonnes d’un DataFrame selon leur type (nombres, texte, dates…). Pour le pré-traitement, select_dtypes te fait gagner du temps. Commence par vérifier la répartition des types de colonnes :

print(df.dtypes.value_counts())
Python
int64    3
str      2
Name: count, dtype: int64

Dans Pandas 3, les colonnes de texte ont le type str ; dans Pandas 2 et avant, elles ont le type object (c’est la seule différence visible dans cet article).

Puis sélectionne les colonnes numériques (entiers et décimaux) avec include='number', ou à l’inverse toutes les autres avec exclude='number'. C’est plus court que de lister ['float64', 'int64'], et ça couvre aussi int32, float32, etc.

print(df.select_dtypes(include='number').head(3))
print()
print(df.select_dtypes(exclude='number').head(3))
Python
   niveau  maths  sciences
0       1     35        40
1       2     72        65
2       3     58        70

     ID     ville
0  A001  Lisbonne
1  B014     Paris
2  C022    Zurich

Attention : dans Pandas 3, include='object' ne reste compatible avec les colonnes de texte qu’au prix d’un avertissement. Pour les colonnes de texte, préfère exclude='number' ou include=['object', 'string'].

Manipulation de colonnes

3. Comment copier un DataFrame sans modifier l’original ?

Pour copier un DataFrame sans modifier l’original, utilise df.copy() : l’affectation df2 = df1 ne copie rien. C’est une méthode importante si tu n’en as pas encore entendu parler. Regarde ce code :

import pandas as pd

df1 = pd.DataFrame({'a': [0, 0, 0], 'b': [1, 1, 1]})
df2 = df1
df2['a'] = df2['a'] + 1
print(df1)
print(df1 is df2)
Python
   a  b
0  1  1
1  1  1
2  1  1
True

Le DataFrame df1 a été modifié alors qu’on n’a touché qu’à df2. En effet, df2 = df1 ne crée pas de copie : c’est un deuxième nom pour le même objet (la dernière ligne l’affiche : df1 is df2 vaut True). Toute modification via l’un des deux noms se voit donc dans l’autre.

Pour obtenir un DataFrame indépendant, utilise copy() :

import pandas as pd

df1 = pd.DataFrame({'a': [0, 0, 0], 'b': [1, 1, 1]})
df2 = df1.copy()
df2['a'] = df2['a'] + 1
print(df1)
print(df2)
Python
   a  b
0  0  1
1  0  1
2  0  1
   a  b
0  1  1
1  1  1
2  1  1

(La fonction deepcopy du module copy donne le même résultat, mais df.copy() est plus court.) Avec Pandas 3, les sous-tableaux issus d’un filtre ou d’un découpage se comportent toujours comme des copies (c’est le mécanisme « Copy-on-Write »), mais une simple affectation df2 = df1 reste un second nom pour le même objet.

4. Comment remplacer des valeurs avec map ?

La méthode map() remplace chaque valeur d’une série à l’aide d’un dictionnaire. Il s’agit d’une commande simple pour transformer des valeurs. Tu définis un dictionnaire dont les clés sont les anciennes valeurs et les valeurs sont les nouvelles :

niveau_map = {1: 'élevé', 2: 'moyen', 3: 'faible'}
df['niveau_txt'] = df['niveau'].map(niveau_map)
print(df[['ID', 'niveau', 'niveau_txt']].head(4))
Python
     ID  niveau niveau_txt
0  A001       1      élevé
1  B014       2      moyen
2  C022       3     faible
3  D031       1      élevé

Les valeurs absentes du dictionnaire deviennent NaN. Les usages classiques : transformer True et False en 1 et 0 pour la modélisation, définir des niveaux, appliquer un codage défini par toi.

reussite = df['maths'] >= 50
print(reussite.map({True: 1, False: 0}).tolist())
Python
[0, 1, 1, 0, 1, 1, 0, 1, 0, 1]

5. Faut-il utiliser apply sur un DataFrame ?

La méthode apply(axis=1) appelle une fonction Python sur chaque ligne : elle est pratique mais lente, donc à réserver aux cas sans équivalent vectorisé. Si tu veux créer une colonne à partir de plusieurs autres, apply avec axis=1 peut être très pratique. Ici (avec la colonne niveau_txt créée à l’astuce 4), on donne un bonus aux élèves de niveau « moyen » qui ont plus de 75 en maths :

def regle(niveau, maths):
    if niveau == 'moyen' and maths > 75:
        return 1
    else:
        return 0

df['bonus'] = df.apply(lambda ligne: regle(ligne['niveau_txt'], ligne['maths']), axis=1)
print(df[['ID', 'niveau_txt', 'maths', 'bonus']])
Python
     ID niveau_txt  maths  bonus
0  A001      élevé     35      0
1  B014      moyen     72      0
2  C022     faible     58      0
3  D031      élevé     35      0
4  E047      moyen     90      1
5  F058     faible     64      0
6  G063      élevé     47      0
7  H070      moyen     81      1
8  I082     faible     35      0
9  J095      élevé     66      0

Le problème : avec axis=1, Pandas appelle ta fonction Python pour chaque ligne. C’est lent. Quand une opération vectorisée existe, elle fait le même travail bien plus vite. Ici, on peut écrire la règle directement avec des comparaisons :

df['bonus_vec'] = ((df['niveau_txt'] == 'moyen') & (df['maths'] > 75)).astype(int)
print((df['bonus'] == df['bonus_vec']).all())
Python
True

Pour calculer le maximum de deux colonnes, utilise directement max(axis=1) :

df['meilleur_score'] = df[['maths', 'sciences']].max(axis=1)
print(df[['maths', 'sciences', 'meilleur_score']].head(4))
Python
   maths  sciences  meilleur_score
0     35        40              40
1     72        65              72
2     58        70              70
3     35        30              35

plutôt que df.apply(lambda x: max(x['maths'], x['sciences']), axis=1). Sur 200 000 lignes, l’écart est énorme :

import time
import numpy as np
import pandas as pd

grand = pd.DataFrame({
    'c1': np.random.default_rng(0).integers(0, 100, 200_000),
    'c2': np.random.default_rng(1).integers(0, 100, 200_000),
})

debut = time.perf_counter()
grand.apply(lambda x: max(x['c1'], x['c2']), axis=1)
print(f"apply  : {time.perf_counter() - debut:.3f} s")

debut = time.perf_counter()
grand[['c1', 'c2']].max(axis=1)
print(f"max()  : {time.perf_counter() - debut:.3f} s")
Python
apply  : 0.969 s
max()  : 0.018 s

Règle d’or : n’utilise pas apply ligne par ligne si les fonctions intégrées de Pandas ou de NumPy font la même chose.

6. Comment insérer une colonne à une position précise avec insert ?

La méthode insert() ajoute une colonne à la position de ton choix, au lieu de la fin. Quand tu ajoutes une colonne, elle arrive à la fin par défaut. Avec insert, tu choisis sa position : l’indice (entier) en premier argument, le nom de la colonne en deuxième, les valeurs en troisième. Les valeurs doivent avoir autant d’éléments que de lignes, et le nom ne doit pas déjà exister.

import numpy as np
import pandas as pd

petit = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [10, 20, 30, 40], 'c': [5, 6, 7, 8]})
nouveau = np.random.default_rng(0).integers(5, size=len(petit))
petit.insert(2, 'nouvelle_col', nouveau)
print(petit)
Python
   a   b  nouvelle_col  c
0  1  10             4  5
1  2  20             3  6
2  3  30             2  7
3  4  40             1  8

7. Comment retirer une colonne tout en la conservant avec pop ?

La méthode pop() retire une colonne du DataFrame et te la renvoie sous forme de série, en une seule opération. Pour supprimer une colonne d’un DataFrame tout en la gardant dans une série séparée, plutôt qu’une affectation suivie d’un drop, utilise directement pop :

import pandas as pd

ventes = pd.DataFrame({'produit': ['A', 'B', 'C'], 'prix': [10, 25, 40], 'quantite': [3, 1, 2]})
prix = ventes.pop('prix')
print(prix)
print(ventes)
Python
0    10
1    25
2    40
Name: prix, dtype: int64
  produit  quantite
0       A         3
1       B         1
2       C         2

Filtrage et sélection

8. Comment sélectionner des lignes par identifiants avec isin ?

La méthode isin() repère les lignes dont la valeur figure dans une liste : c’est l’équivalent du IN de SQL. En SQL, tu écrirais SELECT * FROM … WHERE ID IN ('A001', 'C022', …). En Pandas, c’est isin ; le symbole ~ inverse le filtre (équivalent de « NOT IN ») :

filtre = df['ID'].isin(['A001', 'C022', 'J095'])
print(df[filtre][['ID', 'ville', 'maths']])
print()
print(df[~filtre]['ID'].tolist())
Python
     ID     ville  maths
0  A001  Lisbonne     35
2  C022    Zurich     58
9  J095    Zurich     66

['B014', 'D031', 'E047', 'F058', 'G063', 'H070', 'I082']

9. Comment filtrer un DataFrame avec query() ?

La méthode query() filtre un DataFrame avec une expression écrite sous forme de texte. Elle reconnaît les opérations entre colonnes :

print(df.query('maths < sciences')[['ID', 'maths', 'sciences']])
Python
     ID  maths  sciences
0  A001     35        40
2  C022     58        70
6  G063     47        52
8  I082     35        45
9  J095     66        71
print(df.query('maths - sciences > 5')[['ID', 'maths', 'sciences']])
Python
     ID  maths  sciences
1  B014     72        65
5  F058     64        55

Tu peux combiner plusieurs conditions avec & ou and, utiliser une variable Python avec @, et tester une appartenance avec in. Si le nom d’une colonne contient un espace, entoure-le d’accents graves : `prix total` > 3.

seuil = 60
print(df.query('maths > @seuil & ville == "Paris"')[['ID', 'ville', 'maths']])
print()
print(df.query('ville in ["Zurich", "Paris"] and sciences >= 70')[['ID', 'ville', 'sciences']])
Python
     ID  ville  maths
1  B014  Paris     72
4  E047  Paris     90
7  H070  Paris     81

     ID   ville  sciences
2  C022  Zurich        70
4  E047   Paris        88
7  H070   Paris        79
9  J095  Zurich        71

10. Comment comparer des colonnes avec gt(), lt(), eq() et les autres méthodes ?

Les opérateurs <, >, <=, >=, ==, != ont des équivalents sous forme de méthodes : .lt(), .gt(), .le(), .ge(), .eq() et .ne(). Ils sont pratiques pour enchaîner les méthodes sur une seule ligne et servent à :

  • comparer un DataFrame à une valeur de base (résultat : des booléens)
  • découper (slicing) le DataFrame selon une comparaison
  • créer une nouvelle colonne booléenne à partir de la comparaison de deux colonnes
# 1. Comparaison à une valeur de base
print(df[['maths', 'sciences']].gt(50).head(3))

# 2. Slicing basé sur une comparaison
df_diff = df[df['maths'].ne(35)]
df_egal = df[df['maths'].eq(35)]
print(len(df_diff), len(df_egal))

# 3. Nouvelle colonne booléenne à partir de deux colonnes
df['fort_en_maths'] = df['maths'].ge(df['sciences'])
print(df[['ID', 'maths', 'sciences', 'fort_en_maths']].head(4))
Python
   maths  sciences
0  False     False
1   True      True
2   True      True
7 3
     ID  maths  sciences  fort_en_maths
0  A001     35        40          False
1  B014     72        65           True
2  C022     58        70          False
3  D031     35        30           True

11. Comment obtenir les N plus grandes valeurs avec nlargest() ?

Pour obtenir les N plus grandes valeurs avec toutes leurs colonnes, utilise df.nlargest(N, columns='col') ; nsmallest() fait l'inverse. On veut souvent connaître les N meilleures ou les N pires lignes selon une colonne. Appliquer nlargest sur une seule colonne ne renvoie que les valeurs : les autres colonnes sont perdues (l'index permet seulement de retrouver les lignes).

print(df['maths'].nlargest(3))
Python
4    90
7    81
1    72
Name: maths, dtype: int64

Appliqué au DataFrame, il garde toutes les colonnes. En cas d'égalité, keep='first' (par défaut) coupe au premier rang, tandis que keep='all' garde toutes les lignes à égalité, comme ici pour les trois élèves à 35 :

print(df.nlargest(3, columns='maths')[['ID', 'ville', 'maths']])
print()
print(df.nsmallest(3, columns='maths', keep='all')[['ID', 'ville', 'maths']])
Python
     ID  ville  maths
4  E047  Paris     90
7  H070  Paris     81
1  B014  Paris     72

     ID     ville  maths
0  A001  Lisbonne     35
3  D031  Lisbonne     35
8  I082  Lisbonne     35

Côté performance, nlargest et nsmallest sont plus rapides que sort_values() suivi de head(), puisqu'ils n'ont pas besoin de trier toute la colonne. D'après mes mesures, ils sont de 3 à 5 fois plus rapides selon la version de Pandas :

import time
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
grand = pd.DataFrame({'score': rng.normal(size=1_000_000)})

debut = time.perf_counter()
grand.nlargest(3, columns='score')
print(f"nlargest        : {time.perf_counter() - debut:.3f} s")

debut = time.perf_counter()
grand.sort_values(by='score', ascending=False).head(3)
print(f"sort_values+head: {time.perf_counter() - debut:.3f} s")
Python
nlargest        : 0.016 s
sort_values+head: 0.062 s

Groupby et agrégation

12. Comment compter les occurrences de chaque valeur avec value_counts() ?

La méthode value_counts() compte le nombre d'occurrences de chaque valeur d'une colonne. Cette méthode donne la distribution des valeurs d'une colonne. Quelques arguments utiles :

  • normalize=True : la fréquence au lieu du nombre
  • dropna=False : inclut les valeurs manquantes dans le décompte
  • .reset_index() : convertit le résultat en DataFrame
  • .sort_index() : trie par valeurs distinctes plutôt que par fréquence
print(df['ville'].value_counts())
print()
print(df['ville'].value_counts(normalize=True))
Python
ville
Lisbonne    4
Paris       3
Zurich      3
Name: count, dtype: int64

ville
Lisbonne    0.4
Paris       0.3
Zurich      0.3
Name: proportion, dtype: float64
import numpy as np
import pandas as pd

s = pd.Series(['a', 'b', 'a', np.nan, 'a', 'b', np.nan])
print(s.value_counts(dropna=False))
print()
print(df['ville'].value_counts().reset_index())
print()
print(df['maths'].value_counts().sort_index().head(4))
Python
a      3
b      2
NaN    2
Name: count, dtype: int64

      ville  count
0  Lisbonne      4
1     Paris      3
2    Zurich      3

maths
35    3
47    1
58    1
64    1
Name: count, dtype: int64

13. Comment extraire un groupe précis avec groupby().get_group() ?

Pour extraire un groupe précis, crée l'objet GroupBy une seule fois, puis appelle get_group('valeur') : c'est utile surtout quand tu extrais plusieurs groupes. Au lieu de coller tout de suite une agrégation à groupby(), tu peux garder l'objet GroupBy et le réutiliser. get_group() récupère alors le sous-ensemble d'un groupe précis :

par_ville = df.groupby('ville')
print(par_ville.get_group('Lisbonne')[['ID', 'maths']])
Python
     ID  maths
0  A001     35
3  D031     35
5  F058     64
8  I082     35

Attention à ne pas surestimer cette astuce : pour extraire un seul groupe, un filtre booléen classique (df[df['ville'] == 'Lisbonne']) est même un peu plus rapide, car le groupby doit d'abord tout calculer. L'intérêt apparaît quand tu extrais plusieurs groupes d'un même objet GroupBy : les groupes sont calculés une seule fois. Sur un million de lignes et cinq villes, j'obtiens plus de deux fois moins de temps :

import time
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
villes = ['Lisbonne', 'Paris', 'Zurich', 'Genève', 'Berlin']
grand = pd.DataFrame({'ville': rng.choice(villes, 1_000_000), 'x': rng.normal(size=1_000_000)})

debut = time.perf_counter()
masques = [grand[grand['ville'] == v] for v in villes]
print(f"5 filtres booléens          : {time.perf_counter() - debut:.3f} s")

debut = time.perf_counter()
groupes = grand.groupby('ville')
sous_ensembles = [groupes.get_group(v) for v in villes]
print(f"1 groupby + 5 get_group     : {time.perf_counter() - debut:.3f} s")
Python
5 filtres booléens          : 0.373 s
1 groupby + 5 get_group     : 0.152 s

14. Comment calculer une somme cumulée par groupe ?

Pour une somme cumulée par groupe, utilise df.groupby('groupe')['valeur'].cumsum(). cumsum calcule la somme cumulée d'une colonne, mais sans tenir compte des groupes. Pour une somme cumulée par groupe, passe par groupby :

ventes = pd.DataFrame({
    'groupe': ['A', 'A', 'B', 'A', 'B', 'B'],
    'valeur': [10, 20, 5, 30, 15, 25],
})
ventes['cumul'] = ventes['valeur'].cumsum()
ventes['cumul_par_groupe'] = ventes.groupby('groupe')['valeur'].cumsum()
print(ventes)
Python
  groupe  valeur  cumul  cumul_par_groupe
0      A      10     10                10
1      A      20     30                30
2      B       5     35                 5
3      A      30     65                60
4      B      15     80                20
5      B      25    105                45

15. Comment calculer plusieurs statistiques à la fois avec agg() ?

Il existe de nombreuses fonctions d'agrégation (moyenne, somme, décompte…). Pour en calculer plusieurs d'un coup, par exemple la moyenne et le décompte pour repérer des moyennes calculées sur trop peu de lignes, utilise agg :

print(df.groupby('ville')['maths'].agg(['mean', 'count']))
Python
           mean  count
ville
Lisbonne  42.25      4
Paris     81.00      3
Zurich    57.00      3

Tu peux aussi nommer les colonnes du résultat avec l'agrégation nommée, ce qui évite les noms de colonnes à deux niveaux :

resume = df.groupby('ville').agg(
    maths_moy=('maths', 'mean'),
    sciences_moy=('sciences', 'mean'),
    eleves=('ID', 'count'),
)
print(resume.round(1))
Python
          maths_moy  sciences_moy  eleves
ville
Lisbonne       42.2          42.5       4
Paris          81.0          77.3       3
Zurich         57.0          64.3       3

16. Comment créer des groupes de percentiles avec qcut() ?

Pour classer les valeurs d'une colonne numérique en groupes selon des seuils de percentile (par exemple les 5 % meilleures, de 80 à 95 %, de 50 à 80 % et les 50 % du bas), pd.qcut fait exactement ce travail, sans boucle ni apply. Tu donnes les quantiles voulus dans q et des étiquettes dans labels :

rng = np.random.default_rng(0)
notes = pd.DataFrame({'c': rng.normal(size=1000)})

notes['groupe'] = pd.qcut(
    notes['c'],
    q=[0, 0.5, 0.8, 0.95, 1],
    labels=['bas 50 %', '50-80 %', '80-95 %', 'top 5 %'],
)
print(notes['groupe'].value_counts().sort_index())
Python
groupe
bas 50 %    500
50-80 %     300
80-95 %     150
top 5 %      50
Name: count, dtype: int64

Ne confonds pas qcut (intervalles définis par des quantiles, donc des groupes d'effectifs voulus) et cut (intervalles de largeur choisie, voir l'astuce 22).

Dates

17. Comment regrouper des dates par mois ou par trimestre avec to_period() ?

La méthode dt.to_period() convertit une date en période (mois, trimestre, année…), ce qui permet de regrouper les dates par période. On manipule les dates avec des intervalles variés (jour, semaine, mois, trimestre…). Les fonctions de dates sont regroupées sous l'accesseur dt. La méthode to_period convertit une date en période, ce qui permet par exemple de distinguer décembre 2020 de décembre 2021, ce que .dt.month seul ne permet pas :

dates = pd.DataFrame({'date': pd.to_datetime(['2020-12-15', '2021-12-03', '2021-02-20', '2021-03-14', '2021-12-28'])})
dates['mois_seul'] = dates['date'].dt.month
dates['mois'] = dates['date'].dt.to_period('M')
dates['trimestre'] = dates['date'].dt.to_period('Q')
print(dates)
print()
print(dates.groupby('mois').size())
Python
        date  mois_seul     mois trimestre
0 2020-12-15         12  2020-12    2020Q4
1 2021-12-03         12  2021-12    2021Q4
2 2021-02-20          2  2021-02    2021Q1
3 2021-03-14          3  2021-03    2021Q1
4 2021-12-28         12  2021-12    2021Q4

mois
2020-12    1
2021-02    1
2021-03    1
2021-12    2
Freq: M, dtype: int64

Performance et mémoire

18. Comment parcourir un DataFrame ligne par ligne rapidement ?

Pour parcourir un DataFrame ligne par ligne, itertuples() est plus de 40 fois plus rapide que iterrows() dans mon test ; une opération vectorisée est encore meilleure. Si tu dois vraiment parcourir un DataFrame ligne par ligne (à éviter en général), itertuples() est nettement plus rapide que iterrows() : iterrows() fabrique une série Pandas pour chaque ligne, alors que itertuples() renvoie de simples tuples nommés. Et quand c'est possible, la vectorisation est encore bien meilleure :

import time
import numpy as np
import pandas as pd

d = pd.DataFrame({'Nombre': np.random.default_rng(0).integers(0, 100, 200_000)})

debut = time.perf_counter()
total = 0
for _, ligne in d.iterrows():
    total += ligne['Nombre']
print(f"iterrows   : {time.perf_counter() - debut:.2f} s  (total = {total})")

debut = time.perf_counter()
total = 0
for ligne in d.itertuples(index=False):
    total += ligne.Nombre
print(f"itertuples : {time.perf_counter() - debut:.2f} s  (total = {total})")

debut = time.perf_counter()
total = d['Nombre'].sum()
print(f"sum()      : {time.perf_counter() - debut:.4f} s  (total = {total})")
Python
iterrows   : 3.11 s  (total = 9898742)
itertuples : 0.07 s  (total = 9898742)
sum()      : 0.0004 s  (total = 9898742)

Sur un DataFrame d'un million de lignes, j'ai mesuré environ 12 secondes pour iterrows(), 0,2 seconde pour itertuples() et moins d'une milliseconde pour sum().

19. Comment réduire la mémoire d'une colonne de texte avec le type category ?

Le type category stocke chaque valeur distincte d'une colonne de texte une seule fois : il divise la mémoire par 60 dans l'exemple ci-dessous. Les colonnes de texte contenant peu de valeurs distinctes (villes, pays, catégories…) peuvent prendre le type category, qui stocke chaque valeur distincte une seule fois. Attention : pour mesurer la mémoire d'une colonne de texte, il faut passer deep=True à memory_usage, sinon Pandas ne compte que les pointeurs (8 Mo ici au lieu de 60 Mo).

import numpy as np
import pandas as pd

villes = np.random.default_rng(0).choice(['Lisbonne', 'Paris', 'Zurich', 'Genève', 'Berlin'], 1_000_000)
s_obj = pd.Series(villes, dtype=object)
s_cat = s_obj.astype('category')

print(f"object   : {s_obj.memory_usage(deep=True) / 1e6:.1f} Mo")
print(f"category : {s_cat.memory_usage(deep=True) / 1e6:.1f} Mo")
Python
object   : 60.0 Mo
category : 1.0 Mo

Pour aller plus loin, lis mon article sur les 7 techniques d'optimisation de la mémoire avec Pandas.

Nettoyage et export

20. Comment compter les valeurs manquantes par ligne ?

Pour compter les valeurs manquantes par ligne (utile pour exclure les lignes trop incomplètes avant de construire un modèle), combine isnull() et sum(axis=1) :

dfn = pd.DataFrame({
    'c1': [1, np.nan, 3, np.nan],
    'c2': [5, np.nan, np.nan, np.nan],
    'c3': [7, 8, 9, np.nan],
})
dfn['nb_manquants'] = dfn[['c1', 'c2', 'c3']].isnull().sum(axis=1)
print(dfn)
print()
print(dfn[dfn['nb_manquants'] < 3])
Python
    c1   c2   c3  nb_manquants
0  1.0  5.0  7.0             0
1  NaN  NaN  8.0             2
2  3.0  NaN  9.0             1
3  NaN  NaN  NaN             3

    c1   c2   c3  nb_manquants
0  1.0  5.0  7.0             0
1  NaN  NaN  8.0             2
2  3.0  NaN  9.0             1

21. Comment exporter proprement un DataFrame avec to_csv() ?

Sans nom de fichier, to_csv() renvoie le texte du fichier : pratique pour prévisualiser ce qui sera écrit avant d'exporter. index=False évite d'écrire l'index.

print(df[['ID', 'ville', 'maths']][:3].to_csv(index=False))
Python
ID,ville,maths
A001,Lisbonne,35
B014,Paris,72
C022,Zurich,58

Autre piège : une colonne qui contient des entiers et des valeurs manquantes devient automatiquement une colonne de décimaux (float), et tes entiers sont exportés avec un « .0 » gênant. On voit parfois conseillé d'ajouter float_format='%.0f', mais attention : cet argument s'applique à toutes les colonnes décimales et les arrondit toutes, y compris celles qui contiennent de vraies décimales (la taille 1,72 devient 2) :

manq = pd.DataFrame({'id': [1, 2, 3], 'age': [25, np.nan, 31], 'taille': [1.72, 1.65, 1.80]})
print(manq.dtypes)
print()
print(manq.to_csv(index=False))
print(manq.to_csv(index=False, float_format='%.0f'))
Python
id          int64
age       float64
taille    float64
dtype: object

id,age,taille
1,25.0,1.72
2,,1.65
3,31.0,1.8

id,age,taille
1,25,2
2,,2
3,31,2

La bonne solution est de convertir seulement la colonne concernée en entier nullable (Int64, avec un I majuscule), qui accepte les valeurs manquantes :

manq['age'] = manq['age'].astype('Int64')
print(manq.dtypes)
print()
print(manq.to_csv(index=False))
Python
id          int64
age         Int64
taille    float64
dtype: object

id,age,taille
1,25,1.72
2,,1.65
3,31,1.8

22. Comment découper des valeurs en intervalles avec cut() ?

La fonction cut() découpe des valeurs continues en intervalles discrets. Le plus simple : demander un nombre d'intervalles de même largeur.

print(pd.cut(df['maths'], bins=2).head(4))
Python
0    (34.945, 62.5]
1      (62.5, 90.0]
2    (34.945, 62.5]
3    (34.945, 62.5]
Name: maths, dtype: category
Categories (2, interval[float64, right]): [(34.945, 62.5] < (62.5, 90.0]]

Ou définir toi-même les bornes :

print(pd.cut(df['maths'], bins=[0, 50, 100]).head(4))
Python
0      (0, 50]
1    (50, 100]
2    (50, 100]
3      (0, 50]
Name: maths, dtype: category
Categories (2, interval[int64, right]): [(0, 50] < (50, 100]]

Ou, mieux, donner des étiquettes explicites plutôt que des intervalles bruts :

df['resultat'] = pd.cut(df['maths'], bins=[0, 50, 100], labels=['Échec', 'Réussite'])
print(df[['ID', 'maths', 'resultat']].head(5))
print()
print(df['resultat'].value_counts())
Python
     ID  maths  resultat
0  A001     35     Échec
1  B014     72  Réussite
2  C022     58  Réussite
3  D031     35     Échec
4  E047     90  Réussite

resultat
Réussite    6
Échec       4
Name: count, dtype: int64

Deux pièges à connaître : les intervalles sont ouverts à gauche et fermés à droite, donc la valeur exactement égale à la borne inférieure (0 ici) n'est dans aucun intervalle ; et les valeurs hors des bornes (101) donnent NaN. Ajoute include_lowest=True pour inclure la borne inférieure :

notes = pd.Series([0, 25, 50, 75, 101])
print(pd.cut(notes, bins=[0, 50, 100], labels=['A', 'B']).tolist())
print(pd.cut(notes, bins=[0, 50, 100], labels=['A', 'B'], include_lowest=True).tolist())
Python
[nan, 'A', 'A', 'B', nan]
['A', 'A', 'A', 'B', nan]

Questions fréquentes sur les astuces Pandas

iterrows() ou itertuples() : lequel utiliser ?

itertuples(). Dans mon test sur 200 000 lignes, il est plus de 40 fois plus rapide que iterrows(). Mais la meilleure option reste l'opération vectorisée (sum(), max(axis=1), comparaisons…), qui évite toute boucle.

nlargest() est-il plus rapide que sort_values().head() ?

Oui. Comme il n'a pas besoin de trier toute la colonne, nlargest() est de 3 à 5 fois plus rapide dans mes mesures sur un million de lignes, avec Pandas 2.3 et 3.0.

df2 = df1 crée-t-il une copie d'un DataFrame ?

Non. Cette instruction donne un deuxième nom au même objet : modifier df2 modifie aussi df1. Pour obtenir une copie indépendante, utilise df1.copy().

Comment réduire la mémoire d'un gros DataFrame ?

Lis seulement les colonnes utiles avec usecols, choisis des types plus légers avec dtype (int32, float32) et convertis les colonnes de texte à peu de valeurs distinctes en category. Dans mon exemple de 200 000 lignes, la mémoire passe de 17,5 Mo à 1,8 Mo.

Quelle différence entre cut() et qcut() ?

cut() découpe selon des bornes de valeurs (par exemple 0-50 et 50-100). qcut() découpe selon des quantiles, pour obtenir des groupes d'effectifs voulus (par exemple les 5 % meilleurs).

Conclusion

Tu as maintenant 22 astuces Pandas testées pour lire, filtrer, regrouper et exporter tes données plus efficacement. Pour aller plus loin, tu peux lire mon article sur les 12 techniques de manipulation de données avec Pandas.

Connaître ces astuces, c'est bien ; les avoir dans les doigts, c'est mieux. Sur DataXp, le parcours « Analyse et visualisation des données » te fait manipuler des DataFrames sur des exercices guidés, avec un coach IA qui t'aide quand tu bloques. Les premiers modules sont gratuits.

Pour aller plus loin avec Pandas

Publications similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *