Tous les cours
Les données · Palier 1 · Lire les données — Voir le parcours

Quiz d'entraînement : les trois quarts des bonnes réponses pour le réussir.

Python pour les données : le DataFrame

En science des données, l'outil de tous les jours s'appelle pandas, et son objet central le DataFrame : un tableau en mémoire qu'on lit dans un fichier, qu'on filtre, qu'on regroupe et qu'on joint en quelques lignes de Python. Il ressemble à une feuille de calcul et à une table SQL. Il n'est ni l'une ni l'autre, et c'est là que naissent les erreurs : un type deviné de travers, un calcul qui s'aligne sur des étiquettes, un filtre qui garde ce qu'on voulait écarter, une écriture qui tombe dans le vide.

Ce cours donne le modèle mental qui évite la plupart d'entre elles, à travers six situations que l'on rencontre dès la première semaine avec pandas. Pour qui : tu sais lire un peu de Python. Si tu as vu SQL, tu reconnaîtras les opérations, et tu verras où pandas répond autrement. Rien à installer : une console pandas t'attend en fin de parcours.

Le fil du cours : le catalogue d'une appli de streaming imaginaire. Huit titres, avec leur genre, leur année, une note et un nombre de vues. Les titres et les chiffres sont inventés ; les résultats des figures, eux, sont ceux de vrai pandas (versions 3.0.6 et 2.2.2) et de SQLite, rejoués par un script. Chaque bloc marqué « Figure interactive » réagit : clique, écris, regarde ce qui change.


Le modèle mental : des colonnes alignées sur un index

Un DataFrame, ce sont des colonnes, chacune une Series d'un seul type (int64, float64, texte, date…), qui partagent toutes les mêmes étiquettes de lignes : l'index. Une feuille de calcul laisse chaque cellule choisir son type ; ici, c'est la colonne qui le porte. On ne travaille donc pas ligne par ligne, mais colonne par colonne, d'un coup :

import pandas as pd

df = pd.read_csv("catalogue.csv", sep=";")
df.dtypes                                     # le type de chaque colonne
df["vues_par_point"] = df["vues"] / df["note"]    # toute la colonne, en une opération

La dernière ligne ne fait pas de boucle : elle divise deux colonnes entières, étiquette par étiquette. C'est rapide, et c'est sûr… tant qu'on garde les étiquettes. Presque tous les pièges du cours en découlent : le type d'une colonne est deviné à la lecture, les étiquettes décident de qui se combine avec qui, et une valeur manquante, le NaN, voyage dans les calculs au lieu de les arrêter.

L'index n'est pas une colonne comme les autres : c'est lui qui désigne une ligne. Par défaut, ce sont les numéros 0, 1, 2…, mais on peut y mettre les titres (df.set_index("titre")). Deux façons de lire une ligne en découlent : df.loc["Orbite"] va chercher une étiquette, df.iloc[0] la ligne à une position. Cette différence, étiquette contre position, est celle qui revient dans tout le cours, et elle explique pourquoi pandas peut ranger les lignes dans un autre ordre sans rien casser.


Lire un fichier : les types se devinent

Un fichier CSV est du texte : il ne dit pas si 0042 est un nombre ou un code. read_csv regarde donc chaque colonne et devine. Si toutes les valeurs sont des entiers, la colonne est int64 ; si certaines ont une décimale, float64 ; True et False donnent des booléens ; tout le reste est du texte (str depuis pandas 3.0, sorti le 21 janvier 2026 ; object avant). Une valeur vide devient NaN, qui est un flottant : une colonne d'entiers qui en reçoit un bascule donc en float64. Les dates, elles, restent du texte tant qu'on ne le demande pas.

Un fichier français ajoute ses habitudes : le point-virgule comme séparateur (sep=";") et la virgule comme virgule décimale (decimal=","). Voici un extrait du catalogue, avec un identifiant 0042 qui est un code, pas un nombre. Comment obtenir les quatre bons types : l'identifiant en texte, la note en nombre décimal, les vues en nombre, la date en date ? Branche les options une à une jusqu'à ce que plus aucune colonne ne soit mal lue. Puis casse le fichier toi-même : vide une case, mets un point à la place d'une virgule, ajoute un mot dans une colonne de nombres.

Figure interactiveLire un CSV : quel type pandas devine-t-il ?Écris dans le fichier, puis branche les options de read_csv une à une. Le but : qu'aucune colonne ne soit mal lue.
idnotevuessortie
12019-03-01
22015-11-20
32021-05-12
42012-09-30
Options de read_csv
idint6442731058notestr7,48,1NaN5,9vuesfloat64120.045.080.0NaNsortiestr2019-03-012015-11-202021-05-122012-09-30
Colonnes mal lues3 / 4à corriger

L'identifiant est lu comme un nombre : « 0042 » devient 42. Les zéros de tête sont perdus, sans erreur ni avertissement. La note est restée du texte : « 7,4 » n'est pas un nombre pour pandas, qui attend un point, tant que decimal="," ne lui dit pas que la virgule est décimale. Les vues sont flottantes : une case vide est un NaN, NaN est un flottant, et toute la colonne a basculé. La date reste du texte (str) : sans parse_dates, pandas ne la convertit pas.

pd.read_csv("catalogue.csv",
    sep=";")
id;note;vues;sortie
0042;7,4;120;2019-03-01
0007;8,1;45;2015-11-20
0310;;80;2021-05-12
0058;5,9;;2012-09-30

Un extrait du catalogue (titres et chiffres inventés), lu comme pd.read_csv avec sep=";". Le moteur est comparé au vrai pandas 3.0.6 sur plus de quatre-vingts colonnes d'essai. La colonne des dates est figée : pandas accepte beaucoup d'écritures de dates que la figure ne reproduit pas.

Sans option, l'identifiant est devenu 42 : pandas l'a pris pour un entier et a perdu les zéros de tête, sans erreur ni avertissement. La note est restée du texte, parce que 7,4 n'est pas un nombre pour pandas tant qu'on ne lui dit pas que la virgule est décimale. Les vues sont devenues flottantes à cause d'une seule case vide, et la date n'est qu'un texte. D'où l'habitude à prendre : juste après read_csv, regarder df.dtypes (ou df.info()) avant de calculer quoi que ce soit, et préciser dtype, decimal et parse_dates plutôt que de laisser pandas deviner. Une précision : si une seule date est illisible, parse_dates laisse toute la colonne en texte, sans rien dire.


Les opérations s'alignent sur l'index

Quand on combine deux Series, pandas fait correspondre les étiquettes avant de calculer. La documentation le dit : l'alignement est « intrinsèque », et le lien entre étiquettes et données ne se rompt que si on le rompt soi-même. Si une étiquette manque d'un côté, le résultat porte l'union des deux index et marque NaN ce qui ne peut pas être calculé.

Prenons les vues de quatre titres et l'objectif de vues de chacun, et calculons l'écart. L'objectif est rangé dans un autre ordre que les vues. Orbite reçoit-elle bien son propre objectif ? Compare ensuite avec .values, qui rend un tableau NumPy sans étiquettes.

Figure interactivePar étiquette ou par positionChange l'ordre de l'objectif, puis le mode de calcul. Regarde les fils.
L'ordre de l'objectif
Le calcul
ecart = vues.values - objectif.values
Orbite120Le Phare45Minuit80Écho15Minuit50Écho20Orbite100Le Phare50Orbite70Le Phare25Minuit-20Écho-35
Justes0écarts
Faux4en silence
Manquants0titres sans partenaire

Les tableaux nus se soustraient case par case : 4 écarts sont faux, sans erreur ni avertissement. Les titres cerclés d'or sont reliés à un titre différent : Orbite reçoit l'objectif d'un autre. C'est l'erreur la plus difficile à voir, parce que le résultat a l'air normal.

Les vues de quatre titres moins leur objectif (titres et chiffres inventés). Un fil relie chaque valeur à son résultat ; un titre cerclé d'or est relié à un titre différent. Résultats vérifiés contre pandas 3.0.6 et 2.2.2, et contre NumPy.

Par étiquette, les fils se croisent : pandas retrouve chaque titre avant de soustraire, et range le résultat par ordre alphabétique quand les index diffèrent. Par position, les fils restent droits, mais ils relient des titres différents : les écarts sont faux, sans aucun avertissement, et le résultat a l'air normal (quand une ligne tombe juste par hasard, c'est pire encore : on croit que tout va bien). Quand un titre change, pandas garde l'union et affiche NaN : les trous se voient. Deux réflexes en découlent : garder les étiquettes (pas de .values pour calculer entre deux tables), et regarder les NaN qu'un alignement fait apparaître, car ils signalent des lignes qui ne se correspondent pas. Quand ces trous sont attendus, on choisit ce qu'on en fait : vues.sub(objectif, fill_value=0) compte un objectif absent pour zéro, reindex impose l'ordre et les étiquettes voulus, et dropna() écarte les lignes sans partenaire. Dans tous les cas, c'est une décision écrite, plutôt qu'un hasard d'ordre.


Filtrer, regrouper, joindre

Les trois opérations de base ressemblent à du SQL, et gardent les mêmes pièges :

bien_notes = df[df["note"] > 7]                                 # filtrer
df[(df["genre"] == "drame") & (df["annee"] >= 2018)]            # deux conditions : & et des parenthèses
df.groupby("genre")["vues"].sum()                               # regrouper
df.merge(abonnes, on="titre", how="left", validate="one_to_one")  # joindre

Filtrer, c'est calculer un masque : df["note"] > 7 est une Series de booléens, une valeur par ligne, et df[masque] garde les lignes True. Pour combiner deux conditions on écrit &, | et ~, jamais and, or ni not, et on entoure chaque condition de parenthèses, parce que & passe avant les comparaisons. Une jointure vers une table où une clé apparaît plusieurs fois multiplie les lignes (voir SQL : interroger une base) : l'option validate de merge vérifie que la jointure est du type attendu, et lève une erreur MergeError sinon. Deux mots de plus sur la jointure : how="left" garde tous les titres de la table de gauche, même sans correspondance, avec NaN dans les colonnes de droite ; how="inner" ne garde que les titres présents des deux côtés. Si abonnes compte deux lignes pour un même titre, ce titre apparaît deux fois dans le résultat, et la somme des vues double sans prévenir : c'est exactement le cas que validate attrape. Après un filtre, les lignes gardées conservent leurs étiquettes d'origine : l'index n'est pas renuméroté, et c'est ce qui permet à l'alignement de les retrouver.


Les valeurs manquantes : NaN traverse tout

pandas marque une valeur manquante par NaN (ou None, NaT pour les dates). Comme NULL en SQL, elle se teste avec isna(), jamais avec == : NaN == NaN vaut False. Dans notre catalogue, deux titres n'ont pas encore de note. Un collègue veut les titres notés plus de 7, et écrit le contraire de « au plus 7 » : ~(df["note"] <= 7). Donne-t-il la même liste que df["note"] > 7 ? Parie, puis regarde le masque ligne par ligne, et essaie les autres écritures.

Figure interactiveLe masque d'un filtre, et le NaNParie d'abord. Ensuite, choisis un filtre et lis le masque, ligne par ligne.

Deux titres du catalogue n'ont pas de note (NaN). Les filtres df["note"] > 7 et df[~(df["note"] <= 7)] donnent-ils la même liste ?

Huit titres du catalogue (inventés), dont deux sans note. T = True, F = False ; un rond plein = ligne gardée. Masques vérifiés contre pandas 3.0.6.

Les deux écritures ne sont pas équivalentes. Une comparaison avec NaN vaut toujours False : NaN > 7 est faux, et NaN <= 7 l'est aussi. Le ~ retourne ce deuxième False en True, et les deux titres sans note passent le filtre. De là deux règles : écrire les filtres comme des conditions positives (> 7), qui écartent aussi les valeurs manquantes, ou ajouter explicitement .notna() & ; et chercher les manquants avec isna(), car == None ne trouve jamais rien. Les statistiques, elles, sautent les NaN : mean() fait la moyenne des notes présentes (7,23 sur six notes). fillna(0) n'est pas anodin : la moyenne tombe à environ 5,4, parce que deux titres sans note comptent maintenant pour une note de zéro. Enfin, une colonne d'entiers qui reçoit un NaN devient flottante ; les types « nullables » (Int64) l'évitent.


Regrouper quand des valeurs manquent

groupby pose la même question deux fois : que faire d'une ligne dont la clé manque, et d'un groupe dont les valeurs manquent ? Et pandas ne répond pas comme SQL. Voici six titres, dont un documentaire : le genre de l'un est inconnu, et la note de deux autres manque. Falaise est le seul titre de son genre. Que vaut la somme de son groupe ? Parie, puis compare pandas et SQL pour chaque agrégat.

Figure interactivegroupby contre GROUP BYParie d'abord. Ensuite, change l'agrégat, puis garde les clés manquantes.

Falaise est le seul titre de son genre, et sa note manque. Que vaut df.groupby("genre")["note"].sum() pour ce genre ?

Six titres (inventés), dont un documentaire. Le genre de Roue libre est inconnu ; la note de Minuit et celle de Falaise manquent. La même question posée à pandas (groupby) et à SQL (GROUP BY). Résultats vérifiés contre pandas 3.0.6 et SQLite 3.54.

La somme d'un groupe sans aucune valeur vaut 0 pour pandas, et NULL pour SQL. La moyenne d'un tel groupe est NaN chez pandas, NULL chez SQL : c'est la même chose. count() compte les valeurs présentes, size() compte les lignes, trous compris. Et la ligne dont le genre manque a disparu du résultat : par défaut, groupby écarte les clés manquantes (dropna=True), alors que SQL en fait un groupe. Avec dropna=False, le genre manquant redevient un groupe. Pour obtenir NaN plutôt que 0 sur une somme vide, on ajoute min_count=1. Même question, deux réponses : c'est la raison pour laquelle on vérifie, sur un petit exemple, ce qu'un regroupement fait des manquants avant de lui confier un vrai fichier.


Écrire sans casser : .loc et les copies

On veut marquer d'une alerte les titres qui dépassent 50 vues. Deux écritures semblent dire la même chose. La première enchaîne deux opérations : df[df["vues"] > 50] d'abord, puis ["alerte"] = True sur le résultat. La seconde fait tout en un appel : df.loc[df["vues"] > 50, "alerte"] = True. Une seule des deux modifie df. Laquelle ? Parie à l'aveugle, puis regarde où chaque écriture est tombée.

Figure interactiveDeux écritures, une seule modifie dfParie à l'aveugle, puis regarde où chaque écriture est tombée.

A

df[df["vues"] > 50]["alerte"] = True
dftitrevuesOrbite120Le Phare45Minuit80Écho15

B

df.loc[df["vues"] > 50, "alerte"] = True
dftitrevuesOrbite120Le Phare45Minuit80Écho15

Les deux panneaux marquent d'une alerte les titres de plus de 50 vues. Lequel modifie vraiment df ?

Pour l'instant, les deux écritures disent la même chose : marquer d'une alerte les titres de plus de 50 vues. Rien ne les distingue à la lecture, et aucune ne fait d'erreur. Fais ton pari.

Quatre titres (inventés). Résultats relevés avec pandas 3.0.6 ; avec pandas 2.2.2, la première écriture ne modifie pas non plus df, mais prévient par un SettingWithCopyWarning.

Filtrer avec un masque fabrique un nouveau DataFrame, une copie. Le ["alerte"] = True de la chaîne écrit dans cette copie temporaire, qui disparaît aussitôt : df n'a pas bougé. .loc écrit directement dans df, en une seule opération. Avant pandas 3.0, la situation était trompeuse : dans pandas 2.2, la forme df["alerte"][masque] = True (la colonne d'abord) modifiait bien df, avec un avertissement, et la forme du dessus, jamais. Depuis pandas 3.0, publié le 21 janvier 2026, chaque étape d'indexation se comporte comme une copie (le Copy-on-Write) : l'affectation chaînée ne marche plus nulle part, et pandas le signale par un ChainedAssignmentError. La règle tient en une phrase : on écrit en une seule opération, avec df.loc[lignes, colonne] = valeur. Les lignes qui ne reçoivent pas la valeur gardent NaN : on initialise la colonne d'abord (df["alerte"] = False) si l'on veut un vrai booléen.


À la console : tes propres expressions

Un petit tableau df, le catalogue des huit titres, et une série objectif sont chargés dans une console. Choisis une expression dans la liste, modifie-la, ou écris la tienne ; quatre missions se cochent seules. Question avant de commencer : pour garder les drames sortis depuis 2018, où faut-il mettre les parenthèses ?

Figure interactiveConsole pandas : le catalogue d'une appli de streamingTape une expression pandas, ou choisis-en une dans la liste et modifie-la. Les missions se cochent seules.
Les données : df (titre, genre, annee, note, vues) et objectif
df
        titre    genre  annee  note  vues
0      Orbite       sf   2019   8.1   120
1    Le Phare    drame   2015   7.4    45
2      Minuit    drame   2021   NaN    80
3  Roue libre     None   2018   6.2    30
4     La Dune       sf   2021   8.8   200
5        Écho  comédie   2012   5.9    15
6    Sous-sol    drame   2020   7.0    60
7     Vertige  comédie   2016   NaN    40

objectif : 0 → 100, 1 → 50, 2 → 50, 3 → 50, 9 → 10

  1. À faire : Calcule la note moyenne : que deviennent les films sans note ?
  2. À faire : Trouve les drames sortis depuis 2018 (attention aux parenthèses).
  3. À faire : Fais le total des vues par genre.
  4. À faire : Ajoute l'objectif aux vues : où apparaissent les NaN, et pourquoi ?

Évaluateur pandas jouet sur un tableau df (8 films) et une série objectif : sélection, filtres, .loc, NaN, alignement sur l'index, groupby. Ses résultats sont identiques à ceux du vrai pandas sur plus de 50 expressions de test ; l'affichage imite celui de pandas. Pas d'affectation ni d'apply.

Les résultats sont ceux du vrai pandas sur plus de cinquante expressions de test, et l'affichage imite le sien. Le piège le plus fréquent est celui des parenthèses : & passe avant == et >=. Sans parenthèses, pandas lève une erreur ; avec une parenthèse sur deux, il rend un tableau vide sans rien dire, ce qui est pire. Entoure chaque condition de parenthèses, toujours. Essaie aussi ce que le cours vient de montrer : un filtre avec ~, une somme par genre avec dropna=False, l'alignement de df["vues"] + objectif quand l'index de objectif ne correspond pas.


Ce qu'on oublie souvent

  • read_csv devine, il ne sait pas. Un code postal 01000 devient le nombre 1000 : on précise dtype={"code_postal": str}. Les dates restent du texte ; 01/03/2019 est lu par pandas comme le 3 janvier (le mois d'abord), à moins de donner format="%d/%m/%Y" à pd.to_datetime.
  • Regarder avant de calculer. df.info() donne les types et le nombre de valeurs présentes par colonne, df.describe() les statistiques, et df["genre"].value_counts(dropna=False) compte aussi les manquants, que value_counts() cache par défaut.
  • Les boucles sur les lignes sont lentes. for … in df.iterrows() traite une ligne à la fois en Python ; on calcule sur des colonnes entières, qui s'exécutent en code compilé.
  • Plus besoin de .copy() défensif. Depuis pandas 3.0, une extraction se comporte toujours comme une copie : les .copy() posés pour faire taire SettingWithCopyWarning n'ont plus d'objet, et cet avertissement n'existe plus.
  • Un tri ou un filtre ne modifie pas le tableau. df.sort_values("note") renvoie un nouveau DataFrame : sans affectation, le résultat est perdu.
  • Le texte répété coûte cher. Une colonne comme genre, qui répète quelques valeurs des milliers de fois, tient dans bien moins de mémoire en type category.
  • Tout tient en mémoire. Un DataFrame vit dans la RAM. Pour un fichier plus gros, on ne lit que les colonnes utiles (usecols), on lit par morceaux (chunksize), ou on interroge les données là où elles sont, en SQL.
  • NumPy n'a pas d'étiquettes. df.to_numpy() (comme .values) rend un tableau nu : rapide, mais sans étiquettes, donc sans alignement.

Teste-toi

Quelques questions sur les idées qu'on vient de croiser. La raison s'affiche après chaque réponse : c'est elle qui compte.

Figure interactiveTeste-toi : le DataFrameAprès chaque réponse, la raison s'affiche.

Question 1 sur 12

9 bonnes réponses attendues sur 12.

Dans pandas, qu'est-ce qu'un DataFrame ?

Une bonne réponse par question. Ce sont les pièges qu'on rencontre dès les premières semaines avec pandas.


Le vocabulaire en un coup d'œil

TermeEn une phrase
DataFrameUn tableau en mémoire : des colonnes typées alignées sur un index.
SeriesUne colonne : des valeurs d'un seul type, avec leurs étiquettes.
IndexLes étiquettes des lignes, qui servent à aligner les calculs.
dtypeLe type d'une colonne : entier, flottant, texte, date…
Alignementpandas fait correspondre les étiquettes avant de calculer.
NaNLa valeur manquante ; elle n'est égale à rien, pas même à elle-même.
MasqueUne Series de booléens, une valeur par ligne, qui sert à filtrer.
groupbyRegrouper les lignes par valeur d'une colonne, puis les résumer.
mergeJoindre deux DataFrame sur une ou plusieurs colonnes.
VectoriserCalculer sur des colonnes entières plutôt que ligne par ligne.
.locLire ou écrire des lignes et des colonnes, en une seule opération.
Copy-on-WriteDepuis pandas 3.0, toute extraction se comporte comme une copie : l'affectation chaînée ne modifie plus rien.

Ce qu'il faut retenir

  • Un DataFrame, ce sont des colonnes typées alignées sur un index.
  • read_csv devine les types : on regarde df.dtypes tout de suite, et on précise dtype, decimal et parse_dates.
  • Les calculs entre Series s'alignent sur les étiquettes ; .values les oublie, et mélange les lignes sans prévenir.
  • Les NaN qui apparaissent après un calcul signalent des lignes sans correspondance.
  • Une comparaison avec NaN vaut toujours False : on écrit des filtres positifs, et on cherche les manquants avec isna().
  • groupby écarte par défaut les clés manquantes, et la somme d'un groupe vide vaut 0 : pas comme en SQL.
  • On écrit avec .loc, en une seule opération : l'affectation chaînée écrit dans une copie.

Pour aller plus loin

Et maintenant ?