Tutoriel sur pandas (documentation), bibliothèque python permettant d'analyser et d'interroger rapidement des données; particulièrement utile lorsqu'on a des fichiers volumineux qu'un tableur comme Excel, Calc ou Google Sheets a de la difficulté à avaler. Contenu préparé pour le EDM5240, cours de journalisme informatique de l'Université du Québec à Montréal (mars 2017; mis à jour en mars 2019), plus récemment changé pour EDM4466 (Journalisme de données II).
Le premier bloc de code, ci-dessous, ressemble au début des scripts que vous avez réalisés jusqu'à maintenant. Il s'agit d'importer les modules dont nous aurons besoin. On leur donne même un surnom. C'est ainsi que pandas
devient pd
, par exemple.
La ligne %matplotlib inline
sert simplement à demander à notre carnet d'afficher des graphiques dans la page lorsqu'on souhaitera en créer.
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib as mp
On peut changer les options d'affichage de pandas avec la fonction .set_option
.
Ici, on demande que l'affichage des nombres ne soit pas en notation scientifique, comme il l'est par défaut.
pd.options.display.float_format = "{:.2f}".format
Ci-dessous, on lit deux des fichiers que je vous ai donnés.
On les mets chacun dans une variable. J'ai choisi de baptiser l'une de ces variables md
puisque le fichier indiqué comprend des données sur l'ensemble des membres du Collège des médecins, et l'autre mil
puisque le fichier indiqué contient, pour sa part, des données sur les contrats octroyés par le ministère de la Défense nationale.
Pour pandas, cette structure de données, qui consiste en un tableau à deux dimensions, est appelé un dataframe.
md = pd.read_csv("cmq.csv")
mil = pd.read_csv("militaires.csv")
Pour connaître le type des variables que contient notre tableau, on peut utiliser la fonction .dtypes
.
Dans pandas, les principaux types de variables sont :
int
-> nombres entiersfloat
-> nombres décimauxobject
-> chaînes de caractèresdatetime
-> datesbool
-> vrai ou fauxCes types sont parfois suivis d'un nombre qui indique la taille, en caractères, maximale des variables de ce type.
md.dtypes
mil.dtypes
Pour afficher simplement le contenu de notre variable, il n'est pas nécessaire d'écrire print(md)
.
Écrire le nom de la variable suffit.
Cela produit un immense tableau avec les 30 premières et les 30 dernières lignes de notre fichier.
md
Pour avoir un aperçu des dimensions de de votre tableau, utilisez plutôt la fonction .shape
.
md.shape
Pour consulter les premières ou les dernières lignes de votre tableau, vous pouvez vous servir de fonctions qui ressemblent à ce qu'on a vu dans Unix, au début de la session, .head()
et .tail()
.
md.head()
mil.tail()
Pour consulter un intervalle précis de lignes, vous pouvez vous servir des crochets. Vous reconnaissez cette syntaxe puisque pandas, c'est du python!
C'est ainsi que, ci-dessous, je demande à afficher les lignes 1000 à 1009 de mon fichier (comme en python, la limite supérieure est exclue de mon intervalle).
md[1000:1010]
Pour faire afficher un nombre limité de colonnes, on peut ajouter une liste du nom des colonnes qu'on souhaite.
Il faut cependant le faire avec la fonction .loc
.
md.loc[7000:7010,["nom","prenom","num"]]
On peut aussi utiliser, au lieu du nom des colonnes, le numéro d'index des colonnes qu'on souhaite afficher.
Il faut alors utiliser la fonction .iloc
mil.iloc[100000:100011,3:]
Si on souhaite compter combien il y a d'éléments dans chacune de nos colonnes, on peut utiliser la fonction .count()
.
Employons-la avec nos deux fichiers.
On constate qu'avec les médecins, il nous manque parfois de l'information. Cela peut faire du sens, comme pour les colonnes specialite2
et specialite3
, puisque ce ne sont pas tous les médecins qui ont deux ou trois spécialités. Mais cela peut aussi vouloir dire qu'il y a des données manquantes. Ainsi, on n'a pas la ville dans laquelle excercent tous les médecins.
md.count()
mil.count()
Il est souvent utile de connaître le nom des colonnes de notre fichier.
Pour ce faire, entrez la fonction .columns
.
md.columns
Vous remarquez que le nom de chaque colonne se trouve dans une liste. On peut se servir de cette liste pour renommer nos colonnes dans le cas où elles ont des noms barbares. Il suffit de dire que md.columns
est égal à une liste dans laquelle on mettra nos nouveaux noms de colonnes.
C'est ainsi que la commande ci-dessous, par exemple, me permet de rebaptiser la colonne dateChangStatut
par date
.
md.columns = ['num', 'annee', 'prenom', 'nom', 'genre', 'specialite1', 'specialite2',
'specialite3', 'statut', 'date', 'typePermis', 'ville',
'prov', 'pays']
En invoquant la fonction .columns
, je constate que le nom de la colonne a changé.
md.columns
Ce n'est pas le cas ici, mais il arrive fréquemment, lorsqu'on travaille avec des données ouvertes, qu'il y a beaucoup de colonnes. Cela peut rendre les données difficiles à lire, sans compter que ça peut taxer la mémoire de notre ordi. Il est possible de supprimer des colonnes contenant des informations superflues.
Si, par exemple, dans le fichier de médecins, vous souhaitiez vous débarasser de la colonne ville
, ce serait avec la fonction .drop()
que vous vous y prendriez.
Cette fonction prend deux arguments. Le premier est la colonne (ou les colonnes) que vous souhaitez rayer de la surface de la Terre. Lorsqu'il y a plusieurs colonnes, il faut les mettre dans une liste : ["colonne_1", "colonne_2", "colonne_3"]
. Le second indique si ce sont des colonnes que vous souhaitez supprimer ou des lignes. Dans le cas de colonnes, l'argument doit être 1
, dans le cas de lignes, c'est 0
.
md = md.drop("ville", 1)
md.columns # La colonne «ville» a disparu!
Il est utile de simplifier les noms des colonnes puisqu'on peut faire afficher le contenu d'une colonne de notre choix simplement en ajoutant son nom à notre nom de variable.
On obtient alors non plus un dataframe, mais une structure de données unidimentionnelle que pandas appelle une serie.
Voici un exemple avec le genre des médecins :
md.genre
Cette sélection de colonnes est utile pour effectuer une série d'opérations de base qu'on va maintenant aborder.
Si on a une colonne qui contient des nombres, on peut en afficher la valeur maximale ou minimale à l'aide des fonctions .max()
et .min()
.
Voici un exemple avec la colonne montant
de notre fichier de données sur les contrats militaires.
mil.montant.max()
mil.montant.min()
On peut aussi effectuer des calculs simples sur ces colonnes.
Ici, on effectue :
.sum()
.mean()
Et on trouve la médiane avec la fonction .median()
mil.montant.sum()
mil.montant.mean()
mil.montant.median()
Dans les cas où vous avez plusieurs colonnes composées de nombres, il peut être intéressant de réunir toutes ces informations à l'intérieur d'un seul et même tableau.
C'est ce que nous permet de faire la fonction .describe()
.
mil.describe()
La meilleure façon d'interroger des données est d'y effectuer différents regroupements.
Dans notre fichier sur les médecins, par exemple, on a une colonne appelée genre
qui nous dit si un médecin est un homme ou une femme. Cette information est représentée par deux valeurs : "M" ou "F".
La fonction value_counts()
nous permet de compter combien il y a d'occurences de chacune de ces valeurs, donc de compter le nombre d'hommes et de femmes dans notre tableau.
md.genre.value_counts()
Si on veut, plutôt, calculer le pourcentage de chacune de ces valeurs, on peut se rappeler que pandas est du bon vieux python et utiliser la fonction len()
dans la formule suivante :
md.genre.value_counts() / len(md.genre)*100
63,92% des médecins inscrits au tableau du Collège des médecins, donc, sont des hommes; 36,08% des femmes.
On peut faire la même chose avec toutes les variables et, en combinant les fonctions .value_counts()
et .head()
, choisir de n'afficher qu'un nombre restreint de valeurs quand on en a plusieurs. Dans notre fichier de contrats militaires, par exemple, la formule ci-dessous nous permet d'identifier quel sont les 10 fournisseurs qui ont obtenu le plus grand nombre de contrats du ministère de la Défense.
mil.fournisseur.value_counts().head(10)
Vous remarquerez cependant, ici, que le 2e et le 10e fournisseur sont la même entreprise, mais écrite d'une manière différente («IMPERIAL OIL» dans un cas, «Imperial Oil Limited» dans l'autre).
Ici, donc, avant d'utiliser pandas, il serait préférable d'effectuer d'abord un nettoyage de nos données avec OpenRefine.
.str.replace()
¶Si vos données ne requièrent pas de nettoyage trop complexe, vous pouvez utiliser la fonction .str.replace()
.
C'est l'une des nombreuses fonctions permettant de traiter du texte (chaînes de caractères ou strings, d'où le «.str») dans pandas.
Dans la variable md
, par exemple, la première colonne contient le numéro de permis de tous les médecins. Ce numéro est précédé d'un dièse ou hashtag (#). Vous voulez anéantir ce caractère superfétatoire. Voici comment faire.
md.num = md.num.str.replace("#","")
md
Les fonctions .str
n'agissent que sur des series.
Ainsi, si vous écrivez la ligne de code suivante :
md = md.num.str.replace("#","")
Votre dataframe md
va être remplacé au complet par une simple serie ne contenant que les numéros de permis des médecins.
On peut éviter ce fâcheux contretemps en demandant à str.replace()
de ne transformer que la colonne md.num
, comme je l'ai fait quelques cellules plus haut.
Dans pandas, il existe une autre fonction qui permet d'effectuer des regroupements.
Il s'agit de la fonction .groupby()
On peut alors se demander quelle est la différence entre :
mil.fournisseur.value_counts()
etmil.groupby("fournisseur")
La différence est simple. Il faut se souvenir que pandas travaille avec deux grandes structures de données, les series et les dataframes.
Quand on fait mil.fournisseur
, on crée une series à partir de mil
. C'est tout simplement une liste de valeurs. Et la fonction .value_counts()
ne fait que dénombrer ces valeurs. Il faut aussi savoir que cette fonction ne s'applique qu'à des series.
Quand on fait mil.groupby("fournisseurs")
on crée en fait autant de dataframes qu'il y a de fournisseurs. La fonction .value_counts()
ne fonctionne plus. Mais on peut tout de même effectuer des calculs ou des regroupements en fonction des autres colonnes qui se trouvent toujours dans nos dataframe, une structure de données plus complexe qu'une series.
C'est ainsi que s'il est intéressant de connaître le nombre de contrats que chaque fournisseur a obtenu, il est encore plus pertinent de savoir qui sont les fournisseurs qui ont décroché les contrats les plus lucratifs.
La fonction .groupby()
, combinée à d'autres fonctions qu'on a vues antérieurement, va nous permettre de trouver la réponse.
La formule ci-dessous enchaîne cinq opérations :
mil
en fonction de la colonne fournisseur
montant
.sum()
, la somme des montants de chacune de ces series.sort_values()
et le paramètre ascending=**False**
mil.groupby("fournisseur").montant.sum().sort_values(ascending=False).head(10)
On a vu, plus haut, comment sélectionner une partie d'un tableau en fonction des numéros de lignes ou de colonnes.
Il est aussi possible de faire des sélections en fonction d'une valeur donnée. Par exemple, si, dans nos médecins, on souhait ne choisir que le groupe des médecins qui ont été radiés de leur ordre professionnel. On pourrait commencer par examiner la colonne statut
pour voir quelles sont les différentes valeurs qu'elle contient.
md.statut.value_counts()
On remarque qu'il y a 67 médecins dont le statut est «Radié» et 88 dont le statut est «Radié pour non-paiement de cotisation». On ne s'intéresse qu'aux premiers.
On va donc créer une variable que je vais appeler rad
et je vais y mettre uniquement les médecins dont le statut est «Radié» avec la formule suivante :
rad = md.statut == "Radié"
Je peux maintenant utiliser le sous-ensemble rad
de mon tableau md
pour effectuer toutes les opérations qu'on a vues jusqu'à maintenant.
Il faut seulement se souvenir que ce sous-ensemble est désigné ainsi : md[rad]
md[rad]
Je peux donc examiner quelles sont, par exemples, les spécialités les plus courantes dans le sous-ensemble des médecins radiés.
md[rad].specialite1.value_counts()
On peut également construire des sous-ensembles de façon plus souple en se basant sur la présence d'une chaîne de caractères donnés.
Par exemple, dans nos contrats militaires, vous avez peut-être remarqué qu'il y en a plusieurs qui ont été octroyés à différentes composantes des forces armées américaines. Il y a entre autres :
Si on souhaite créer un sous-ensemble regroupant tous les contrats dont le nom du fournisseur contient les mots «UNITED STATES», on peut utiliser la fonction str.contains()
de la façon suivante :
us = mil.fournisseur.str.contains("UNITED STATES")
On peut ensuite faire un décompte des différents noms de fournisseurs qu'on a ainsi regroupés pour constater qu'il y a plusieurs agences américaines à qui la défense canadienne a octroyé des contrats.
mil[us].fournisseur.value_counts()
Et quelle est la valeur totale des contrats que ces agences ont obtenu entre 2004 et 2016?
mil[us].montant.sum()
Cette somme représente quelle proportion du total qui a été octroyé en contrats par le ministère de la Défense au cours de la même période?
mil[us].montant.sum() / mil.montant.sum() * 100
La fonction str.contains()
est sensible à la casse. Ainsi, si on revient à nos médecins, la formule
md.statut.str.contains("Actif")
Ne donnera pas les mêmes résultats que
md.statut.str.contains("actif")
actifs = md.statut.str.contains("actif")
len(md[actifs])
actifs = md.statut.str.contains("Actif")
len(md[actifs])
La première ne regroupe que 10 médecins, tandis que la seconde en compte 21 201!
Pourquoi?
Parce que la première ne regroupe que les médecins dont le statut contient la chaîne «actif» avec un «a» minuscule, à savoir :
La seconde comprend les médecins dont les statuts contiennent la même chaîne, mais avec un «A» majuscule, c'est-à-dire :
À partir de notre sous-ensemble de médecins actifs, on peut aussi créer d'autres sous-ensembles. La formule ci-dessous sélectionne les médecins de famille dans ce groupe.
famille = md[actifs].specialite1 == "Médecine de famille"
Mais attention, si vous voulez afficher le sous-ensemble que vous venez de créer, vous pourriez être tenté d'écrire ceci :
md[famille]
Vous obtiendrez un message d'erreur tout aussi mystérieux qu'il peut être contrariant.
Il faut se souvenir que «famille» a été créé à partir de «actifs», il n'existe qu'en fonction du sous-ensemble md[actifs]
, et il faudra simplement enchaîner les deux sous-ensembles ainsi :
md[actifs][famille]
Une autre façon d'arriver exactement au même résultat est d'effectuer de façon indépendante vos deux regroupements.
On pourra créer un sous-ensemble des médecins de famille à partir de l'ensemble des médecins :
famille = md.specialite1 == "Médecine de famille"
Et là, écrire md[famille]
va fonctionner (ce sous-ensemble regroupe 13 150 médecins).
md[famille]
Puis, pour faire une intersection avec nos 21 201 médecins actifs, il vous suffira d'écrire ceci :
md[actifs & famille]
Et voilà!
On sait que, dans l'ensemble des 35 109 membres et ex-membres du Collège des médecins, on a 10 107 médecins actifs qui sont des médecins de famille.
Maintenant, dans ce sous-groupe, quelle est la répartition homme/femme?
md[famille & actifs].genre.value_counts()
Ce qui représente quelles proportions?
md[famille & actifs].genre.value_counts() / len(md[famille & actifs]) * 100
On a peut-être une nouvelle, ici. Chez les médecins de famille actifs, au Québec, il y a une majorité de femmes (55,5%, contre 44,5% chez les hommes).
On peut même faire des sous-ensembles encore plus pointus. Par exemple, quelle est la répartition homme/femme des membres du Collège des médecins qui sont chirurgiens, actifs, ayant obtenu leur diplôme depuis 2000 et pratiquant au Québec?
On a déjà notre sous-ensemble des médecins actifs. On va en créer trois autres.
D'abord les médecins pratiquant au Québec.
qc = md.prov == "Québec"
md[qc]
# Sous-ensemble de 29 794 médecins
Ensuite, les médecins ayant intégré la profession depuis 2000.
Au lieu d'utiliser ==
, on va se servir d'un autre opérateur qui peut s'appliquer à des nombres.
depuis2000 = md.annee >= 2000
md[depuis2000]
# Ce sous-ensemble compte 10411 médecins
Ensuite, on va identifier les chirurgiens.
Ici, on va se servir de la fonction .str.contains()
pour réunir tous les MD dont la spécialité contient l'expression «hirurgie», ce qui permet de repérer autant «Chirurgie générale» que «Neurochirurgie».
chir1 = md.specialite1.str.contains("hirurgie")
md[chir1]
# On en obtient 3 259
Il faut également vérifier les médecins dont la 2e ou 3e spécialité est la chirurgie, car on a trois colonnes de spécialités.
Il faudra cependant ajouter un paramètre à la fonction .str.contains
qui va faire en sorte d'ignorer les médecins qui n'ont aucune 2e ou 3e spécialité avec l'argument na=False
.
chir2 = md.specialite2.str.contains("hirurgie", na=False)
md[chir2]
# On obtient ici 268 médecins
chir3 = md.specialite3.str.contains("hirurgie", na=False)
md[chir3]
# Quatre médecins dans ce sous-ensemble
Si on veut les réunir tous et y voir la répartition homme/femme, la formule ci-dessous les combine de la façon suivante :
Médecins actifs ET
pratiquant au Québec ET
depuis 2000 ET
dont la spécialité 1 OU la spécialité 2 OU la spécialité 3 est une forme ou une autre de chirurgie.
md[actifs & qc & depuis2000 & (chir1 | chir2 | chir3)].genre.value_counts()
Au tout début de notre carnet, on a importé cette bibliothèque au nom affeux, matplotlib.
On va s'en servir pour créer des graphiques. Pour ce faire, il suffit d'ajouter la fonction .plot()
à la fin d'une formule.
md[famille & actifs].genre.value_counts().plot()
Ce n'est pas très évocateur. On peut heureusement changer le type de graphique pour, par exemple, choisir un graphique en pointes de tarte. Il suffit d'ajouter, à la fonction .plot()
l'argument kind
auquel on donne la valeur de pie
.
On peut aussi donner un titre à notre graphique avec l'argument title
.
md[famille & actifs].genre.value_counts().plot(kind="pie", title="Majorité de femmes médecin de famille au Québec")
Un autre type de graphique intéressant est l'histogramme.
Pour tracer l'évolution du nombre de médecins ayant intégré la profession médicale au cours des ans, on pourrait faire un histogramme de notre tableau md
en fonction de la colonne annee
avec la formule suivante :
md.annee.hist()
Mais voilà qui ne raconte pas grand-chose. C'est que par défaut, l'histogramme trace 10 barres, ce qui fait que chaque barre de notre histogramme regroupe 7 ou 8 années.
Les barres, c'est que ce matplotlib appelle des «bins». On peut ainsi spécifier le nombre de ces bins avec un argument qui s'appelle... vous l'aurez deviné... bins
Ici, comme nos données couvrent 87 années, on va créer 87 barres.
Chacune représente le nombre de médecins qui ont obtenu leur permis de pratique au cours d'une année donnée.
md.annee.hist(bins=87)
Il remarque une chute au début des années 1990. Elle correspond à une politique du gouvernement québécois qui avait limité le nombre d'inscriptions dans les facultés de médecine quelques années auparavant.
Inspiré par ce graphique, vous pourriez avoir envie de voir combien d'hommes et de femmes sont devenus médecin au fil des ans afin de visualiser la féminisation de la profession médicale. Pour ce faire, il suffirait de modifier la formule ci-dessus en ajoutant un groupby
par genre
, puis l'argument alpha
à la fonction .hist()
pour faire en sorte que nos barres aient une transparence de 50%.
md.groupby("genre").annee.hist(bins=87,alpha=0.5)
Le graphique ci-dessus raconte une histoire avec les données.
On y voit que jusqu'en 1970, les hommes (en jaune) étaient clairement majoritaires dans la profession.
À partir de 1970 semble s'opérer un renversement. Le nombre d'hommes diminue progressivement pour, dès 1990, devenir inférieur au nombre de femmes (en bleu).
Aujourd'hui, les deux tiers des recrues, en médecine, chaque année, sont des femmes.
En terminant, faisons aussi un graphique avec nos données de contrats.
Calculons la somme des contrats octroyés chaque année, créons un diagramme à barres horizontales (kind="barh"
) et donnons une couleur de camouflage (teal) à nos barres.
mil.groupby("annee").montant.sum().plot(kind="barh",color="teal")