pandas : travailler sur une table de données
Ce que ce chapitre apporte6 points
- Distinguer une table d'un tableau NumPy, et savoir quand chacune convient.
- Lire une table inconnue : dimensions, types, premières lignes, résumé statistique.
- Choisir des colonnes, filtrer des lignes par une condition, et fabriquer une colonne.
- Grouper pour comparer des sous-populations, ce qui est la question qui décide de presque tout.
- Repérer et traiter les valeurs manquantes et les libellés incohérents.
- Lire et écrire un fichier CSV, et tracer depuis une table.
Un relevé réel n'est presque jamais une suite de nombres. C'est une table : une ligne par observation, une colonne par grandeur, et des colonnes qui ne sont pas du même type. La journée du 14 mars porte une date, une température, une humidité, un nombre de locations et le nom d'une station. Un tableau NumPy ne sait pas ranger cela : il impose un type unique et ne connaît pas les noms de colonnes.
pandas ajoute les deux. Une colonne y porte un nom, chaque colonne a son type, et les opérations parlent le langage de la question posée : résumer une colonne, comparer des groupes, retrouver les lignes qui manquent. C'est l'outil que supposent le chapitre de statistiques, celui sur la corrélation et tout le parcours d'intelligence artificielle.
Une table n'est pas un tableau
Le chapitre sur NumPy range des nombres d'un seul type dans une grille anonyme, repérée par des indices. Une table range des colonnes nommées, chacune avec son propre type. Les deux objets portent le même nom courant, « tableau », et ce sont deux choses différentes.
Un DataFrame se construit ici à partir d'un dictionnaire : une clé par colonne, une liste de valeurs. La numérotation à gauche, de 0 à 5, est l'index des lignes ; il ne fait pas partie des données.
shape donne les dimensions comme en NumPy, et dtypes le type de chaque colonne : float64 pour les mesures, et object pour la salle et le capteur, qui est le nom hérité de NumPy pour « autre chose que des nombres », ici du texte. Une même table mélange donc les types, ce qu'un tableau NumPy refuse. La valeur absente de la quatrième ligne est devenue NaN, celui du chapitre sur les nombres à virgule, et c'est ainsi que pandas note un trou.
Une Series est une colonne : une suite de valeurs d'un même type, avec un index. Un DataFrame est une table : plusieurs Series qui partagent le même index.
Extraire une colonne donne une Series ; en extraire plusieurs donne un DataFrame. C'est la distinction qui explique la plupart des surprises d'affichage.
NumPy quand toutes les valeurs sont du même type et que la question est un calcul : intégrer, filtrer, faire des statistiques sur une série de mesures.
pandas quand les colonnes sont hétérogènes, portent des noms, viennent d'un fichier, et que la question mêle des catégories à des nombres : « la moyenne par salle », « les lignes où le capteur est en panne ».
Les deux cohabitent sans effort : chaque colonne numérique d'une table est un tableau NumPy, et toutes les fonctions du chapitre précédent s'y appliquent.
Le premier regard
Devant une table inconnue, trois appels avant toute chose. Ils prennent dix secondes et évitent des heures.
shape dit combien de lignes et de colonnes. head() montre les cinq premières lignes, de quoi voir à quoi ressemblent les valeurs, ce que tail() fait à l'autre bout. Et describe() produit, colonne numérique par colonne numérique, le résumé que le chapitre de statistiques calcule à la main : effectif, moyenne, écart-type, minimum, les trois quartiles, maximum.
Ce relevé est exactement celui de la section « Travailler sur des données tabulaires » du chapitre de statistiques : moyenne 23,3 °C, 1 413 locations par jour, écart-type 493, médiane 1 450. Le tableau que ce chapitre affiche, la ligne describe() le recalcule. Il n'y a donc rien à croire sur parole : le relancer, changer la graine, et regarder ce qui bouge.
Trois choses se lisent déjà dans ce résumé, et aucune ne demande de calcul. L'écart-type des locations vaut plus du tiers de leur moyenne, donc la série est dispersée. Leur médiane dépasse leur moyenne, donc la distribution est étirée vers le bas. Et le minimum, 50, est très loin du premier quartile : une poignée de journées sortent du lot, et il faudra comprendre pourquoi avant de moyenner quoi que ce soit.
describe() ignore les colonnes de texte. Pour celles-là, la question n'est pas la moyenne mais le décompte des modalités :
value_counts() compte les occurrences de chaque valeur, classées de la plus fréquente à la plus rare, et normalize=True les rend en proportions. C'est l'équivalent, pour une colonne qualitative, du tableau d'effectifs et de fréquences du cours de statistiques.
1.Quelle différence entre v["temperature"] et v[["temperature"]] ?
2.Que donne describe() sur une colonne de texte ?
Choisir des colonnes et des lignes
Trois gestes suffisent, et ils se combinent.
Le filtre releves["temp"] > 23 fabrique une colonne de True et de False, exactement le masque du chapitre NumPy, et la table n'en garde que les lignes vraies. Les conditions se combinent avec & et |, jamais and et or, et chaque comparaison se met entre parenthèses : c'est la même règle qu'en NumPy, pour la même raison.
loc demande deux choses séparées par une virgule : quelles lignes, quelles colonnes. C'est l'écriture à préférer dès qu'on veut les deux, et la seule qui permette de modifier une sélection sans mauvaise surprise. Sa voisine iloc travaille sur les positions, iloc[0, 2] étant la première ligne et la troisième colonne, comme en NumPy ; elle sert plus rarement, parce qu'une table se désigne par des noms.
locÉcrire table[table["temp"] > 23]["temp"] = 0 ne change rien à la table d'origine, et pandas se contente d'un avertissement : la première sélection a fabriqué une copie, et la modification s'applique à cette copie, jetée aussitôt.
La forme qui marche tient en une ligne : table.loc[table["temp"] > 23, "temp"] = 0. Une sélection, une affectation, sur la table elle-même.
Fabriquer une colonne
Une colonne se calcule à partir des autres, sans boucle, exactement comme un tableau NumPy.
releves["ecart"] = ... crée la colonne si elle n'existe pas, et la remplace sinon. Le calcul s'applique à toute la colonne d'un coup, et np.where choisit terme à terme entre deux valeurs selon une condition, ce qui est la façon habituelle de fabriquer une colonne de catégories à partir d'une colonne de nombres.
Grouper pour comparer
C'est la raison principale d'employer pandas, et la question qui décide de presque toutes les actions : les sous-populations se ressemblent-elles ? Une moyenne générale n'y répond jamais.
groupby("saison") découpe la table en paquets de lignes qui partagent la même saison, ["locations"] choisit la colonne à résumer, et mean() calcule une valeur par paquet. La phrase Python se lit comme la question française : grouper par saison, prendre les locations, en faire la moyenne.
agg demande plusieurs résumés à la fois, et c'est presque toujours ce qu'on veut : une moyenne sans son effectif ni sa dispersion n'apprend pas grand-chose. Ici, la moyenne monte avec la saison, ce qui n'étonne personne, mais l'écart-type descend : plus il fait chaud, plus la fréquentation est prévisible. C'est une information de dimensionnement qu'aucune moyenne ne donne.
Le second découpage est plus instructif encore. Un jour ouvré compte 1 590 locations en moyenne, contre 966 sinon, soit 624 de différence. À comparer aux 757 qui séparent la saison la plus froide de la plus chaude : le jour de la semaine pèse presque autant que la saison, alors que c'est la saison à laquelle on pense en premier.
On groupe aussi par deux colonnes, et l'on obtient alors une ligne par croisement :
pd.crosstab compte les lignes de chaque croisement : c'est le tableau à double entrée du cours de statistiques, et le premier réflexe quand deux variables qualitatives se mêlent. sort_values trie la table entière sur une colonne, ascending=False du plus grand au plus petit, et head(3) ne garde que le sommet.
agg affiche l'effectif de chaque groupe, et c'est la colonne à lire en premier. Une moyenne calculée sur trois lignes n'est pas une moyenne, c'est une anecdote, et elle s'affiche pourtant avec la même assurance que celle calculée sur deux cents.
C'est aussi ce qui fait basculer certaines conclusions : le chapitre 1 du parcours d'intelligence artificielle montre, avec le même groupby, un cas où la marque de machine qui semble la pire devient la meilleure dès qu'on regarde atelier par atelier. Rien n'a changé dans les données, seulement le découpage.
1.Que signifie v.groupby("salle")["temp"].mean() ?
2.Pourquoi préférer agg(["count", "mean"]) à mean() seul ?
Les trous
Une table réelle a des cases vides : capteur en panne, champ non saisi, mesure hors plage écartée à la collecte. pandas les note NaN, et il faut décider quoi en faire avant de calculer.
isna().sum() est le premier appel à faire sur une table inconnue : il compte les trous colonne par colonne. Les calculs de pandas les ignorent ensuite silencieusement, ce qui est commode et dangereux : la moyenne ci-dessus porte sur quatre valeurs et s'affiche comme n'importe quelle autre.
dropna() ne garde que les lignes complètes, fillna(valeur) remplit les trous. Les deux sont des décisions, pas des détails : supprimer des lignes change la population étudiée, remplir par la médiane réduit artificiellement la dispersion. Le chapitre 3 du parcours d'intelligence artificielle traite cette question pour elle-même ; ce qui compte ici est de savoir la poser, et de l'écrire dans le rapport.
Le texte d'une colonne
Les colonnes de texte arrivent sales : espaces en trop, majuscules incohérentes, deux orthographes pour la même chose. Une modalité fantôme se glisse alors dans tous les décomptes.
.str donne accès, colonne entière, aux méthodes de chaîne du chapitre sur les structures de données : strip retire les espaces de bordure, upper met en majuscules, replace remplace, contains teste. Sans ce nettoyage, la table comptait quatre salles là où il n'y en a que deux, et toute moyenne par salle était calculée sur la moitié des lignes, sans qu'aucune erreur ne soit levée.
Lire et écrire un fichier
C'est le point de départ réel : la table vient d'un fichier, presque toujours un CSV exporté d'un tableur ou d'un système de mesure.
Trois arguments règlent l'essentiel des ennuis. sep=";" parce qu'un export français sépare par des points-virgules, la virgule étant déjà prise par les décimales. decimal="," pour que 21,4 devienne le nombre 21,4 et non du texte : sans lui, la colonne reste une colonne de chaînes, et toute moyenne échoue. Et encoding="utf-8", à préciser dès qu'un accent traîne, comme au chapitre sur les fichiers.
to_csv(..., index=False) écrit la table ; sans index=False, la numérotation des lignes part dans le fichier et revient comme une colonne parasite à la relecture suivante.
read_csv devine le type de chaque colonne, et se trompe dès qu'une valeur détonne : un - mis pour « pas de mesure », une virgule décimale non déclarée, un code postal qui perd son zéro initial en devenant un nombre. La ligne à écrire juste après la lecture est donc print(table.dtypes). Une colonne de mesures annoncée comme du texte est un problème qui se règle en une seconde à cet endroit, et en une heure trois écrans plus loin.
na_values=["-", "NC"] déclare les marques de valeur absente propres au fichier, et dtype={"code": str} impose un type à une colonne.
Un CSV exporté d'un tableur français contient 21,4. Lu sans précaution, que devient cette colonne ?
Tracer depuis une table
Une table se trace avec Matplotlib, en lui passant les colonnes, puisqu'une colonne numérique est un tableau NumPy.
Le nuage de points montre ce qu'aucun tableau ne montre : la relation est nette mais large, et la dispersion autour d'elle est ce qu'une autre variable explique, le jour ouvré. corr donne le coefficient de corrélation linéaire, celui que le chapitre sur la corrélation interprète, et v.corr(numeric_only=True) le calcule pour toutes les paires de colonnes d'un coup.
Résumer chaque colonne : head, describe, isna().sum(), value_counts.
Comparer les sous-populations : groupby avec agg, en lisant l'effectif avant la moyenne.
Regarder la forme avant de croire un résumé : un nuage, un histogramme, des boîtes côte à côte.
Cet ordre n'est pas interchangeable, et c'est celui du chapitre de statistiques. Commencer par la conclusion, c'est-à-dire chercher dans les données ce qu'on espérait y trouver, produit les analyses les plus convaincantes et les plus fausses.
Quand pandas n'apporte rien
Une liste de trente nombres se traite très bien avec une liste Python ou un tableau NumPy, et importer pandas pour cela alourdit le programme sans rien simplifier. La table devient le bon outil quand les colonnes sont nommées et de types différents, quand les données viennent d'un fichier, ou quand la question contient le mot « par » : par salle, par saison, par jour de la semaine.
À mettre en pratique
Le relevé des salles. À partir de la table ci-dessous, afficher le nombre de trous par colonne, la température moyenne par salle en ignorant les valeurs manquantes, et la liste des salles dont la moyenne dépasse 23 degrés.
Afficher la solution
Le nettoyage vient en premier : sans lui, a101 et A101 comptent pour deux salles, et la moyenne de chacune porte sur une seule mesure. La colonne count est là pour cette raison, et elle montre que B204 n'a qu'une mesure exploitable sur deux : une moyenne calculée sur une valeur unique, à ne surtout pas publier sans le dire.
Pour aller plus loin : reprendre le relevé de vélos, et chercher si l'effet du jour ouvré est le même à toutes les saisons. Un groupby sur les deux colonnes suffit, et la réponse se lit en comparant les écarts d'une saison à l'autre.
Synthèse
- Un DataFrame est une table : des colonnes nommées, chacune avec son type. Une Series est une colonne. NumPy pour des nombres d'un seul type, pandas dès que les colonnes diffèrent ou viennent d'un fichier.
- Le premier regard tient en quatre appels :
shape,head(),describe()pour les colonnes numériques,value_counts()pour les qualitatives. table["col"]choisit une colonne,table[["a", "b"]]une table,table[condition]des lignes, ettable.loc[condition, "col"]les deux, y compris pour modifier.- Une colonne se calcule à partir des autres, sans boucle, comme un tableau NumPy ;
np.wherefabrique une colonne de catégories. groupbyest le cœur de pandas : découper, choisir une colonne, résumer.agg(["count", "mean", "std"])plutôt quemean()seul, et l'effectif se lit avant la moyenne.pd.crosstabcroise deux colonnes qualitatives ;sort_valuestrie la table.isna().sum()compte les trous, que les calculs ignorent silencieusement.dropnaetfillnasont des décisions qui changent les résultats, et qui s'écrivent..str.strip()et.str.upper()nettoient une colonne de texte : sans cela, une même salle compte pour deux modalités.pd.read_csv(f, sep=";", decimal=",")lit un export français ; vérifierdtypesjuste après.to_csv(..., index=False)écrit.- Une colonne numérique est un tableau NumPy : Matplotlib la trace directement.