Introduction à Pandas

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…).

Tutoriel
Manipulation
Author

Lino Galiana

Published

2024-04-27

Pour essayer les exemples présents dans ce tutoriel :

Le package Pandas 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 mais méritaient d’être réagencées pour convenir aux besoins des data scientists. 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. Les deux prochains chapitres, celui-ci et le suivant, vont présenter quelques éléments structurants de ce package. Ce chapitre prend la forme d’un tutoriel alors que l’exercice suivant est plutôt un ensemble d’exercices. A l’issue du chapitre précédent, grâce à des croisements de données, nous diposerons d’une base fine sur les empreintes carbones des Français1.

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 :

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 :

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

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 :

Cheasheet Pandas

Cheasheet Pandas

1 Logique 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.

Un DataFrame est composé des éléments suivants :

  • l’indice de la ligne ;
  • le nom de la colonne ;
  • la valeur de la donnée ;

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 des données tidy 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.

Illustration du concept de tidy data (emprunté à H. Wickham)

Illustration du concept de tidy data (emprunté à H. Wickham)

Pandas propose ainsi un schéma de données assez familier aux utilisateurs de logiciels statistiques comme R. A l’instar des principaux paradigmes de traitement de la données comme le tidyverse (R), la grammaire de Pandas est héritière de la logique SQL. La philosophie est très proche : on effectue des opérations de sélection de ligne, de colonne, des tris de ligne en fonction de valeurs de certaines colonnes, des traitements standardisés sur des variables, etc. De manière générale, on privilégie les traitements faisant appel à des noms de variables à des numéros de ligne ou de colonne.

Les principales manipulations sont les suivantes:

Sélectionner des colonnes Renommer des colonnes

Créer de nouvelles colonnes Sélectionner des lignes

Réordonner le DataFrame

Réordonner le DataFrame

Que vous soyez familiers de SQL ou de R, vous retrouverez une logique similaire à celle que vous connaissez quoique les noms puissent diverger: df.loc[df['y']=='b'] s’écrira peut-être df %>% filter(y=='b') (R) ou SELECT * FROM df WHERE y == 'b' (SQL) mais la logique est la même.

Pandas propose énormément de fonctionnalités pré-implémentées. Il est vivement recommandé, avant de se lancer dans l’écriture d’uneA 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.

2 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.

2.1 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.0, 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 exercices :

Download nbviewer Onyxia Onyxia
Open In Colab githubdev

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 array numpy 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.

2.2 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 + 1 for i in range(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 + 1 for i in range(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().

3 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.

Un DataFrame non-indexé a la structure suivante :

df = pd.DataFrame(
    {"taille": [1.0, 1.5, 1], "poids": [3, 5, 2.5]}, index=["chat", "chien", "koala"]
)
df.reset_index()
index taille poids
0 chat 1.0 3.0
1 chien 1.5 5.0
2 koala 1.0 2.5

Alors que le même DataFrame indexé aura la structure suivante :

df = pd.DataFrame(
    {"taille": [1.0, 1.5, 1], "poids": [3, 5, 2.5]}, index=["chat", "chien", "koala"]
)
df.head()
taille poids
chat 1.0 3.0
chien 1.5 5.0
koala 1.0 2.5

3.1 Les attributs et méthodes utiles

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 chapitre d’exercice (prochain chapitre).

L’import de données depuis un fichier plat se fait avec la fonction read_csv:

df = pd.read_csv(
    "https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert"
)
df
INSEE commune Commune Agriculture Autres transports Autres transports international CO2 biomasse hors-total Déchets Energie Industrie hors-énergie Résidentiel Routier Tertiaire
0 01001 L'ABERGEMENT-CLEMENCIAT 3711.425991 NaN NaN 432.751835 101.430476 2.354558 6.911213 309.358195 793.156501 367.036172
1 01002 L'ABERGEMENT-DE-VAREY 475.330205 NaN NaN 140.741660 140.675439 2.354558 6.911213 104.866444 348.997893 112.934207
2 01004 AMBERIEU-EN-BUGEY 499.043526 212.577908 NaN 10313.446515 5314.314445 998.332482 2930.354461 16616.822534 15642.420313 10732.376934
3 01005 AMBERIEUX-EN-DOMBES 1859.160954 NaN NaN 1144.429311 216.217508 94.182310 276.448534 663.683146 1756.341319 782.404357
4 01006 AMBLEON 448.966808 NaN NaN 77.033834 48.401549 NaN NaN 43.714019 398.786800 51.681756
... ... ... ... ... ... ... ... ... ... ... ... ...
35793 95676 VILLERS-EN-ARTHIES 1628.065094 NaN NaN 165.045396 65.063617 11.772789 34.556067 176.098160 309.627908 235.439109
35794 95678 VILLIERS-ADAM 698.630772 NaN NaN 1331.126598 111.480954 2.354558 6.911213 1395.529811 18759.370071 403.404815
35795 95680 VILLIERS-LE-BEL 107.564967 NaN NaN 8367.174532 225.622903 534.484607 1568.845431 22613.830247 12217.122402 13849.512001
35796 95682 VILLIERS-LE-SEC 1090.890170 NaN NaN 326.748418 108.969749 2.354558 6.911213 67.235487 4663.232127 85.657725
35797 95690 WY-DIT-JOLI-VILLAGE 1495.103542 NaN NaN 125.236417 97.728612 4.709115 13.822427 117.450851 504.400972 147.867245

35798 rows × 12 columns

L’affichage des DataFrames dans les notebooks est assez ergonomique. Les premières et dernières lignes s’affichent automatiquement. Pour des tables de valorisation présentes dans un rapport ou un article de recherche, le chapitre suivant présente great_tables qui offre de très riches fonctionnalités de mise en forme des tableaux.

Pour afficher une partie ciblée des données, 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.

3.2 Dimensions et structure du DataFrame

Les premières méthodes utiles permettent d’afficher quelques attributs d’un DataFrame.

df.axes
[RangeIndex(start=0, stop=35798, step=1),
 Index(['INSEE commune', 'Commune', 'Agriculture', 'Autres transports',
        'Autres transports international', 'CO2 biomasse hors-total', 'Déchets',
        'Energie', 'Industrie hors-énergie', 'Résidentiel', 'Routier',
        'Tertiaire'],
       dtype='object')]
df.columns
Index(['INSEE commune', 'Commune', 'Agriculture', 'Autres transports',
       'Autres transports international', 'CO2 biomasse hors-total', 'Déchets',
       'Energie', 'Industrie hors-énergie', 'Résidentiel', 'Routier',
       'Tertiaire'],
      dtype='object')
df.index
RangeIndex(start=0, stop=35798, step=1)

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

Comme évoqué précédemment, 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)]

3.3 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

En premier lieu, on peut faire une somme, pour chaque variable, des valeurs présentes dans celle-ci. Pandas implémente celle-ci par le biais de la méthode sum:

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

Il est également possible de faire une moyenne:

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

A noter que les valeurs manquantes sont automatiquement éliminées de la statistique. Pour connaître le nombre de valeurs non manquantes pour chaque colonne, la méthode count est disponible:

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

Cependant, cette dernière ne distingue pas les valeurs dupliquées. Parfois, il est pratique de connaître le nombre de valeurs uniques d’une variable afin de savoir si elle dispose de beaucoup de modalités ou non:

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

Les méthodes statistiques préimplementées vont au-delà de simples sommes ou moyennes. Il est possible de faire, par exemple, des calculs de quantiles sur les différentes colonnes d’un jeu de données

df.quantile(q=[0.1, 0.25, 0.5, 0.75, 0.9], numeric_only=True)
Agriculture Autres transports Autres transports international CO2 biomasse hors-total Déchets Energie Industrie hors-énergie Résidentiel Routier Tertiaire
0.10 382.620882 25.034578 4.430792 109.152816 14.811230 2.354558 6.911213 50.180933 199.765410 49.289082
0.25 797.682631 52.560412 10.050967 197.951108 25.655166 2.354558 6.911213 96.052911 419.700460 94.749885
0.50 1559.381285 106.795928 19.924343 424.849988 54.748653 4.709115 13.822427 227.091193 1070.895593 216.297718
0.75 3007.883903 237.341501 32.983111 1094.749825 110.820941 51.800270 152.046694 749.469293 3098.612157 576.155869
0.90 5442.727470 528.349529 59.999169 3143.759029 190.695774 367.311008 1154.172630 2937.699671 8151.047248 1897.732565

Le résultat est un tableau en 2 dimensions: une ligne par statistique demandée pour chaque colonne numérique de notre jeu de données. Cela peut permettre de se représenter rapidement la distribution des données. Néanmoins c’est un produit assez brut ; pour avoir une meilleure compréhension de la distribution de chaque colonne, une table stylisée (obtenue par great_tables) ou un graphique est probablement plus pertinent.

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.

Le tableau suivant récapitule le code équivalent pour avoir des statistiques sur toutes les colonnes d’un dataframe en R.

Opération pandas dplyr (R) data.table (R)
Nombre de valeurs non manquantes df.count() df %>% summarise_each(funs(sum(!is.na(.)))) df[, lapply(.SD, function(x) sum(!is.na(x)))]
Moyenne de toutes les variables df.mean() df %>% summarise_each(funs(mean((., na.rm = TRUE)))) df[,lapply(.SD, function(x) mean(x, na.rm = TRUE))]

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

3.4 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().

4 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 :

df["Déchets"].plot()

Par défaut, la visualisation obtenue est une série. Ce n’est pas forcément celle attendue puisqu’elle n’est adaptée qu’à des données temporelles. Souvent, on s’intéresse à un histogramme. Pour cela, il suffit d’ajouter l’argument kind = 'hist':

df["Déchets"].hist()

Avec des données dont la distribution est non normalisée, celui-ci peut être peu instructif. Le log peut être une solution pour remettre à une échelle comparable certaines valeurs extrêmes:

df["Déchets"].plot(kind="hist", logy=True)

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). Cependant, il s’agit d’une méthode rapide pour la construction de figures qui nécessite du travail pour une visualisation finalisée. Cela passe par un travail approfondi sur l’objet matplotlib ou l’utilisation d’une librairie plus haut niveau pour la représentation graphique (seaborn, plotnine, plotly, etc.) Le chapitre consacré à la visualisation présentera succinctement ces différents paradigmes de visualisation. Ceux-ci ne dispensent pas de faire preuve de bon sens dans le choix du graphique utilisé pour représenter une statistique descriptive (cf. cette conférence d’Eric Mauvière ).

5 Accéder à des éléments d’un DataFrame

5.1 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 ou caractères spéciaux, ce qui exclut souvent des jeux de données réels. Elle n’est pas recommandée.
  • 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.

5.2 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.iloc : utilise les indices. C’est une méthode moyennement fiable car les indices d’un DataFrame peuvent évoluer au cours d’un traitement (notamment lorsqu’on fait des opérations par groupe).
  • df.loc : utilise les labels. Cette méthode est recommandée.

iloc va se référer à l’indexation de 0 à NN est égal à df.shape[0] d’un pandas.DataFrame. loc va se référer aux valeurs de l’index de df.

Par exemple, avec le pandas.DataFrame df_example:

df_example = pd.DataFrame(
    {"month": [1, 4, 7, 10], "year": [2012, 2014, 2013, 2014], "sale": [55, 40, 84, 31]}
)
df_example = df_example.set_index("month")
df_example
year sale
month
1 2012 55
4 2014 40
7 2013 84
10 2014 31
  • 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.

6 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 table1.merge(table2, left_on = 'id', right_on = 'x') table1 %>% inner_join(table2, by = c('id'='x')) merge(table1, table2, by.x = 'id', by.y = 'x')

6.1 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 mises en oeuvre. L’opération la plus classique consiste à ajouter ou retirer des variables à la table de données.

df_new = df.copy()

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 in vars]] = 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:

df_new.assign(Energie_log=lambda x: np.log(x["Energie"]))
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

Dans les méthodes suivantes, il est possible de modifier le pandas.DataFrame en 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):

df_new = df_new.rename({"Energie": "eneg", "Agriculture": "agr"}, 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"])

6.2 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

df = df.sort_values("Résidentiel", ascending=False)

Ainsi, en une ligne de code, on identifie les villes où le secteur résidentiel consomme le plus.

6.3 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:

df[df["INSEE commune"].str.startswith("92")].head(2)
INSEE commune Commune Agriculture Autres transports Autres transports international CO2 biomasse hors-total Déchets Energie Industrie hors-énergie Résidentiel Routier Tertiaire
35494 92012 BOULOGNE-BILLANCOURT NaN 1250.483441 34.234669 51730.704250 964.828694 8817.818741 25882.493998 92216.971456 64985.280901 60349.109482
35501 92025 COLOMBES NaN 411.371588 14.220061 53923.847088 698.685861 12855.885267 50244.664227 87469.549463 52070.927943 41526.600867

Pour remplacer des valeurs spécifiques, on utilise la méthode where ou une réassignation couplée à la méthode précédente.

Par exemple, pour assigner des valeurs manquantes aux départements du 92, on peut faire cela

df_copy = df.copy()
df_copy = df_copy.where(~df["INSEE commune"].str.startswith("92"))

et vérifier les résultats:

df_copy[df["INSEE commune"].str.startswith("92")].head(2)
df_copy[~df["INSEE commune"].str.startswith("92")].head(2)
INSEE commune Commune Agriculture Autres transports Autres transports international CO2 biomasse hors-total Déchets Energie Industrie hors-énergie Résidentiel Routier Tertiaire
12167 31555 TOULOUSE 1434.045233 4482.980062 130.792683 576394.181208 88863.732538 91549.914092 277062.573234 410675.902028 586054.672836 288175.400126
16774 44109 NANTES 248.019465 138738.544337 250814.701179 193478.248177 18162.261628 17461.400209 77897.138554 354259.013785 221068.632724 173447.582779

ou alors utiliser une réassignation plus classique:

df_copy = df.copy()
df_copy[df_copy["INSEE commune"].str.startswith("92")] = np.nan

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

6.4 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 pandas ne 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

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 :

SELECT dep, count(INSEE commune)
FROM df
GROUP BY dep;

Ce qui, en pandas, donne:

df.groupby("dep")["INSEE commune"].count()
dep
01    410
02    805
03    318
04    199
05    168
     ... 
91    196
92     36
93     40
94     47
95    185
Name: INSEE commune, Length: 96, dtype: int64

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 0x7f16ca495410>

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:

numeric_columns = df.select_dtypes(["number"]).columns
df.loc[:, numeric_columns.tolist() + ["dep"]].groupby("dep").agg(
    ["min", "median", "max"], numeric_only=True
)
Agriculture Autres transports Autres transports international CO2 biomasse hors-total ... Industrie hors-énergie Résidentiel Routier Tertiaire
min median max min median max min median max min ... max min median max min median max min median max
dep
01 0.003432 1304.519570 14402.057335 3.307596 75.686090 617.281080 0.297256 6.985161 2.209492e+01 30.571400 ... 175185.892467 9.607822 351.182294 57689.832901 20.848982 1598.934149 45258.256406 10.049230 401.490676 30847.366865
02 0.391926 1205.725078 13257.716591 0.326963 130.054615 1126.961565 0.517437 15.492120 5.799402e+01 28.294993 ... 220963.067245 7.849347 138.819865 99038.124236 22.936184 700.826152 49245.101730 6.220952 130.639994 34159.345750
03 5.041238 5382.194339 24912.249269 24.158870 144.403590 1433.217868 29.958027 42.762328 8.269019e+01 44.825515 ... 154061.446374 19.441088 217.959697 75793.882483 120.667614 1426.905646 40957.845304 17.705787 191.892445 31099.772884
04 30.985972 1404.752852 11423.535554 33.513854 158.780500 362.637639 NaN NaN NaN 7.162928 ... 16889.531061 1.708652 133.130946 18088.189529 30.206298 687.390045 31438.078325 0.957070 122.504902 16478.024806
05 38.651727 1520.896526 13143.465812 0.299734 139.754980 456.042002 NaN NaN NaN 20.931602 ... 4271.129851 6.871678 211.945147 46486.555748 57.132270 958.506314 37846.651181 4.785348 151.695524 23666.235898
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
91 0.400740 516.908303 5965.349174 25.785594 177.177127 513140.971691 1.651873 14.762210 7.858782e+05 41.661474 ... 50288.560827 15.886514 2580.902085 48464.979708 20.260110 3610.009634 72288.020125 36.368643 1428.426303 38296.204729
92 0.073468 6.505185 32.986132 7.468879 297.529178 1250.483441 1.104401 11.482381 3.423467e+01 2173.614704 ... 95840.512400 4122.277198 33667.904692 92216.971456 4968.382962 23516.458236 113716.853033 800.588678 18086.633085 65043.364499
93 3.308495 3.308495 1362.351634 24.188172 320.755486 45251.869710 0.171075 12.449476 1.101146e+06 899.762120 ... 89135.302368 4364.038661 31428.227282 87927.730552 1632.496185 22506.758771 193039.792609 2257.370945 20864.923339 71918.163984
94 1.781885 1.781885 556.939161 6.249609 294.204166 103252.271268 0.390223 14.944807 1.571965e+05 928.232154 ... 96716.055178 2668.358896 24372.900300 100948.169898 1266.101605 19088.651049 97625.957714 1190.115985 14054.223449 58528.623477
95 8.779506 445.279844 2987.287417 1.749091 80.838639 44883.982753 0.201508 13.149987 1.101131e+06 13.490977 ... 66216.914749 11.585833 1434.343631 104543.465908 2.619451 3417.197938 147040.904455 11.484835 725.467969 61497.821477

96 rows × 30 columns

La première ligne est présente pour nous faciliter la récupération des noms de colonnes des variables numériques

6.5 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 caracteres
df[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 :

numeric_columns = df.select_dtypes(["number"]).columns
(
    df.loc[:, numeric_columns.tolist() + ["dep"]]
    .groupby("dep")
    .agg(["min", "median", "max"])
    .apply(lambda s: s / 1000)
)
Agriculture Autres transports Autres transports international CO2 biomasse hors-total ... Industrie hors-énergie Résidentiel Routier Tertiaire
min median max min median max min median max min ... max min median max min median max min median max
dep
01 0.000003 1.304520 14.402057 0.003308 0.075686 0.617281 0.000297 0.006985 0.022095 0.030571 ... 175.185892 0.009608 0.351182 57.689833 0.020849 1.598934 45.258256 0.010049 0.401491 30.847367
02 0.000392 1.205725 13.257717 0.000327 0.130055 1.126962 0.000517 0.015492 0.057994 0.028295 ... 220.963067 0.007849 0.138820 99.038124 0.022936 0.700826 49.245102 0.006221 0.130640 34.159346
03 0.005041 5.382194 24.912249 0.024159 0.144404 1.433218 0.029958 0.042762 0.082690 0.044826 ... 154.061446 0.019441 0.217960 75.793882 0.120668 1.426906 40.957845 0.017706 0.191892 31.099773
04 0.030986 1.404753 11.423536 0.033514 0.158781 0.362638 NaN NaN NaN 0.007163 ... 16.889531 0.001709 0.133131 18.088190 0.030206 0.687390 31.438078 0.000957 0.122505 16.478025
05 0.038652 1.520897 13.143466 0.000300 0.139755 0.456042 NaN NaN NaN 0.020932 ... 4.271130 0.006872 0.211945 46.486556 0.057132 0.958506 37.846651 0.004785 0.151696 23.666236
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
91 0.000401 0.516908 5.965349 0.025786 0.177177 513.140972 0.001652 0.014762 785.878155 0.041661 ... 50.288561 0.015887 2.580902 48.464980 0.020260 3.610010 72.288020 0.036369 1.428426 38.296205
92 0.000073 0.006505 0.032986 0.007469 0.297529 1.250483 0.001104 0.011482 0.034235 2.173615 ... 95.840512 4.122277 33.667905 92.216971 4.968383 23.516458 113.716853 0.800589 18.086633 65.043364
93 0.003308 0.003308 1.362352 0.024188 0.320755 45.251870 0.000171 0.012449 1101.145545 0.899762 ... 89.135302 4.364039 31.428227 87.927731 1.632496 22.506759 193.039793 2.257371 20.864923 71.918164
94 0.001782 0.001782 0.556939 0.006250 0.294204 103.252271 0.000390 0.014945 157.196520 0.928232 ... 96.716055 2.668359 24.372900 100.948170 1.266102 19.088651 97.625958 1.190116 14.054223 58.528623
95 0.008780 0.445280 2.987287 0.001749 0.080839 44.883983 0.000202 0.013150 1101.131222 0.013491 ... 66.216915 0.011586 1.434344 104.543466 0.002619 3.417198 147.040904 0.011485 0.725468 61.497821

96 rows × 30 columns

En effet, cela effectue le résultat désiré. Cependant, il y a mieux : utiliser la méthode div:

import timeit
df_numeric = df.loc[:, numeric_columns.tolist() + ["dep"] ]
%timeit df_numeric.groupby('dep').agg(['min',"median","max"]).div(1000)
%timeit df_numeric.groupby('dep').agg(['min',"median","max"]).apply(lambda s: s/1000)

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:

%timeit df_numeric.groupby('dep').agg(['min',"median","max"])/1000

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.

7 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 flou2).

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.

Il est recommandé de lire ce guide assez complet sur la question des jointures avec R qui donne des recommandations également utiles pour un utilisateur de Python.

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.

8 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 :

Download nbviewer Onyxia Onyxia
Open In Colab githubdev

9 Pandas dans une chaîne d’opérations

9.1 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.

9.2 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.

Références

  • Le site pandas.pydata fait office de référence

  • Le livre Modern Pandas de Tom Augspurger : https://tomaugspurger.github.io/modern-1-intro.html

McKinney, Wes. 2012. Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython. " O’Reilly Media, Inc.".

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-04-27

Python version used:

'3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.12.2
aiohttp 3.9.3
aioitertools 0.11.0
aiosignal 1.3.1
alembic 1.13.1
aniso8601 9.0.1
annotated-types 0.6.0
appdirs 1.4.4
archspec 0.2.3
astroid 3.1.0
asttokens 2.4.1
attrs 23.2.0
Babel 2.14.0
bcrypt 4.1.2
beautifulsoup4 4.12.3
black 24.4.2
blinker 1.7.0
blis 0.7.11
bokeh 3.4.0
boltons 23.1.1
boto3 1.34.51
botocore 1.34.51
branca 0.7.1
Brotli 1.1.0
cachetools 5.3.3
cartiflette 0.0.2
Cartopy 0.23.0
catalogue 2.0.10
cattrs 23.2.3
certifi 2024.2.2
cffi 1.16.0
charset-normalizer 3.3.2
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.16.0
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.3.0
conda-libmamba-solver 24.1.0
conda-package-handling 2.2.0
conda_package_streaming 0.9.0
confection 0.1.4
contextily 1.6.0
contourpy 1.2.1
cryptography 42.0.5
cycler 0.12.1
cymem 2.0.8
cytoolz 0.12.3
dask 2024.4.1
dask-expr 1.0.10
debugpy 1.8.1
decorator 5.1.1
dill 0.3.8
distributed 2024.4.1
distro 1.9.0
docker 7.0.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et-xmlfile 1.1.0
exceptiongroup 1.2.0
executing 2.0.1
fastjsonschema 2.19.1
fiona 1.9.6
flake8 7.0.0
Flask 3.0.2
folium 0.16.0
fontawesomefree 6.5.1
fonttools 4.51.0
frozenlist 1.4.1
fsspec 2023.12.2
GDAL 3.8.4
gensim 4.3.2
geographiclib 2.0
geopandas 0.12.2
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.29.0
graphene 3.3
graphql-core 3.2.3
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.5.0
greenlet 3.0.3
gunicorn 21.2.0
htmltools 0.5.1
hvac 2.1.0
idna 3.6
imageio 2.34.1
importlib_metadata 7.1.0
importlib_resources 6.4.0
inflate64 1.0.0
ipykernel 6.29.3
ipython 8.22.2
ipywidgets 8.1.2
isort 5.13.2
itsdangerous 2.1.2
jedi 0.19.1
Jinja2 3.1.3
jmespath 1.0.1
joblib 1.3.2
jsonpatch 1.33
jsonpointer 2.4
jsonschema 4.21.1
jsonschema-specifications 2023.12.1
jupyter-cache 1.0.0
jupyter_client 8.6.1
jupyter_core 5.7.2
jupyterlab_widgets 3.0.10
kaleido 0.2.1
kiwisolver 1.4.5
kubernetes 29.0.0
langcodes 3.4.0
language_data 1.2.0
lazy_loader 0.4
libmambapy 1.5.7
llvmlite 0.42.0
locket 1.0.0
lxml 5.2.1
lz4 4.3.3
Mako 1.3.2
mamba 1.5.7
mapclassify 2.6.1
marisa-trie 1.1.0
Markdown 3.6
MarkupSafe 2.1.5
matplotlib 3.8.3
matplotlib-inline 0.1.6
mccabe 0.7.0
menuinst 2.0.2
mercantile 1.2.1
mizani 0.11.2
mlflow 2.11.3
mlflow-skinny 2.11.3
msgpack 1.0.7
multidict 6.0.5
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy 1.9.0
mypy-extensions 1.0.0
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.8.1
numba 0.59.1
numpy 1.26.4
oauthlib 3.2.2
opencv-python-headless 4.9.0.80
openpyxl 3.1.2
OWSLib 0.28.1
packaging 23.2
pandas 2.2.1
paramiko 3.4.0
parso 0.8.4
partd 1.4.1
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.3.0
pip 24.0
pkgutil_resolve_name 1.3.10
platformdirs 4.2.0
plotly 5.19.0
plotnine 0.13.5
pluggy 1.4.0
polars 0.20.18
preshed 3.0.9
prometheus_client 0.20.0
prometheus-flask-exporter 0.23.0
prompt-toolkit 3.0.42
protobuf 4.25.3
psutil 5.9.8
ptyprocess 0.7.0
pure-eval 0.2.2
py7zr 0.20.8
pyarrow 15.0.0
pyarrow-hotfix 0.6
pyasn1 0.5.1
pyasn1-modules 0.3.0
pybcj 1.0.2
pycodestyle 2.11.1
pycosat 0.6.6
pycparser 2.21
pycryptodomex 3.20.0
pydantic 2.7.1
pydantic_core 2.18.2
pyflakes 3.2.0
Pygments 2.17.2
PyJWT 2.8.0
pylint 3.1.0
PyNaCl 1.5.0
pynsee 0.1.7
pyOpenSSL 24.0.0
pyparsing 3.1.2
pyppmd 1.1.0
pyproj 3.6.1
pyshp 2.3.1
PySocks 1.7.1
python-dateutil 2.9.0
python-dotenv 1.0.1
python-magic 0.4.27
pytz 2024.1
pyu2f 0.1.5
pywaffle 1.1.0
PyYAML 6.0.1
pyzmq 25.1.2
pyzstd 0.15.10
QtPy 2.4.1
querystring-parser 1.2.4
rasterio 1.3.10
referencing 0.34.0
regex 2023.12.25
requests 2.31.0
requests-cache 1.2.0
requests-oauthlib 2.0.0
rpds-py 0.18.0
rsa 4.9
Rtree 1.2.0
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.1
scikit-image 0.23.2
scikit-learn 1.4.1.post1
scipy 1.13.0
seaborn 0.13.2
setuptools 69.2.0
shapely 2.0.3
six 1.16.0
smart-open 6.4.0
smmap 5.0.0
snuggs 1.4.7
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.4
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.29
sqlparse 0.4.4
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.1
tabulate 0.9.0
tblib 3.0.0
tenacity 8.2.3
texttable 1.7.0
thinc 8.2.3
threadpoolctl 3.4.0
tifffile 2024.4.24
tomli 2.0.1
tomlkit 0.12.4
toolz 0.12.1
topojson 1.8
tornado 6.4
tqdm 4.66.2
traitlets 5.14.2
truststore 0.8.0
typer 0.9.4
typing_extensions 4.11.0
tzdata 2024.1
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.18
wasabi 1.1.2
wcwidth 0.2.13
weasel 0.3.4
webcolors 1.13
webdriver-manager 4.0.1
websocket-client 1.7.0
Werkzeug 3.0.2
wheel 0.43.0
widgetsnbextension 4.0.10
wordcloud 1.9.3
wrapt 1.16.0
xgboost 2.0.3
xlrd 2.0.1
xyzservices 2024.4.0
yarl 1.9.4
yellowbrick 1.5
zict 3.0.0
zipp 3.17.0
zstandard 0.22.0

View file history

SHA Date Author Description
c9f9f8a 2024-04-24 15:09:35 Lino Galiana Dark mode and CSS improvements (#494)
d75641d 2024-04-22 18:59:01 Lino Galiana Editorialisation des chapitres de manipulation de données (#491)
c03aa61 2024-01-16 17:33:18 Lino Galiana Exercice sur les chemins relatifs (#483)
056c606 2023-12-20 20:08:25 linogaliana Change pandas image
005d89b 2023-12-20 17:23:04 Lino Galiana Finalise l’affichage des statistiques Git (#478)
3fba612 2023-12-17 18:16:42 Lino Galiana Remove some badges from python (#476)
1684220 2023-12-02 12:06:40 Antoine Palazzolo Première partie de relecture de fin du cours (#467)
1f23de2 2023-12-01 17:25:36 Lino Galiana Stockage des images sur S3 (#466)
a06a268 2023-11-23 18:23:28 Antoine Palazzolo 2ème relectures chapitres ML (#457)
09654c7 2023-11-14 15:16:44 Antoine Palazzolo Suggestions Git & Visualisation (#449)
cef6a0d 2023-10-18 13:18:46 Lino Galiana Allègement des actions github (#437)
97676f5 2023-10-14 17:56:44 Lino Galiana Du style pour le site (#434)
7221e7b 2023-10-10 14:00:44 Thomas Faria Relecture Thomas TD Pandas (#431)
a771183 2023-10-09 11:27:45 Antoine Palazzolo Relecture TD2 par Antoine (#418)
ac80862 2023-10-07 21:05:25 Lino Galiana Relecture antuki (#427)
7e03cea 2023-10-04 14:07:17 Lino Galiana Clean pandas tutorial and exercises (#417)
e8d0062 2023-09-26 15:54:49 Kim A Relecture KA 25/09/2023 (#412)
154f09e 2023-09-26 14:59:11 Antoine Palazzolo Des typos corrigées par Antoine (#411)
9a4e226 2023-08-28 17:11:52 Lino Galiana Action to check URL still exist (#399)
8082302 2023-08-25 17:48:36 Lino Galiana Mise à jour des scripts de construction des notebooks (#395)
3bdf3b0 2023-08-25 11:23:02 Lino Galiana Simplification de la structure 🤓 (#393)
c312bdc 2023-08-11 18:06:25 Lino Galiana A few controls for Quarto website (#389)
5d4874a 2023-08-11 15:09:33 Lino Galiana Pimp les introductions des trois premières parties (#387)
dde3e93 2023-07-21 22:22:05 Lino Galiana Fix bug on chapter order (#385)
3560f1f 2023-07-21 17:04:56 Lino Galiana Build on smaller sized image (#384)
f146354 2023-07-21 18:15:10 Lino Galiana Update index.qmd
f6dde33 2023-07-18 22:32:00 Lino Galiana Change badges (#376)
143e706 2023-07-18 19:37:28 Lino Galiana Améliore la navigation (#375)
130ed71 2023-07-18 19:37:11 Lino Galiana Restructure les titres (#374)
ef28fef 2023-07-07 08:14:42 Lino Galiana Listing pour la première partie (#369)
64baaf8 2023-07-03 17:05:53 Lino Galiana Script for branch deploy (#367)
f21a24d 2023-07-02 10:58:15 Lino Galiana Pipeline Quarto & Pages 🚀 (#365)
867325e 2023-06-11 13:56:43 Lino Galiana Add numeric_only argument (#359)
9918817 2022-12-30 15:10:59 Lino Galiana Retour sur le chapitre DallE / StableDiffusion (#344)
94e7c0a 2022-12-29 09:42:35 Lino Galiana pip install pynsee (#342)
a8dd720 2022-12-26 21:35:52 Lino Galiana Improve aesthetics on Github (#338)
e2b53ac 2022-09-28 17:09:31 Lino Galiana Retouche les chapitres pandas (#287)
eb8f922 2022-09-22 17:40:43 Lino Galiana Corrige bug TP pandas (#276)
fd439f0 2022-09-19 09:37:50 avouacr fix ssp cloud links
3056d41 2022-09-02 12:19:55 avouacr fix all SSP Cloud launcher links
8042a16 2022-08-24 16:23:36 Lino Galiana Box pour les notebooks :sparkles: (#256)
494a85a 2022-08-05 14:49:56 Lino Galiana Images featured ✨ (#252)
d201e3c 2022-08-03 15:50:34 Lino Galiana Pimp la homepage ✨ (#249)
2360ff7 2022-08-02 16:29:57 Lino Galiana Test wowchemy update (#247)
d3a5406 2022-06-27 17:44:30 Lino Galiana Utilisation test du système de référence de quarto (#240)
1239e3e 2022-06-21 14:05:15 Lino Galiana Enonces (#239)
48606dd 2022-05-31 19:05:11 Lino Galiana Amélioration rendu dataframe pandas (#229)
12965ba 2022-05-25 15:53:27 Lino Galiana :launch: Bascule vers quarto (#226)
9c71d6e 2022-03-08 10:34:26 Lino Galiana Plus d’éléments sur S3 (#218)
5cac236 2021-12-16 19:46:43 Lino Galiana un petit mot sur mercator (#201)
6777f03 2021-10-29 09:38:09 Lino Galiana Notebooks corrections (#171)
2a8809f 2021-10-27 12:05:34 Lino Galiana Simplification des hooks pour gagner en flexibilité et clarté (#166)
5ad057f 2021-10-10 15:13:16 Lino Galiana Relectures pandas & geopandas (#159)
4870662 2021-10-05 08:29:33 Romain Avouac fix and simplify pyinsee install (#157)
0677932 2021-10-03 15:32:51 Lino Galiana Ajoute un code pour download pynsee (#156)
2fa78c9 2021-09-27 11:24:19 Lino Galiana Relecture de la partie numpy/pandas (#152)
85ba119 2021-09-16 11:27:56 Lino Galiana Relectures des TP KA avant 1er cours (#142)
2f4d390 2021-09-02 15:12:29 Lino Galiana Utilise un shortcode github (#131)
2e4d586 2021-09-02 12:03:39 Lino Galiana Simplify badges generation (#130)
4a317e3 2021-08-31 12:38:17 Lino Galiana pynsee pour importer des données Insee 🚀 (#127)
2f7b52d 2021-07-20 17:37:03 Lino Galiana Improve notebooks automatic creation (#120)
6729a72 2021-06-22 18:07:05 Lino Galiana Mise à jour badge onyxia (#115)
4cdb759 2021-05-12 10:37:23 Lino Galiana :sparkles: :star2: Nouveau thème hugo :snake: :fire: (#105)
175d377 2021-05-04 18:29:26 Raphaele Adjerad Quelques manipulations supplémentaires pandas (#106)
7f9f97b 2021-04-30 21:44:04 Lino Galiana 🐳 + 🐍 New workflow (docker 🐳) and new dataset for modelization (2020 🇺🇸 elections) (#99)
0a0d034 2021-03-26 20:16:22 Lino Galiana Ajout d’une section sur S3 (#97)
6d010fa 2020-09-29 18:45:34 Lino Galiana Simplifie l’arborescence du site, partie 1 (#57)
66f9f87 2020-09-24 19:23:04 Lino Galiana Introduction des figures générées par python dans le site (#52)
76e206c 2020-09-09 18:02:08 Lino Galiana Finalisation du chapitre pandas (#24)
5c1e76d 2020-09-09 11:25:38 Lino Galiana Ajout des éléments webscraping, regex, API (#21)
d48e68f 2020-09-08 18:35:07 Lino Galiana Continuer la partie pandas (#13)
85365ca 2020-09-05 14:50:10 linogaliana ajout badges onyxia
611be4d 2020-09-05 14:27:47 linogaliana modifs marginales
0559398 2020-09-05 14:22:55 linogaliana modifs marginales
9c12c2c 2020-09-04 17:39:09 Lino Galiana Introduction à pandas (#11)
Back to top

Footnotes

  1. A vrai dire, ce n’est pas l’empreinte carbone puisque la base de données correspond à une vision production, pas consommation. Les émissions faites dans une commune pour satisfaire la consommation d’une autre seront imputées à la première là où le concept d’empreinte carbone voudrait qu’on l’impute aux secondes. Il ne s’agit pas, avec cet exercice, de construire une statistique fiable mais plutôt de comprendre la logique de l’association de données pour construire des statistiques descriptives.↩︎

  2. 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}
}
For attribution, please cite this work as:
Galiana, Lino. 2023. Python Pour La Data Science. https://doi.org/10.5281/zenodo.8229676.