import numpy as np
import pandas as pd
url = "https://data.ademe.fr/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert"
emissions = pd.read_csv(url)
emissions['dep'] = emissions["INSEE commune"].str[:2]- Connaître les principales limites de la syntaxe de
Pandas; - Savoir chaîner des opérations de manière plus lisible grâce aux pipes ;
- Connaître les principaux packages alternatifs à
Pandas(Polars,DuckDB,Spark).
1 Introduction
Les deux chapitres précédents ont permis d’explorer en profondeur la richesse de l’écosystème Pandas, qu’il s’agisse d’associer des données grâce à des jointures ou de construire des statistiques descriptives et de restructurer des données. Pandas est un indispensable dans la boite à outil du data scientist.
Ce court chapitre propose de prendre un peu de recul. Malgré toutes ses qualités, Pandas n’est pas parfait: sa syntaxe, héritée de plus de quinze ans d’histoire, comporte quelques aspérités. Nous verrons également que d’autres paradigmes, plus récents, permettent d’aller au-delà de Pandas, notamment lorsqu’il s’agit de traiter des volumes de données plus importants.
1.1 Données de ce chapitre
Pour illustrer ce chapitre, nous repartons du jeu de données brut des émissions de gaz à effet de serre de l’Ademe, déjà utilisé dans les chapitres précédents:
Pour obtenir des résultats reproductibles, on peut fixer la racine du générateur pseudo-aléatoire.
np.random.seed(123)2 Pandas dans une chaine d’opérations
En général, dans un projet, le nettoyage de données va consister en un ensemble de
méthodes appliquées à un DataFrame ou alors une Serie lorsqu’on travaille exclusivement sur une colonne.
Autrement dit, ce qui est généralement attendu lorsqu’on fait du Pandas c’est d’avoir une chaîne qui prend un DataFrame en entrée et ressort ce même DataFrame enrichi, ou une version agrégée de celui-ci, en sortie.
Cette manière de procéder est le coeur de la syntaxe dplyr en R mais n’est pas forcément native en Pandas selon les opérations qu’on désire mettre en oeuvre. En effet, la manière naturelle de mettre à jour un dataframe en Pandas passe souvent par une syntaxe du type:
import numpy as np
import pandas as pd
data = [[8000, 1000], [9500, np.nan], [5000, 2000]]
df = pd.DataFrame(data, columns=['salaire', 'autre_info'])
df['salaire_net'] = df['salaire']*0.8En SQL on pourrait directement mettre à jour notre base de données avec la nouvelle colonne :
SELECT *, salaire*0.8 AS salaire_net FROM dfL’écosystème du tidyverse en R, l’équivalent de Pandas, fonctionne selon la même logique que SQL de mise à jour de table. On ferait en effet la commande suivante avec dplyr:
df %>% mutate(salaire_net = salaire*0.8)Techniquement on pourrait faire ceci avec un assign en Pandas
1df = df.drop("salaire_net", axis = "columns")
df = df.assign(salaire_net = lambda s: s['salaire']*0.8)- 1
- Pour effacer la variable afin de repartir de l’exemple initial
Cependant cette syntaxe assign n’est pas très naturelle. Il est nécessaire de lui passer une lambda function qui attend comme input un DataFrame là où on voudrait une colonne. Il ne s’agit donc pas vraiment d’une syntaxe lisible et pratique.
Il est néanmoins possible d’enchaîner des opérations sur des jeux de données grâce aux pipes. Ceux-ci reprennent la même philosophie que celle de dplyr, elle-même inspirée du pipe Linux.
Cette approche permettra de rendre plus lisible le code en définissant des fonctions effectuant des opérations sur une ou plusieurs colonnes d’un DataFrame. Le premier argument à indiquer à la fonction est le DataFrame, les autres sont ceux permettant de contrôler son comportement
def calcul_salaire_net(df: pd.DataFrame, col: str, taux: float = 0.8):
df["salaire_net"] = df[col]*taux
return dfCe qui transforme notre chaine de production en
(
df
.pipe(calcul_salaire_net, "salaire")
)| salaire | autre_info | salaire_net | |
|---|---|---|---|
| 0 | 8000 | 1000.0 | 6400.0 |
| 1 | 9500 | NaN | 7600.0 |
| 2 | 5000 | 2000.0 | 4000.0 |
2.1 Quelques limites sur la syntaxe de Pandas
Il y a un avant et un après Pandas dans l’analyse de données en Python. Sans ce package ô combien pratique Python, malgré toutes les forces de ce langage, aurait eu du mal à s’installer dans le paysage de l’analyse de données. Cependant, si Pandas propose une syntaxe cohérente sur de nombreux aspects, elle n’est pas parfaite non plus. Les paradigmes plus récents d’analyse de données en Python ont d’ailleurs parfois l’ambition de corriger ces imperfections syntaxiques là.
Parmi les points les plus génants au quoditien il y a le besoin de régulièrement faire des reset_index lorsqu’on construit des statistiques descriptives. En effet, il peut être dangereux de garder des indices qu’on ne contrôle pas bien car, sans attention de notre part lors des phases de merge, ils peuvent être utilisés à mauvais escient par Pandas pour joindre les données ce qui peut provoquer des suprises.
Pandas est extrêmement bien fait pour restructurer des données du format long to wide ou wide to long. Cependant, ce n’est pas la seule manière de restructurer un jeu de données qu’on peut vouloir mettre en oeuvre. Il arrive régulièrement qu’on désire comparer la valeur d’une observation à celle d’un groupe à laquelle elle appartient. C’est notamment particulièrement utile dans une phase d’analyse des anomalies, valeurs aberrantes ou lors d’une investigation de détection de fraude. De manière native, en Pandas, il faut construire une statistique agrégée par groupe et refaire un merge aux données initiales par le biais de la variable de groupe. C’est un petit peu fastidieux:
emissions_moyennes = emissions.groupby("dep").agg({"Agriculture": "mean"}).reset_index()
emissions_enrichies = (
emissions
.merge(emissions_moyennes, on = "dep", suffixes = ['', '_moyenne_dep'])
)
emissions_enrichies['relatives'] = emissions_enrichies["Agriculture"]/emissions_enrichies["Agriculture_moyenne_dep"]
emissions_enrichies.head()| INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | dep | Agriculture_moyenne_dep | relatives | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | 309.358195 | 793.156501 | 367.036172 | 01 | 1974.535382 | 1.879645 |
| 1 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | 6.911213 | 104.866444 | 348.997893 | 112.934207 | 01 | 1974.535382 | 0.240730 |
| 2 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | 2930.354461 | 16616.822534 | 15642.420313 | 10732.376934 | 01 | 1974.535382 | 0.252740 |
| 3 | 01005 | AMBERIEUX-EN-DOMBES | 1859.160954 | NaN | NaN | 1144.429311 | 216.217508 | 94.182310 | 276.448534 | 663.683146 | 1756.341319 | 782.404357 | 01 | 1974.535382 | 0.941569 |
| 4 | 01006 | AMBLEON | 448.966808 | NaN | NaN | 77.033834 | 48.401549 | NaN | NaN | 43.714019 | 398.786800 | 51.681756 | 01 | 1974.535382 | 0.227378 |
Dans le tidyverse, cette opération en deux temps pourrait être faite en une seule étape, ce qui est plus pratique
emissions %>%
group_by(dep) %>%
mutate(relatives = Agriculture/mean(Agriculture))Ce n’est pas si grave mais cela alourdit la longueur des chaines de traitement faites en Pandas et donc la charge de maintenance pour les faire durer dans le temps.
De manière plus générale, les chaînes de traitement Pandas peuvent être assez verbeuses, car il faut régulièrement redéfinir le DataFrame qu’on utilise plutôt que simplement les colonnes. Par exemple, pour faire un filtre sur les lignes et les colonnes, il faudra faire:
(
emissions
.loc[
(emissions["dep"] == "12") & (emissions["Routier"]>500), ['INSEE commune', 'Commune']
]
.head(5)
)| INSEE commune | Commune | |
|---|---|---|
| 4058 | 12001 | AGEN-D'AVEYRON |
| 4059 | 12002 | AGUESSAC |
| 4062 | 12006 | ALRANCE |
| 4063 | 12007 | AMBEYRAC |
| 4064 | 12008 | ANGLARS-SAINT-FELIX |
En SQL on pourrait se contenter de faire référence aux colonnes dans le filter
SELECT "INSEE commune", 'Commune'
FROM emissions
WHERE dep=="12" AND Routier>500Dans le tidyverse (R) on pourrait aussi faire ceci simplement
df %>%
filter(dep=="12", Routier>500) %>%
select(`INSEE commune`, `Commune`)3 Les autres paradigmes
Malgré toutes les limites que nous venons d’évoquer, et les solutions alternatives que nous allons présenter, Pandas reste LE package central de l’écosystème de la donnée avec Python. Nous allons voir dans les prochains chapitres son intégration native à l’écosystème Scikit pour le machine learning ou l’extension de Pandas aux données spatiales avec GeoPandas.
Les autres solutions techniques que nous allons ici évoquer peuvent être pertinentes si on désire traiter des volumes de données importants ou si on désire utiliser des syntaxes alternatives.
Les principales alternatives à Pandas sont Polars, DuckDB et Spark. Il existe également Dask, une librairie pour paralléliser des traitements écris en Pandas.
3.1 Polars
Polars est certainement le paradigme le plus inspiré de Pandas, jusqu’au choix du nom. La première différence fondamentale est dans les couches internes utilisées. Polars s’appuie sur l’implémentation Rust de Arrow là où Pandas s’appuie sur Numpy ce qui est facteur de perte de performance. Cela permet à Polars d’être plus efficace sur de gros volumes de données, d’autant que de nombreuses opérations sont parallélisées et reposent sur l’évaluation différées (lazy evaluation) un principe de programmation qui permet d’optimiser les requêtes pour ne pas les exécuter dans l’ordre de définition mais dans un ordre logique plus optimal.
Une autre force de Polars est la syntaxe plus cohérente, qui bénéficie du recul d’une quinzaine d’années d’existence de Pandas et d’une petite dizaine d’années de dplyr (le package de manipulation de données au sein du paradigme du tidyverse en R). Pour reprendre l’exemple précédent, il n’est plus nécessaire de forcer la référence au DataFrame, dans une chaîne d’exécution toutes les références ultérieures seront faites au regard du DataFrame de départ
import polars as pl
emissions_polars = pl.from_pandas(emissions)
(
emissions_polars
.filter(pl.col("dep") == "12", pl.col("Routier") > 500)
.select('INSEE commune', 'Commune')
.head(5)
)| INSEE commune | Commune |
|---|---|
| str | str |
| "12001" | "AGEN-D'AVEYRON" |
| "12002" | "AGUESSAC" |
| "12006" | "ALRANCE" |
| "12007" | "AMBEYRAC" |
| "12008" | "ANGLARS-SAINT-FELIX" |
Pour découvrir Polars, de nombreuses ressources en ligne sont accessibles, notamment ce notebook construit pour le réseau des data scientists de la statistique publique.
3.2 DuckDB
DuckDB est le nouveau venu dans l’écosystème de l’analyse de données repoussant les limites des données pouvant être traitées avec Python sans passer par des outils big data comme Spark.
DuckDB est la quintessence d’un nouveau paradigme, celui du “Big data is dead”, où on peut traiter des données de volumétrie importante sans recourir à des infrastructures imposantes.
Outre sa grande efficacité, puisqu’avec DuckDB on peut traiter des données d’une volumétrie supérieure à la mémoire vive de l’ordinateur ou du serveur, DuckDB présente l’avantage de proposer une syntaxe uniforme quelle que soit le langage qui appelle DuckDB (Python, R, C++ ou Javascript). DuckDB privilégie la syntaxe SQL pour traiter les données avec de nombreuses fonctions pré-implementées pour simplifier certaines transformations de données (par exemple pour les données textuelles, les données temporelles, etc.).
Par rapport à d’autres systèmes s’appuyant sur SQL, comme PostGreSQL, DuckDB est très simple d’installation, ce n’est qu’une librairie Python là où beaucoup d’outils comme PostGreSQL nécessite une infrastructure adaptée.
Pour reprendre l’exemple précédent, on peut utiliser directement le code SQL précédent
import duckdb
duckdb.sql(
"""
SELECT "INSEE commune", "Commune"
FROM emissions
WHERE dep=='12' AND Routier>500
LIMIT 5
""")┌───────────────┬─────────────────────┐
│ INSEE commune │ Commune │
│ varchar │ varchar │
├───────────────┼─────────────────────┤
│ 12001 │ AGEN-D'AVEYRON │
│ 12002 │ AGUESSAC │
│ 12006 │ ALRANCE │
│ 12007 │ AMBEYRAC │
│ 12008 │ ANGLARS-SAINT-FELIX │
└───────────────┴─────────────────────┘
Ici la clause FROM emissions vient du fait qu’on peut directement exécuter du SQL depuis un objet Pandas par le biais de DuckDB. Si on fait la lecture directement dans la requête, celle-ci se complexifie un petit peu mais la logique est la même
import duckdb
duckdb.sql(
f"""
SELECT "INSEE commune", "Commune"
FROM read_csv_auto("{url}")
WHERE
substring("INSEE commune",1,2)=='12'
AND
Routier>500
LIMIT 5
""")┌───────────────┬─────────────────────┐
│ INSEE commune │ Commune │
│ varchar │ varchar │
├───────────────┼─────────────────────┤
│ 12001 │ AGEN-D'AVEYRON │
│ 12002 │ AGUESSAC │
│ 12006 │ ALRANCE │
│ 12007 │ AMBEYRAC │
│ 12008 │ ANGLARS-SAINT-FELIX │
└───────────────┴─────────────────────┘
Le rendu du DataFrame est légèrement différent de Pandas car, comme Polars et de nombreux systèmes de traitement de données volumineuses, DuckDB repose sur l’évaluation différée et donc ne présente en display qu’un échantillon de données.
DuckDB et Polars sont d’ailleurs très bien intégrés l’un à l’autre. On peut très bien faire du SQL sur un objet Polars via DuckDB ou appliquer des fonctions Polars sur un objet initialement lu avec DuckDB.
L’un des intérêts de DuckDB est son excellente intégration avec l’écosystème Parquet, le format de données déjà mentionné qui devient un standard dans le partage de données (il s’agit, par exemple, de la pierre angulaire du partage de données sur la plateforme HuggingFace). Pour en savoir plus sur DuckDB et découvrir son intérêt pour lire les données du recensement de la population française, vous pouvez consulter ce post de blog.
3.3 Spark et le big data
DuckDB a repoussé les frontières du big data qu’on peut définir comme le volume de données à partir duquel on ne peut plus traiter celles-ci sur une machine sans mettre en oeuvre une stratégie de parallélisation.
Néanmoins, pour les données très volumineuses, Python est très bien armé grâce à la librairie PySpark. Celle-ci est une API en Python pour le langage Spark, un langage big data basé sur Scala. Ce paradigme est construit sur l’idée que les utilisateurs de Python y accèdent par le biais de cluster avec de nombreux noeuds pour traiter la donnée de manière parallèle. Celle-ci sera lue par blocs, qui seront traités en parallèle en fonction du nombre de noeuds parallèles. L’API DataFrame de Spark présente une syntaxe proche de celle des paradigmes précédents avec une ingénieurie plus complexe en arrière-plan liée à la parallélisation native.
Informations additionnelles
Ce site a été construit automatiquement par le biais d’une action Github utilisant le logiciel de publication reproductible Quarto
L’environnement utilisé pour obtenir les résultats est reproductible par le biais d’uv. Le fichier pyproject.toml utilisé pour construire cet environnement est disponible sur le dépôt linogaliana/python-datascientist
pyproject.toml
[project]
name = "python-datascientist"
version = "0.1.0"
description = "Source code for Lino Galiana's Python for data science course"
readme = "README.md"
requires-python = ">=3.13,<3.14"
dependencies = [
"altair>=6.0.0",
"cartiflette",
"contextily==1.6.2",
"duckdb>=0.10.1",
"folium>=0.19.6",
"gdal==3.11.4",
"graphviz==0.20.3",
"great-tables>=0.12.0",
"gt-extras>=0.0.8",
"ipykernel>=6.29.5",
"jupyter>=1.1.1",
"jupyter-cache>=1.0.0",
"kaleido>=0.2.1",
"langchain-community>=0.3.27",
"loguru==0.7.3",
"markdown>=3.8",
"nbclient>=0.10.0",
"nbformat>=5.10.4",
"nltk>=3.9.1",
"pandas>=3.0",
"pip>=25.1.1",
"plotly>=6.1.2",
"plotnine>=0.15",
"polars>=1.8.2",
"pyarrow>=17.0.0",
"pynsee>=0.1.8",
"python-dotenv>=1.0.1",
"python-frontmatter>=1.1.0",
"pywaffle>=1.1.1",
"requests>=2.32.3",
"scikit-image>=0.24.0",
"scikit-learn>=1.8.0",
"scipy>=1.13.0",
"seaborn>=0.13.2",
"selenium<4.39.0",
"spacy>=3.8.4",
"webdriver-manager>=4.0.2",
"wordcloud==1.9.3",
]
[tool.uv.sources]
cartiflette = { git = "https://github.com/inseefrlab/cartiflette" }
gdal = [
{ index = "gdal-wheels", marker = "sys_platform == 'linux'" },
{ index = "geospatial_wheels", marker = "sys_platform == 'win32'" },
]
[[tool.uv.index]]
name = "geospatial_wheels"
url = "https://nathanjmcdougall.github.io/geospatial-wheels-index/"
explicit = true
[[tool.uv.index]]
name = "gdal-wheels"
url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/simple"
explicit = true
[dependency-groups]
dev = [
"nb-clean>=4.0.1",
]
Pour utiliser exactement le même environnement (version de Python et packages), se reporter à la documentation d’uv.
| SHA | Date | Author | Description |
|---|---|---|---|
| 56ad5bc8 | 2026-07-14 21:57:39 | Lino Galiana | Données filosofi plus à jour pour le chapitre Pandas (#699) |
Citation
@book{galiana2025,
author = {Galiana, Lino},
title = {Python pour la data science},
date = {2025},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {fr}
}