Pandas est l’élément central de l’écosystème Python pour la data science.
Le succès récent de Python dans l’analyse de données tient beaucoup à Pandas qui a permis d’importer la
logique SQL dans le langage Python. Pandas embarque énormément de
fonctionalités qui permettent d’avoir des chaînes de traitement efficaces pour
traiter des données de volumétrie moyenne (jusqu’à quelques Gigas). Au-delà
de cette volumétrie, il faudra se tourner vers d’autres solutions
(DuckDB, Dask, Polars, Spark…).
Pour essayer les exemples présents dans ce tutoriel :
Le packagePandas est l’une des briques centrales de l’écosystème de
la data science. Le DataFrame,
objet central dans des langages comme R
ou Stata, a longtemps était un grand absent dans l’écosystème Python.
Pourtant, grâce à Numpy, toutes les briques de base étaient présentes.
Wes McKinney, lorsqu’il a créé Pandas en s’appuyant sur Numpy
a ainsi introduit cet objet devenu banal qu’est le DataFrame.
Pandas est rapidement
devenu un incontournable de la data-science. L’ouvrage
de référence de McKinney (2012) présente de manière plus
ample ce package.
Ce tutoriel vise à introduire aux concepts
de base de ce package par l’exemple et à introduire à certaines
des tâches les plus fréquentes de (re)structuration
des données du data scientist. Il ne s’agit pas d’un ensemble
exhaustif de commandes : Pandas est un package tentaculaire
qui permet de réaliser la même opération de nombreuses manières.
Nous nous concentrerons ainsi sur les éléments les plus pertinents
dans le cadre d’une introduction à la data science et laisserons
les utilisateurs intéressés approfondir leurs connaissances
dans les ressources foisonnantes qu’il existe sur le sujet.
Dans ce tutoriel Pandas, nous allons utiliser :
Les émissions de gaz à effet de serre estimées au niveau communal par l’ADEME. Le jeu de données est
disponible sur data.gouv
et requêtable directement dans Python avec
cet url.
Le chapitre suivant permettra de mettre en application des éléments présents dans ce chapitre avec
les données ci-dessus associées à des données de contexte au niveau communal.
Nous suivrons les conventions habituelles dans l’import des packages :
!pip install pynsee
Requirement already satisfied: pynsee in /opt/mamba/lib/python3.9/site-packages (0.1.4)
Requirement already satisfied: pandas>=0.24.2 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (2.1.1)
Requirement already satisfied: tqdm>=4.56.0 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (4.66.1)
Requirement already satisfied: requests>=2.23 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (2.31.0)
Requirement already satisfied: appdirs>=1.4.4 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (1.4.4)
Requirement already satisfied: unidecode>=1.1.0 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (1.3.6)
Requirement already satisfied: shapely>=2.0.0 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (2.0.1)
Requirement already satisfied: urllib3 in /opt/mamba/lib/python3.9/site-packages (from pynsee) (1.26.11)
Requirement already satisfied: numpy>=1.22.4 in /opt/mamba/lib/python3.9/site-packages (from pandas>=0.24.2->pynsee) (1.26.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/mamba/lib/python3.9/site-packages (from pandas>=0.24.2->pynsee) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /opt/mamba/lib/python3.9/site-packages (from pandas>=0.24.2->pynsee) (2022.5)
Requirement already satisfied: tzdata>=2022.1 in /opt/mamba/lib/python3.9/site-packages (from pandas>=0.24.2->pynsee) (2023.3)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/mamba/lib/python3.9/site-packages (from requests>=2.23->pynsee) (2.1.1)
Requirement already satisfied: idna<4,>=2.5 in /opt/mamba/lib/python3.9/site-packages (from requests>=2.23->pynsee) (3.3)
Requirement already satisfied: certifi>=2017.4.17 in /opt/mamba/lib/python3.9/site-packages (from requests>=2.23->pynsee) (2022.9.24)
Requirement already satisfied: six>=1.5 in /opt/mamba/lib/python3.9/site-packages (from python-dateutil>=2.8.2->pandas>=0.24.2->pynsee) (1.16.0)
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport pynsee.download
Pour obtenir des résultats reproductibles, on peut fixer la racine du générateur
pseudo-aléatoire.
np.random.seed(123)
Au cours de cette démonstration des principales fonctionalités de Pandas, et
lors du chapitre suivant,
je recommande de se référer régulièrement aux ressources suivantes :
Ce tutoriel,
pensé certes pour les utilisateurs d’Observable Javascript,
mais qui offre de nombreux exemples intéressants pour les afficionados de Pandas ;
L’objet central dans la logique Pandas est le DataFrame.
Il s’agit d’une structure particulière de données
à deux dimensions, structurées en alignant des lignes et colonnes.
Contrairement à une matrice, les colonnes
peuvent être de types différents.
Le concept de tidy data, popularisé par Hadley Wickham via ses packages R,
est parfaitement pertinent pour décrire la structure d’un DataFrame pandas.
Les trois règles sont les suivantes :
Chaque variable possède sa propre colonne ;
Chaque observation possède sa propre ligne ;
Une valeur, matérialisant une observation d’une variable,
se trouve sur une unique cellule.
Concept de tidy data (emprunté à H. Wickham)
Concernant la syntaxe, une partie des commandes Python est inspirée par la logique SQL.
On retrouvera ainsi une philosophie proche de celle du SQL où on fait des opérations
de sélection de ligne ou de colonne. Voici une illustration de quelques manipulations de données
que nous mettrons en oeuvre par la suite :
Réordonner le DataFrame
Pandas propose énormément de fonctionnalités pré-implémentées.
Il est vivement recommandé, avant de se lancer dans l’écriture d’une
fonction, de se poser la question de son implémentation native dans Numpy, Pandas, etc.
La plupart du temps, s’il existe une solution implémentée dans une librairie, il convient
de l’utiliser car elle sera plus efficace que celle que vous mettrez en oeuvre.
Les Series
En fait, un DataFrame est une collection d’objets appelés pandas.Series.
Ces Series sont des objets d’une dimension qui sont des extensions des
array-unidimensionnels numpy. En particulier, pour faciliter le traitement
de données catégorielles ou temporelles, des types de variables
supplémentaires sont disponibles dans pandas par rapport à
numpy (categorical, datetime64 et timedelta64). Ces
types sont associés à des méthodes optimisées pour faciliter le traitement
de ces données.
Il ne faut pas négliger l’attribut dtype d’un objet
pandas.Series car cela a une influence déterminante sur les méthodes
et fonctions pouvant être utilisées (on ne fait pas les mêmes opérations
sur une donnée temporelle et une donnée catégorielle) et le volume en
mémoire d’une variable (le type de la variable détermine le volume
d’information stockée pour chaque élément ; être trop précis est parfois
néfaste).
Il existe plusieurs types possibles pour un pandas.Series.
Le type object correspond aux types Python str ou mixed.
Il existe un type particulier pour les variables dont le nombre de valeurs
est une liste finie et relativement courte, le type category.
Il faut bien examiner les types de son DataFrame, et convertir éventuellement
les types lors de l’étape de data cleaning.
Indexation
La différence essentielle entre une Series et un objet numpy est l’indexation.
Dans numpy,
l’indexation est implicite ; elle permet d’accéder à une donnée (celle à
l’index situé à la position i).
Avec une Series, on peut bien sûr utiliser un indice de position mais on peut
surtout faire appel à des indices plus explicites.
Par exemple,
taille = pd.Series( [1.,1.5,1], index = ['chat', 'chien', 'koala'])taille.head()
chat 1.0
chien 1.5
koala 1.0
dtype: float64
Cette indexation permet d’accéder à des valeurs de la Series
via une valeur de l’indice. Par
exemple, taille['koala']:
taille['koala']
1.0
L’existence d’indice rend le subsetting particulièrement aisé, ce que vous
pouvez expérimenter dans les TP :
Pour transformer un objet pandas.Series en array numpy,
on utilise la méthode values. Par exemple, taille.values:
taille.values
array([1. , 1.5, 1. ])
Un avantage des Series par rapport à un arraynumpy est que
les opérations sur les Series alignent
automatiquement les données à partir des labels.
Avec des Series labélisées, il n’est ainsi pas nécessaire
de se poser la question de l’ordre des lignes.
L’exemple dans la partie suivante permettra de s’en assurer.
Valeurs manquantes
Par défaut, les valeurs manquantes sont affichées NaN et sont de type np.nan (pour
les valeurs temporelles, i.e. de type datatime64, les valeurs manquantes sont
NaT).
On a un comportement cohérent d’agrégation lorsqu’on combine deux DataFrames (ou deux colonnes).
Par exemple,
x = pd.DataFrame( {'prix': np.random.uniform(size =5),'quantite': [i+1for i inrange(5)] }, index = ['yaourt','pates','riz','tomates','gateaux'])x
prix
quantite
yaourt
0.696469
1
pates
0.286139
2
riz
0.226851
3
tomates
0.551315
4
gateaux
0.719469
5
y = pd.DataFrame( {'prix': [np.nan, 0, 1, 2, 3],'quantite': [i+1for i inrange(5)] }, index = ['tomates','yaourt','gateaux','pates','riz'])y
prix
quantite
tomates
NaN
1
yaourt
0.0
2
gateaux
1.0
3
pates
2.0
4
riz
3.0
5
x + y
prix
quantite
gateaux
1.719469
8
pates
2.286139
6
riz
3.226851
8
tomates
NaN
5
yaourt
0.696469
3
donne bien une valeur manquante pour la ligne tomates. Au passage, on peut remarquer que l’agrégation
a tenu compte des index.
Il est possible de supprimer les valeurs manquantes grâce à dropna().
Cette méthode va supprimer toutes les lignes où il y a au moins une valeur manquante.
Il est aussi possible de supprimer seulement les colonnes où il y a des valeurs manquantes
dans un DataFrame avec dropna() avec le paramètre axis=1 (par défaut égal à 0).
Il est également possible de remplir les valeurs manquantes grâce à la méthode fillna().
Le DataFrame Pandas
Le DataFrame est l’objet central de la librairie pandas.
Il s’agit d’une collection de pandas.Series (colonnes) alignées par les index.
Les types des variables peuvent différer.
Pour présenter les méthodes les plus pratiques pour l’analyse de données,
on peut partir de l’exemple des consommations de CO2 communales issues
des données de l’Ademe. Cette base de données est exploitée plus intensément
dans le TP.
L’import de données depuis un fichier plat se fait avec la fonction read_csv:
Dans un processus de production, où normalement on connait les types des variables du DataFrame qu’on va importer,
il convient de préciser les types avec lesquels on souhaite importer les données
(argument dtype, sous la forme d’un dictionnaire).
Cela est particulièrement important lorsqu’on désire utiliser une colonne
comme une variable textuelle mais qu’elle comporte des attributs proches d’un nombre
qui vont inciter pandas à l’importer sous forme de variable numérique.
Par exemple, une colonne [00001,00002,...] risque d’être importée comme une variable numérique, ignorant l’information des premiers 0 (qui peuvent pourtant la distinguer de la séquence 1, 2, etc.). Pour s’assurer que pandas importe sous forme textuelle la variable, on peut utiliser dtype = {"code": "str"}
Sinon, on peut importer le csv, et modifier les types avec astype().
Avec astype, on peut gérer les erreurs de conversion avec le paramètre errors.
L’affichage des DataFrames est très ergonomique. On obtiendrait le même output
avec display(df). Les premières et dernières lignes s’affichent
automatiquement. Autrement, on peut aussi faire:
head qui permet, comme son
nom l’indique, de n’afficher que les premières lignes ;
tail qui permet, comme son
nom l’indique, de n’afficher que les dernières lignes
sample qui permet d’afficher un échantillon aléatoire de n lignes.
Cette méthode propose de nombreuses options.
Warning
Il faut faire attention au display et aux
commandes qui révèlent des données (head, tail, etc.)
dans un Notebook ou un Markdown qui exploite
des données confidentielles lorsqu’on utilise Git.
En effet, on peut se
retrouver à partager des données, involontairement, dans l’historique
Git. Avec un R Markdown, il suffit d’ajouter les sorties au fichier
.gitignore (par exemple avec une balise de type *.html). Avec un
Notebook Jupyter, la démarche est plus compliquée car les fichiers
.ipynb intègrent dans le même document, texte, sorties et mise en forme.
Techniquement, il est possible d’appliquer des filtres avec Git
(voir
ici)
mais c’est une démarche très complexe.
Ce post de l’équipe à l’origine de nbdev2
résume bien le problème du contrôle de version avec Git et des solutions qui
peuvent y être apportées.
Une solution est d’utiliser Quarto qui permet de générer les
.ipynb en output d’un document texte, ce qui facilite le contrôle sur les
éléments présents dans le document.
Dimensions et structure du DataFrame
Les premières méthodes utiles permettent d’afficher quelques
attributs d’un DataFrame.
Pour connaître les dimensions d’un DataFrame, on peut utiliser quelques méthodes
pratiques :
df.ndim
2
df.shape
(35798, 12)
df.size
429576
Pour déterminer le nombre de valeurs uniques d’une variable, plutôt que chercher à écrire soi-même une fonction,
on utilise la
méthode nunique. Par exemple,
df['Commune'].nunique()
33338
pandas propose énormément de méthodes utiles.
Voici un premier résumé, accompagné d’un comparatif avec R :
Opération
pandas
dplyr (R)
data.table (R)
Récupérer le nom des colonnes
df.columns
colnames(df)
colnames(df)
Récupérer les indices
df.index
unique(df[,get(key(df))])
Récupérer les dimensions
df.shape
dim(df)
dim(df)
Récupérer le nombre de valeurs uniques d’une variable
df['myvar'].nunique()
df %>% summarise(distinct(myvar))
df[,uniqueN(myvar)]
Statistiques agrégées
pandas propose une série de méthodes pour faire des statistiques
agrégées de manière efficace.
On peut, par exemple, appliquer des méthodes pour compter le nombre de lignes,
faire une moyenne ou une somme de l’ensemble des lignes
df.count()
INSEE commune 35798
Commune 35798
Agriculture 35736
Autres transports 9979
Autres transports international 2891
CO2 biomasse hors-total 35798
Déchets 35792
Energie 34490
Industrie hors-énergie 34490
Résidentiel 35792
Routier 35778
Tertiaire 35798
dtype: int64
df.mean(numeric_only =True)
Agriculture 2459.975760
Autres transports 654.919940
Autres transports international 7692.344960
CO2 biomasse hors-total 1774.381550
Déchets 410.806329
Energie 662.569846
Industrie hors-énergie 2423.127789
Résidentiel 1783.677872
Routier 3535.501245
Tertiaire 1105.165915
dtype: float64
df.sum(numeric_only =True)
Agriculture 8.790969e+07
Autres transports 6.535446e+06
Autres transports international 2.223857e+07
CO2 biomasse hors-total 6.351931e+07
Déchets 1.470358e+07
Energie 2.285203e+07
Industrie hors-énergie 8.357368e+07
Résidentiel 6.384140e+07
Routier 1.264932e+08
Tertiaire 3.956273e+07
dtype: float64
df.nunique()
INSEE commune 35798
Commune 33338
Agriculture 35576
Autres transports 9963
Autres transports international 2883
CO2 biomasse hors-total 35798
Déchets 11016
Energie 1453
Industrie hors-énergie 1889
Résidentiel 35791
Routier 35749
Tertiaire 8663
dtype: int64
La version 2.0 de Pandas a introduit un changement
de comportement dans les méthodes d’agrégation.
Il est dorénavant nécessaire de préciser quand on désire
effectuer des opérations si on désire ou non le faire
exclusivement sur les colonnes numériques. C’est pour cette
raison qu’on exlicite ici l’argument numeric_only = True.
Ce comportement
était par le passé implicite.
Il faut toujours regarder les options de ces fonctions en termes de valeurs manquantes, car
ces options sont déterminantes dans le résultat obtenu.
Les exercices de TD visent à démontrer l’intérêt de ces méthodes dans quelques cas précis.
Le tableau suivant récapitule le code équivalent pour avoir des
statistiques sur toutes les colonnes d’un dataframe en R.
La méthode describe permet de sortir un tableau de statistiques
agrégées :
df.describe()
Agriculture
Autres transports
Autres transports international
CO2 biomasse hors-total
Déchets
Energie
Industrie hors-énergie
Résidentiel
Routier
Tertiaire
count
35736.000000
9979.000000
2.891000e+03
35798.000000
35792.000000
3.449000e+04
3.449000e+04
35792.000000
35778.000000
35798.000000
mean
2459.975760
654.919940
7.692345e+03
1774.381550
410.806329
6.625698e+02
2.423128e+03
1783.677872
3535.501245
1105.165915
std
2926.957701
9232.816833
1.137643e+05
7871.341922
4122.472608
2.645571e+04
5.670374e+04
8915.902379
9663.156628
5164.182507
min
0.003432
0.000204
3.972950e-04
3.758088
0.132243
2.354558e+00
1.052998e+00
1.027266
0.555092
0.000000
25%
797.682631
52.560412
1.005097e+01
197.951108
25.655166
2.354558e+00
6.911213e+00
96.052911
419.700460
94.749885
50%
1559.381285
106.795928
1.992434e+01
424.849988
54.748653
4.709115e+00
1.382243e+01
227.091193
1070.895593
216.297718
75%
3007.883903
237.341501
3.298311e+01
1094.749825
110.820941
5.180027e+01
1.520467e+02
749.469293
3098.612157
576.155869
max
98949.317760
513140.971691
3.303394e+06
576394.181208
275500.374439
2.535858e+06
6.765119e+06
410675.902028
586054.672836
288175.400126
Méthodes relatives aux valeurs manquantes
Les méthodes relatives aux valeurs manquantes peuvent être mobilisées
en conjonction des méthodes de statistiques agrégées. C’est utile lorsqu’on
désire obtenir une idée de la part de valeurs manquantes dans un jeu de
données
df.isnull().sum()
INSEE commune 0
Commune 0
Agriculture 62
Autres transports 25819
Autres transports international 32907
CO2 biomasse hors-total 0
Déchets 6
Energie 1308
Industrie hors-énergie 1308
Résidentiel 6
Routier 20
Tertiaire 0
dtype: int64
On trouvera aussi la référence à isna() qui est la même méthode que isnull().
Graphiques rapides
Les méthodes par défaut de graphiques
(approfondies dans la partie visualisation)
sont pratiques pour
produire rapidement un graphique, notamment après des opérations
complexes de maniement de données.
En effet, on peut appliquer la méthode plot() directement à une pandas.Series :
La sortie est un objet matplotlib. La customisation de ces
figures est ainsi
possible (et même désirable car les graphiques matplotlib
sont, par défaut, assez rudimentaires), nous en verrons quelques exemples.
Accéder à des éléments d’un DataFrame
Sélectionner des colonnes
En SQL, effectuer des opérations sur les colonnes se fait avec la commande
SELECT. Avec pandas,
pour accéder à une colonne dans son ensemble on peut
utiliser plusieurs approches :
dataframe.variable, par exemple df.Energie.
Cette méthode requiert néanmoins d’avoir des
noms de colonnes sans espace.
dataframe[['variable']] pour renvoyer la variable sous
forme de DataFrame ou dataframe['variable'] pour
la renvoyer sous forme de Series. Par exemple, df[['Autres transports']]
ou df['Autres transports']. C’est une manière préférable de procéder.
Accéder à des lignes
Pour accéder à une ou plusieurs valeurs d’un DataFrame,
il existe deux manières conseillées de procéder, selon la
forme des indices de lignes ou colonnes utilisées :
df.loc : utilise les labels
df.iloc : utilise les indices
Warning
Les bouts de code utilisant la structure df.ix
sont à bannir car la fonction est deprecated et peut
ainsi disparaître à tout moment.
iloc va se référer à l’indexation de 0 à N où N est égal à df.shape[0] d’un
pandas.DataFrame. loc va se référer aux valeurs de l’index
de df.
df_example.loc[1, :] donnera la première ligne de df (ligne où l’indice month est égal à 1) ;
df_example.iloc[1, :] donnera la deuxième ligne (puisque l’indexation en Python commence à 0) ;
df_example.iloc[:, 1] donnera la deuxième colonne, suivant le même principe.
Principales manipulation de données
L’objectif du TP pandas est de se familiariser plus avec ces
commandes à travers l’exemple des données des émissions de C02.
Les opérations les plus fréquentes en SQL sont résumées par le tableau suivant.
Il est utile de les connaître (beaucoup de syntaxes de maniement de données
reprennent ces termes) car, d’une
manière ou d’une autre, elles couvrent la plupart
des usages de manipulation des données
Opération
SQL
pandas
dplyr (R)
data.table (R)
Sélectionner des variables par leur nom
SELECT
df[['Autres transports','Energie']]
df %>% select(Autres transports, Energie)
df[, c('Autres transports','Energie')]
Sélectionner des observations selon une ou plusieurs conditions;
FILTER
df[df['Agriculture']>2000]
df %>% filter(Agriculture>2000)
df[Agriculture>2000]
Trier la table selon une ou plusieurs variables
SORT BY
df.sort_values(['Commune','Agriculture'])
df %>% arrange(Commune, Agriculture)
df[order(Commune, Agriculture)]
Ajouter des variables qui sont fonction d’autres variables;
SELECT *, LOG(Agriculture) AS x FROM df
df['x'] = np.log(df['Agriculture'])
df %>% mutate(x = log(Agriculture))
df[,x := log(Agriculture)]
Effectuer une opération par groupe
GROUP BY
df.groupby('Commune').mean()
df %>% group_by(Commune) %>% summarise(m = mean)
df[,mean(Commune), by = Commune]
Joindre deux bases de données (inner join)
SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.x
Opérations sur les colonnes : select, mutate, drop
Les DataFrames pandas sont des objets mutables en langage Python,
c’est-à-dire qu’il est possible de faire évoluer le DataFrame au grès
des opérations. L’opération la plus classique consiste à ajouter ou retirer
des variables à la table de données.
df_new = df.copy()
Warning
Attention au comportement de pandas lorsqu’on crée une duplication
d’un DataFrame.
Par défaut, pandas effectue une copie par référence. Dans ce
cas, les deux objets (la copie et l’objet copié) restent reliés. Les colonnes
créées sur l’un vont être répercutées sur l’autre. Ce comportement permet de
limiter l’inflation en mémoire de Python. En faisant ça, le deuxième
objet prend le même espace mémoire que le premier. Le package data.table
en R adopte le même comportement, contrairement à dplyr.
Cela peut amener à quelques surprises si ce comportement d’optimisation
n’est pas anticipé. Si vous voulez, par sécurité, conserver intact le
premier DataFrame, faites appel à une copie profonde (deep copy) en
utilisant la méthode copy, comme ci-dessus.
Attention toutefois, cela a un coût mémoire.
Avec des données volumineuses, c’est une pratique à utiliser avec précaution.
La manière la plus simple d’opérer pour ajouter des colonnes est
d’utiliser la réassignation. Par exemple, pour créer une variable
x qui est le log de la
variable Agriculture:
df_new['x'] = np.log(df_new['Agriculture'])
Il est possible d’appliquer cette approche sur plusieurs colonnes. Un des
intérêts de cette approche est qu’elle permet de recycler le nom de colonnes.
vars= ['Agriculture', 'Déchets', 'Energie']df_new[[v +"_log"for v invars]] = np.log(df_new[vars])df_new
INSEE commune
Commune
Agriculture
Autres transports
Autres transports international
CO2 biomasse hors-total
Déchets
Energie
Industrie hors-énergie
Résidentiel
Routier
Tertiaire
x
Agriculture_log
Déchets_log
Energie_log
0
01001
L'ABERGEMENT-CLEMENCIAT
3711.425991
NaN
NaN
432.751835
101.430476
2.354558
6.911213
309.358195
793.156501
367.036172
8.219171
8.219171
4.619374
0.856353
1
01002
L'ABERGEMENT-DE-VAREY
475.330205
NaN
NaN
140.741660
140.675439
2.354558
6.911213
104.866444
348.997893
112.934207
6.164010
6.164010
4.946455
0.856353
2
01004
AMBERIEU-EN-BUGEY
499.043526
212.577908
NaN
10313.446515
5314.314445
998.332482
2930.354461
16616.822534
15642.420313
10732.376934
6.212693
6.212693
8.578159
6.906086
3
01005
AMBERIEUX-EN-DOMBES
1859.160954
NaN
NaN
1144.429311
216.217508
94.182310
276.448534
663.683146
1756.341319
782.404357
7.527881
7.527881
5.376285
4.545232
4
01006
AMBLEON
448.966808
NaN
NaN
77.033834
48.401549
NaN
NaN
43.714019
398.786800
51.681756
6.106949
6.106949
3.879532
NaN
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
35793
95676
VILLERS-EN-ARTHIES
1628.065094
NaN
NaN
165.045396
65.063617
11.772789
34.556067
176.098160
309.627908
235.439109
7.395148
7.395148
4.175366
2.465791
35794
95678
VILLIERS-ADAM
698.630772
NaN
NaN
1331.126598
111.480954
2.354558
6.911213
1395.529811
18759.370071
403.404815
6.549122
6.549122
4.713854
0.856353
35795
95680
VILLIERS-LE-BEL
107.564967
NaN
NaN
8367.174532
225.622903
534.484607
1568.845431
22613.830247
12217.122402
13849.512001
4.678095
4.678095
5.418865
6.281303
35796
95682
VILLIERS-LE-SEC
1090.890170
NaN
NaN
326.748418
108.969749
2.354558
6.911213
67.235487
4663.232127
85.657725
6.994749
6.994749
4.691070
0.856353
35797
95690
WY-DIT-JOLI-VILLAGE
1495.103542
NaN
NaN
125.236417
97.728612
4.709115
13.822427
117.450851
504.400972
147.867245
7.309951
7.309951
4.582194
1.549500
35798 rows × 16 columns
Il est également possible d’utiliser la méthode assign. Pour des opérations
vectorisées, comme le sont les opérateurs de numpy, cela n’a pas d’intérêt.
Cela permet notamment d’enchainer les opérations sur un même DataFrame (notamment grâce au pipe que
nous verrons plus loin).
Cette approche utilise généralement
des lambda functions. Par exemple le code précédent (celui concernant une
seule variable) prendrait la forme:
Dans les méthodes suivantes, il est possible de modifier le pandas.DataFrameen place, c’est à dire en ne le réassignant pas, avec le paramètre inplace = True.
Par défaut, inplace est égal à False et pour modifier le pandas.DataFrame,
il convient de le réassigner.
On peut facilement renommer des variables avec la méthode rename qui
fonctionne bien avec des dictionnaires (pour renommer des colonnes il faut
préciser le paramètre axis = 1):
Enfin, pour effacer des colonnes, on utilise la méthode drop avec l’argument
columns:
df_new = df_new.drop(columns = ["eneg", "agr"])
Réordonner
La méthode sort_values permet de réordonner un DataFrame. Par exemple,
si on désire classer par ordre décroissant de consommation de CO2 du secteur
résidentiel, on fera
Ainsi, en une ligne de code, on identifie les villes où le secteur
résidentiel consomme le plus.
Filtrer
L’opération de sélection de lignes s’appelle FILTER en SQL. Elle s’utilise
en fonction d’une condition logique (clause WHERE). On sélectionne les
données sur une condition logique. Il existe plusieurs méthodes en pandas.
La plus simple est d’utiliser les boolean mask, déjà vus dans le chapitre
numpy.
Par exemple, pour sélectionner les communes dans les Hauts-de-Seine, on
peut utiliser le résultat de la méthode str.startswith (qui renvoie
True ou False) directement dans les crochets:
Il est conseillé de filtrer avec loc en utilisant un masque.
En effet, contrairement à df[mask], df.loc[mask, :] permet d’indiquer clairement
à Python que l’on souhaite appliquer le masque aux labels de l’index.
Ce n’est pas le cas avec df[mask].
D’ailleurs, lorsqu’on utilise la syntaxe df[mask], pandas renvoie généralement un warning
Opérations par groupe
En SQL, il est très simple de découper des données pour
effectuer des opérations sur des blocs cohérents et recollecter des résultats
dans la dimension appropriée.
La logique sous-jacente est celle du split-apply-combine qui est repris
par les langages de manipulation de données, auxquels pandasne fait pas exception.
L’image suivante, issue de
ce site
représente bien la manière dont fonctionne l’approche
split-apply-combine
Split-apply-combine
Ce tutoriel sur le sujet
est particulièrement utile.
Pour donner quelques exemples, on peut créer une variable départementale qui
servira de critère de groupe.
df['dep'] = df['INSEE commune'].str[:2]
En pandas, on utilise groupby pour découper les données selon un ou
plusieurs axes. Techniquement, cette opération consiste à créer une association
entre des labels (valeurs des variables de groupe) et des
observations.
Par exemple, pour compter le nombre de communes par département en SQL, on
utiliserait la requête suivante :
La syntaxe est quasiment transparente. On peut bien sûr effectuer des opérations
par groupe sur plusieurs colonnes. Par exemple,
df.groupby('dep').mean(numeric_only =True)
Agriculture
Autres transports
Autres transports international
CO2 biomasse hors-total
Déchets
Energie
Industrie hors-énergie
Résidentiel
Routier
Tertiaire
dep
01
1974.535382
100.307344
8.900375
1736.353087
671.743966
280.485435
1744.567552
1346.982227
3988.658995
1021.089078
02
1585.417729
202.878748
17.390638
767.072924
223.907551
76.316247
932.135611
793.615867
1722.240298
403.744266
03
6132.029417
240.076499
45.429978
1779.630883
349.746819
326.904841
1452.423506
1401.650215
3662.773062
705.937016
04
1825.455590
177.321816
NaN
583.198128
253.975910
62.808435
313.913553
587.116013
1962.654370
493.609329
05
1847.508592
141.272766
NaN
502.012857
132.548068
34.971220
102.649239
728.734494
2071.010178
463.604908
...
...
...
...
...
...
...
...
...
...
...
91
802.793163
10114.998156
73976.107892
3716.906101
1496.516194
538.761253
1880.810170
6532.123033
10578.452789
3866.757200
92
8.309835
362.964554
13.132461
29663.579634
7347.163353
6745.611611
19627.706224
40744.279029
33289.456629
23222.587595
93
50.461775
1753.443710
61188.896632
18148.789684
6304.173594
2570.941598
10830.409025
32911.305703
35818.236459
21575.444794
94
48.072971
5474.808839
16559.384091
14710.744314
4545.099181
1624.281505
9940.192318
28444.561597
24881.531613
16247.876321
95
609.172047
682.143912
37984.576873
3408.871963
1334.032970
463.860672
1729.692179
6684.181989
8325.948748
4014.985843
96 rows × 10 columns
A noter que la variable de groupe, ici dep, devient, par défaut, l’index
du DataFrame de sortie. Si on avait utilisé plusieurs variables de groupe,
on obtiendrait un objet multi-indexé. Sur la gestion des multi-index, on
pourra se référer à l’ouvrage Modern Pandas dont la référence est
donnée en fin de cours.
Tant qu’on n’appelle pas une action sur un DataFrame par groupe, du type
head ou display, pandas n’effectue aucune opération. On parle de
lazy evaluation. Par exemple, le résultat de df.groupby('dep') est
une transformation qui n’est pas encore évaluée :
df.groupby('dep')
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f1b5c4dd7f0>
Il est possible d’appliquer plus d’une opération à la fois grâce à la méthode
agg. Par exemple, pour obtenir à la fois le minimum, la médiane et le maximum
de chaque département, on peut faire:
La première ligne est présente pour nous faciliter la récupération des noms de colonnes des variables
numériques
Appliquer des fonctions
pandas est, comme on a pu le voir, un package très flexible, qui
propose une grande variété de méthodes optimisées. Cependant, il est fréquent
d’avoir besoin de méthodes non implémentées.
Dans ce cas, on recourt souvent aux lambda functions. Par exemple, si
on désire connaître les communes dont le nom fait plus de 40 caractères,
on peut appliquer la fonction len de manière itérative:
# Noms de communes superieurs à 40 caracteresdf[df['Commune'].apply(lambda s: len(s)>40)]
INSEE commune
Commune
Agriculture
Autres transports
Autres transports international
CO2 biomasse hors-total
Déchets
Energie
Industrie hors-énergie
Résidentiel
Routier
Tertiaire
dep
28082
70058
BEAUJEU-SAINT-VALLIER-PIERREJUX-ET-QUITTEUR
4024.909815
736.948351
41.943384
1253.135313
125.101996
2.354558
6.911213
549.734302
1288.215480
452.693897
70
4984
14621
SAINT-MARTIN-DE-BIENFAITE-LA-CRESSONNIERE
1213.333523
NaN
NaN
677.571743
72.072503
63.573059
186.602760
298.261044
1396.353375
260.801452
14
19276
51513
SAINT-REMY-EN-BOUZEMONT-SAINT-GENEST-ET-ISSON
1927.401921
NaN
NaN
595.583152
71.675773
4.709115
13.822427
273.826687
521.864748
259.365848
51
5402
16053
BORS (CANTON DE BAIGNES-SAINTE-RADEGONDE)
1919.249545
NaN
NaN
165.443226
16.265904
2.354558
6.911213
54.561623
719.293151
58.859777
16
Cependant, toutes les lambda functions ne se justifient pas.
Par exemple, prenons
le résultat d’agrégation précédent. Imaginons qu’on désire avoir les résultats
en milliers de tonnes. Dans ce cas, le premier réflexe est d’utiliser
la lambda function suivante :
La méthode div est en moyenne plus rapide et a un temps d’exécution
moins variable. Dans ce cas, on pourrait même utiliser le principe
du broadcasting de numpy (cf. chapitre numpy) qui offre
des performances équivalentes:
apply est plus rapide qu’une boucle (en interne, apply utilise Cython
pour itérer) mais reste moins rapide qu’une solution vectorisée quand
elle existe. Ce site
propose des solutions, par exemple les méthodes isin ou digitize, pour
éviter de manuellement créer des boucles lentes.
En particulier, il faut noter que apply avec le paramètre axis=1 est en générale lente.
Joindre des données
Il est commun de devoir combiner des données issues de sources différentes.
Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation
où une information permet d’apparier de manière exacte deux bases de données (autrement nous
serions dans une situation, beaucoup plus complexe, d’appariement flou1).
La situation typique est l’appariement entre deux sources de données selon un identifiant
individuel. Ici, il s’agit d’un identifiant de code commune.
On utilise de manière indifférente les termes merge ou join.
Le deuxième terme provient de la syntaxe SQL.
En Pandas, dans la plupart des cas, on peut utiliser indifféremment df.join et df.merge
Il est aussi possible de réaliser un merge en utilisant la fonction pandas.concat() avec axis=1.
Se référer à la documentation de concat pour voir les options possibles.
Restructurer des données (reshape)
On présente généralement deux types de données :
format wide : les données comportent des observations répétées, pour un même individu (ou groupe), dans des colonnes différentes
format long : les données comportent des observations répétées, pour un même individu, dans des lignes différentes avec une colonne permettant de distinguer les niveaux d’observations
Un exemple de la distinction entre les deux peut être emprunté à l’ouvrage de référence d’Hadley Wickham, R for Data Science :
L’aide mémoire suivante aidera à se rappeler les fonctions à appliquer si besoin :
Le fait de passer d’un format wide au format long (ou vice-versa) peut être extrêmement pratique car
certaines fonctions sont plus adéquates sur une forme de données ou sur l’autre.
En règle générale, avec Python comme avec R, les formats long sont souvent préférables.
Le chapitre suivant, qui fait office de TP, proposera des applications de ces principes :
Les pipe
En général, dans un projet, le nettoyage de données va consister en un ensemble de
méthodes appliquées à un pandas.DataFrame.
On a vu que assign permettait de créer une variable dans un DataFrame.
Il est également possible d’appliquer une fonction, appelée par exemple my_udf au
DataFrame grâce à pipe:
df = (pd.read_csv(path2data) .pipe(my_udf))
L’utilisation des pipe rend le code très lisible et peut être très
pratique lorsqu’on enchaine des opérations sur le même
dataset.
Quelques enjeux de performance
La librairie Dask intègre la structure de numpy, pandas et sklearn.
Elle a vocation à traiter de données en grande dimension, ainsi elle ne sera pas
optimale pour des données qui tiennent très bien en RAM.
Il s’agit d’une librairie construite sur la parallélisation.
Un chapitre dans ce cours lui est consacré.
Pour aller plus loin, se référer à la documentation de Dask.
Sur l’appariement flou, se reporter aux chapitres présentant ElasticSearch.↩︎
Citation
BibTeX citation:
@book{galiana2023,
author = {Galiana, Lino},
title = {Python Pour La Data Science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {en}
}