!pip install xlrd
!pip install pynsee
!pip install great_tables
- Savoir construire des statistiques agrégées fines grâce aux méthodes de
Pandas
; - Savoir restructurer ses données et joindre plusieurs
DataFrames
ensemble ; - Créer des tableaux attractifs pour communiquer sur des résultats agrégés ;
- Connaître les limites de
Pandas
et les packages alternatifs.
1 Introduction
Le chapitre d’introduction à Pandas
a permis de présenter le principe de données organisées sous une forme de DataFrame et la praticité de l’écosystème Pandas
pour effectuer des opérations simples sur un jeu de données.
Il est rare de travailler exclusivement sur une source brute. Un jeu de données prend généralement de la valeur lorsqu’il est comparé à d’autres sources. Pour des chercheurs, cela permettra de contextualiser l’information présente dans une source en la comparant ou en l’associant à d’autres sources. Pour des data scientists dans le secteur privé, il s’agira souvent d’associer des informations sur une même personne dans plusieurs bases clientes ou comparer les clients entre eux.
L’un des apports des outils modernes de data science, notamment Pandas
est la simplicité par laquelle ils permettent de restructurer des sources pour travailler sur plusieurs données sur un projet.
Ce chapitre consolide ainsi les principes vus précédemment en raffinant les traitements faits sur les données. Il va explorer principalement deux types d’opérations:
- les statistiques descriptives par groupe ;
- l’association de données par des caractéristiques communes.
Effectuer ce travail de manière simple, fiable et efficace est indispensable pour les data scientists tant cette tâche est courante. Heureusement Pandas
permet de faire cela très bien avec des données structurées. Nous verrons dans les prochains chapitres, mais aussi dans l’ensemble de la partie sur le traitement des données textuelles, comment faire avec des données moins structurées.
Grâce à ce travail, nous allons approfondir notre compréhension d’un phénomène réel par le biais de statistiques descriptives fines. Cela est une étape indispensable avant de basculer vers la statistique inférentielle, l’approche qui consiste à formaliser et généraliser des liens de corrélation ou de causalité entre des caractéristiques observées et un phénomène.
- Récupérer un jeu de données officiel de l’Insee ;
- Construire des statistiques descriptives par groupe et jongler entre les niveaux des données ;
- Associer des données (reshape, merge) pour leur donner plus de valeur ;
- Faire un beau tableau pour communiquer des statistiques descriptives.
1.1 Environnement
Le chapitre précédent utilisait quasi exclusivement la librairie Pandas
. Nous allons dans ce chapitre utiliser d’autres packages en complément de celui-ci.
Comme expliqué ci-dessous, nous allons utiliser une librairie nommée pynsee
pour récupérer les données de l’Insee utiles à enrichir notre jeu de données de l’Ademe. Cette librairie n’est pas installée par défaut dans Python
. Avant de pouvoir l’utiliser,
il est nécessaire de l’installer, comme la librairie great_tables
que nous verrons à la fin de ce chapitre:
L’instruction !pip install <pkg>
est une manière de faire comprendre à Jupyter
, le moteur d’exécution derrière les notebooks que la commande qui suit (pip install
ce <pkg>
)
est une commande système, à exécuter hors de Python
(dans le terminal par exemple pour un système Linux
).
Les premiers packages indispensables pour démarrer ce chapitre sont les suivants:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pynsee
import pynsee.download
Pour obtenir des résultats reproductibles, on peut fixer la racine du générateur pseudo-aléatoire.
123) np.random.seed(
1.2 Données utilisées
Ce tutoriel continue l’exploration du jeu de données du chapitre précédent:
- 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 ;
Les problématiques d’enrichissement de données (association d’une source à une autre à partir de caractéristiques communes) seront présentées à partir de deux sources produites par l’Insee:
- Le code officiel géographique, un référentiel produit par l’Insee utilisé pour identifier les communes à partir d’un code unique, contrairement au code postal ;
- Les données Filosofi, une source sur les revenus des Français à une échelle spatiale fine construite par l’Insee à partir des déclarations fiscales et d’informations sur les prestations sociales. En l’occurrence, nous allons utiliser les niveaux de revenu et les populations1 au niveau communal afin de les mettre en regard de nos données d’émissions.
Pour faciliter l’import de données Insee, il est recommandé d’utiliser le package
pynsee
qui simplifie l’accès aux principaux jeux de données
de l’Insee disponibles sur le site web insee.fr
ou via des API.
Le package pynsee
comporte deux principaux points d’entrée :
- Les API de l’Insee, ce qui sera illustré dans le chapitre consacré.
- Quelques jeux de données directement issus du site web de l’Insee (insee.fr)
Dans ce chapitre, nous allons exclusivement utiliser cette deuxième
approche. Cela se fera par le module pynsee.download
.
La liste des données disponibles depuis ce package est ici.
La fonction download_file
attend un identifiant unique
pour savoir quelle base de données aller chercher et
restructurer depuis le
site insee.fr.
Connaître la liste des bases disponibles
Pour connaître la liste des bases disponibles, vous
pouvez utiliser la fonction meta = pynsee.get_file_list()
après avoir fait import pynsee
.
Celle-ci renvoie un DataFrame
dans lequel on peut
rechercher, par exemple grâce à une recherche
de mots-clefs :
import pynsee
= pynsee.get_file_list()
meta "label"].str.contains(r"Filosofi.*2016")] meta.loc[meta[
id | name | label | collection | link | type | zip | big_zip | data_file | tab | ... | label_col | date_ref | meta_file | separator | type_col | long_col | val_col | encoding | last_row | missing_value | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
79 | FILOSOFI_COM_2016 | FILOSOFI_COM | Données Filosofi niveau communal – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | COM | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
80 | FILOSOFI_EPCI_2016 | FILOSOFI_EPCI | Données Filosofi niveau EPCI – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | EPCI | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
81 | FILOSOFI_ARR_2016 | FILOSOFI_ARR | Données Filosofi niveau arondissement – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | ARR | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
82 | FILOSOFI_DEP_2016 | FILOSOFI_DEP | Données Filosofi niveau départemental – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | DEP | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
83 | FILOSOFI_REG_2016 | FILOSOFI_REG | Données Filosofi niveau régional – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | REG | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
84 | FILOSOFI_METRO_2016 | FILOSOFI_METRO | Données Filosofi niveau France métropolitaine ... | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | METRO | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
85 | FILOSOFI_AU2010_2016 | FILOSOFI_AU2010 | Données Filosofi niveau aire urbaine – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | AU2010 | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
86 | FILOSOFI_UU2010_2016 | FILOSOFI_UU2010 | Données Filosofi niveau unité urbaine – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | UU2010 | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
87 | FILOSOFI_ZE2010_2016 | FILOSOFI_ZE2010 | Données Filosofi niveau zone d’emploi – 2016 | FILOSOFI | https://www.insee.fr/fr/statistiques/fichier/4... | xls | True | False | base-cc-filosofi-2016.xls | ZE2010 | ... | NaN | 2016-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
9 rows × 24 columns
Ici, meta['label'].str.contains(r"Filosofi.*2016")
signifie:
“pandas
trouve moi tous les labels où sont contenus les termes Filosofi et 2016.”
(.*
signifiant “peu m’importe le nombre de mots ou caractères entre”)
2 Récupération des jeux de données
2.1 Données d’émission de l’Ademe
Comme expliqué au chapitre précédent, ces données peuvent être importées très simplement avec Pandas
import pandas as pd
= "https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert"
url = pd.read_csv(url)
emissions 2) emissions.head(
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 |
Nous allons d’ores et déjà conserver le nom des secteurs émetteurs présents dans la base de données pour simplifier des utilisations ultérieures:
= emissions.select_dtypes(include="number").columns secteurs
Les exploitations ultérieures de ces données utiliseront la dimension départementale dont nous avons montré la construction au chapitre précédent:
"dep"] = emissions["INSEE commune"].str[:2] emissions[
2.2 Données Filosofi
On va utiliser les données Filosofi (données de revenus) au niveau communal de 2016.
Ce n’est pas la même année que les données d’émission de CO2, ce n’est donc pas parfaitement rigoureux,
mais cela permettra tout de même d’illustrer
les principales fonctionnalités de Pandas
Le point d’entrée principal de la fonction pynsee
est la fonction download_file
.
Le code pour télécharger les données est le suivant :
from pynsee.download import download_file
= download_file("FILOSOFI_COM_2016") filosofi
Le DataFrame en question a l’aspect suivant :
3) filosofi.sample(
CODGEO | LIBGEO | NBMENFISC16 | NBPERSMENFISC16 | MED16 | PIMP16 | TP6016 | TP60AGE116 | TP60AGE216 | TP60AGE316 | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
34640 | 93077 | Villemomble | 11968 | 30123.5 | 21782.222222222223 | 61 | 19 | 22 | 23 | 19 | ... | 23.1 | 9 | 5.1 | 2.1 | 1.7 | 1.3 | -19.6 | 9371.481481481482 | 40950.8 | 4.36972532901237 |
9988 | 28073 | Champseru | 121 | 326.5 | 24806 | None | None | None | None | None | ... | None | None | None | None | None | None | None | None | None | None |
17537 | 48190 | Termes | 92 | 210.5 | 19108.571428571428 | None | None | None | None | None | ... | None | None | None | None | None | None | None | None | None | None |
3 rows × 29 columns
Pandas
a géré automatiquement les types de variables. Il le fait relativement bien, mais une vérification est toujours utile pour les variables qui ont un statut spécifique.
Pour les variables qui ne sont pas en type float
alors qu’elles devraient l’être, on modifie leur type.
= filosofi.astype({c: "float" for c in filosofi.columns[2:]}) filosofi
Un simple coup d’oeil sur les données
donne une idée assez précise de la manière dont les données sont organisées.
On remarque que certaines variables de filosofi
semblent avoir beaucoup de valeurs manquantes (secret statistique)
alors que d’autres semblent complètes.
Si on désire exploiter filosofi
, il faut faire attention à la variable choisie.
Notre objectif à terme va être de relier l’information contenue entre ces
deux jeux de données. En effet, sinon, nous risquons d’être frustré : nous allons
vouloir en savoir plus sur les émissions de gaz carbonique mais seront très
limités dans les possibilités d’analyse sans ajout d’une information annexe
issue de filosofi
.
3 Statistiques descriptives par groupe
3.1 Principe
Nous avons vu, lors du chapitre précédent, comment obtenir
une statistique agrégée simplement grâce à Pandas
.
Il est néanmoins commun d’avoir des données avec des strates
intermédiaires d’analyse pertinentes: des variables géographiques, l’appartenance à des groupes socio-démographiques liés à des caractéristiques renseignées, des indicatrices de période temporelle, etc.
Pour mieux comprendre la structure de ses données, les data scientists sont donc souvent amenés à construire des statistiques descriptives sur des sous-groupes présents dans les données. Pour reprendre l’exemple sur les émissions, nous avions précédemment construit des statistiques d’émissions au niveau national. Mais qu’en est-il du profil d’émission des différents départements ? Pour répondre à cette question, il sera utile d’agréger nos données au niveau départemental. Ceci nous donnera une information différente du jeu de données initial (niveau communal) et du niveau le plus agrégé (niveau national).
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
:
En Pandas
, on utilise groupby
pour découper les données selon un ou
plusieurs axes (ce tutoriel sur le sujet
est particulièrement utile).
L’ensemble des opérations d’agrégation (comptage, moyennes, etc.) que nous avions vues précédemment peut être mise en oeuvre par groupe.
Techniquement, cette opération consiste à créer une association
entre des labels (valeurs des variables de groupe) et des
observations. Utiliser la méthode groupby
ne déclenche pas d’opérations avant la mise en oeuvre d’une statistique, cela créé seulement une relation formelle entre des observations et des regroupemens qui seront utilisés a posteriori:
"dep"] = filosofi["CODGEO"].str[:2]
filosofi["dep").__class__ filosofi.groupby(
pandas.core.groupby.generic.DataFrameGroupBy
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 :
"dep") filosofi.groupby(
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f9ee01ca150>
3.2 Illustration 1: dénombrement par groupe
Pour illustrer l’application de ce principe à un comptage, on peut dénombrer le nombre de communes par département en 2023 (chaque année cette statistique change du fait des fusions de communes). Pour cela, il suffit de prendre le référentiel des communes françaises issu du code officiel géographique (COG) et dénombrer par département grâce à count
:
import requests
from io import StringIO
import pandas as pd
= "https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv"
url_cog_2023 = "https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/cog_2023.csv"
url_backup
# Try-except clause to avoid timout issue sometimes
# Without timeout problem, pd.read_csv(url_cog_2023) would be sufficient
try:
= requests.get(url_cog_2023)
response
response.raise_for_status()= pd.read_csv(StringIO(response.text))
cog_2023 except requests.exceptions.Timeout:
print("Failing back to backup")
= pd.read_csv(url_backup) cog_2023
Grâce à ce jeu de données, sans avoir recours aux statistiques par groupe, on peut déjà savoir combien on a, respectivement, de communes, départements et régions en France:
1= cog_2023.loc[cog_2023["TYPECOM"] == "COM"]
communes "COM", "DEP", "REG"]].nunique() communes.loc[:, [
- 1
- On se restreint au statut “Commune” car ce fichier comporte également les codes Insee pour d’autres status, comme les “Arrondissements municipaux” de Paris, Lyon et Marseille.
COM 34945
DEP 101
REG 18
dtype: int64
Maintenant, intéressons nous aux départements ayant le plus de communes. Il s’agit de la même fonction de dénombrement où on joue, cette fois, sur le groupe à partir duquel est calculé la statistique.
Calculer cette statistique se fait de manière assez transparente lorsqu’on connaît le principe d’un calcul de statistiques avec Pandas
:
= cog_2023.loc[cog_2023["TYPECOM"] == "COM"]
communes "DEP").agg({"COM": "nunique"}) communes.groupby(
COM | |
---|---|
DEP | |
01 | 392 |
02 | 798 |
03 | 317 |
04 | 198 |
05 | 162 |
... | ... |
971 | 32 |
972 | 34 |
973 | 22 |
974 | 24 |
976 | 17 |
101 rows × 1 columns
En SQL, on utiliserait la requête suivante:
SELECT dep, COUNT DISTINCT "COM" AS COM
FROM communes
GROUP BY dep
WHERE TYPECOM == 'COM';
La sortie est une Serie
indexée. Ce n’est pas très pratique comme nous avons pu l’évoquer au cours du chapitre précédent. Il est plus pratique de transformer cet objet en DataFrame
avec reset_index
. Enfin, avec sort_values
, on obtient la statistique désirée:
("DEP")
communes.groupby("COM": "nunique"})
.agg({
.reset_index()"COM", ascending=False)
.sort_values( )
DEP | COM | |
---|---|---|
62 | 62 | 890 |
1 | 02 | 798 |
80 | 80 | 772 |
57 | 57 | 725 |
76 | 76 | 708 |
... | ... | ... |
96 | 971 | 32 |
99 | 974 | 24 |
98 | 973 | 22 |
100 | 976 | 17 |
75 | 75 | 1 |
101 rows × 2 columns
3.3 Illustration 2: agrégats par groupe
Pour illustrer les agrégats par groupe nous pouvons prendre le jeu de données de l’Insee filosofi
et compter la population grâce à la variable NBPERSMENFISC16
.
Pour calculer le total au niveau France entière nous pouvons faire de deux manières :
"NBPERSMENFISC16"].sum() * 1e-6 filosofi[
66.9322415
"NBPERSMENFISC16": "sum"}).div(1e6) filosofi.agg({
NBPERSMENFISC16 66.932242
dtype: float64
où les résultats sont reportés en millions de personnes. La logique est identique lorsqu’on fait des statistiques par groupe, il s’agit seulement de remplacer filosofi
par filosofi.groupby('dep')
pour créer une version partitionnée par département de notre jeu de données:
1"dep")["NBPERSMENFISC16"].sum() filosofi.groupby(
- 1
-
Avec cette approche, il faut faire attention à l’ordre des opérations: d’abord on effectue le
groupby
puis on conserve la colonne d’intérêt
dep
01 613088.0
02 514249.0
03 329435.0
04 156537.5
05 133992.5
...
92 1583682.0
93 1586664.5
94 1345977.0
95 1226059.0
97 1191947.0
Name: NBPERSMENFISC16, Length: 97, dtype: float64
"dep").agg({"NBPERSMENFISC16": "sum"}) filosofi.groupby(
NBPERSMENFISC16 | |
---|---|
dep | |
01 | 613088.0 |
02 | 514249.0 |
03 | 329435.0 |
04 | 156537.5 |
05 | 133992.5 |
... | ... |
92 | 1583682.0 |
93 | 1586664.5 |
94 | 1345977.0 |
95 | 1226059.0 |
97 | 1191947.0 |
97 rows × 1 columns
La seconde approche est plus pratique car elle donne directement un DataFrame
Pandas
et non une série indexée sans nom. A partir de celle-ci, quelques manipulations basiques peuvent suffire pour avoir un tableau diffusables sur la démographie départementale. Néanmoins, celui-ci, serait quelques peu brut de décoffrage car nous ne possédons à l’heure actuelle que les numéros de département. Pour avoir le nom de départements, il faudrait utiliser une deuxième base de données et croiser les informations communes entre elles (en l’occurrence le numéro du département). C’est l’objet de la prochaine partie.
3.4 Exercice d’application
Cet exercice d’application s’appuie sur le jeu de données de l’Ademe nommé emissions
précédemment.
Calculer les émissions totales du secteur “Résidentiel” par département et rapporter la valeur au département le plus polluant dans le domaine. En tirer des intutitions sur la réalité que cette statistique reflète.
Calculer, pour chaque département, les émissions totales de chaque secteur. Pour chaque département, calculer la proportion des émissions totales venant de chaque secteur.
Indice pour cette question
- “Grouper par” =
groupby
- “émissions totales” =
agg({*** : "sum"})
A la question 1, le résultat obtenu devrait être le suivant:
dep | Résidentiel | Résidentiel (% valeur max) | |
---|---|---|---|
59 | 59 | 3.498347e+06 | 1.000000 |
75 | 75 | 1.934580e+06 | 0.552998 |
69 | 69 | 1.774653e+06 | 0.507283 |
62 | 62 | 1.738090e+06 | 0.496832 |
57 | 57 | 1.644192e+06 | 0.469991 |
Ce classement reflète peut-être plus la démographie que le processus qu’on désire mesurer. Sans l’ajout d’une information annexe sur la population de chaque département pour contrôler ce facteur, on peut difficilement savoir s’il y a une différence structurelle de comportement entre les habitants du Nord (département 59) et ceux de la Moselle (département 57).
A l’issue de la question 2, prenons la part des émissions de l’agriculture et du secteur tertiaire dans les émissions départementales:
Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | ... | Part Agriculture | Part Autres transports | Part Autres transports international | Part CO2 biomasse hors-total | Part Déchets | Part Energie | Part Industrie hors-énergie | Part Résidentiel | Part Routier | Part Tertiaire | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
dep | |||||||||||||||||||||
23 | 1.430068e+06 | 5060.057601 | 0.000000 | 210196.604389 | 26550.858041 | 9752.578164 | 28626.245699 | 134197.515156 | 434767.868975 | 70733.245013 | ... | 60.855172 | 0.215326 | 0.000000 | 8.944716 | 1.129846 | 0.415012 | 1.218163 | 5.710647 | 18.501132 | 3.009986 |
48 | 7.510594e+05 | 5697.938112 | 0.000000 | 70903.948092 | 26011.591018 | 6065.340741 | 17803.285591 | 61033.998303 | 253618.488432 | 43661.121359 | ... | 60.772448 | 0.461052 | 0.000000 | 5.737238 | 2.104744 | 0.490781 | 1.440564 | 4.938605 | 20.521701 | 3.532867 |
15 | 1.539204e+06 | 8261.874450 | 18.804572 | 228415.892777 | 44814.875202 | 13138.432196 | 85214.659284 | 128315.601994 | 443832.903418 | 84364.615635 | ... | 59.761414 | 0.320777 | 0.000730 | 8.868517 | 1.739990 | 0.510115 | 3.308560 | 4.982005 | 17.232336 | 3.275556 |
12 | 2.122331e+06 | 13796.608978 | 3124.844800 | 331420.983449 | 52412.681268 | 35405.484754 | 112897.690887 | 268862.654280 | 795413.985637 | 170211.845832 | ... | 54.336847 | 0.353227 | 0.080004 | 8.485186 | 1.341893 | 0.906467 | 2.890457 | 6.883541 | 20.364540 | 4.357839 |
32 | 1.026604e+06 | 4599.824552 | 0.000000 | 201732.703762 | 50950.668326 | 16651.432346 | 53468.498055 | 158218.000190 | 446345.993580 | 105662.674213 | ... | 49.732924 | 0.222835 | 0.000000 | 9.772766 | 2.468261 | 0.806664 | 2.590235 | 7.664734 | 21.622845 | 5.118737 |
5 rows × 21 columns
Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | ... | Part Agriculture | Part Autres transports | Part Autres transports international | Part CO2 biomasse hors-total | Part Déchets | Part Energie | Part Industrie hors-énergie | Part Résidentiel | Part Routier | Part Tertiaire | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
dep | |||||||||||||||||||||
75 | 0.000000 | 42216.829025 | 1.837660e+02 | 1.186577e+06 | 27358.781206 | 147965.117571 | 434314.469384 | 1.934580e+06 | 1.625583e+06 | 1.331630e+06 | ... | 0.000000 | 0.627255 | 0.002730 | 17.630092 | 0.406495 | 2.198457 | 6.453018 | 28.743870 | 24.152808 | 19.785275 |
94 | 2259.429643 | 218992.353559 | 3.146283e+05 | 6.914050e+05 | 213619.661516 | 76341.230740 | 467189.038927 | 1.336894e+06 | 1.169432e+06 | 7.636502e+05 | ... | 0.043001 | 4.167781 | 5.987888 | 13.158562 | 4.065530 | 1.452898 | 8.891367 | 25.443275 | 22.256193 | 14.533505 |
92 | 91.408184 | 12340.794839 | 2.101194e+02 | 1.067889e+06 | 264497.880711 | 242842.018012 | 706597.424067 | 1.466794e+06 | 1.198420e+06 | 8.360132e+05 | ... | 0.001577 | 0.212930 | 0.003625 | 18.425550 | 4.563695 | 4.190041 | 12.191761 | 25.308332 | 20.677765 | 14.424724 |
93 | 2018.470982 | 59617.086124 | 1.101400e+06 | 7.259516e+05 | 252166.943778 | 102837.663903 | 433216.360990 | 1.316452e+06 | 1.396911e+06 | 8.630178e+05 | ... | 0.032277 | 0.953326 | 17.612287 | 11.608558 | 4.032355 | 1.644458 | 6.927483 | 21.051146 | 22.337751 | 13.800359 |
83 | 151715.557862 | 21772.374976 | 2.854770e+04 | 5.795888e+05 | 233522.964403 | 47044.063669 | 139710.930613 | 5.938382e+05 | 1.944266e+06 | 5.610540e+05 | ... | 3.527399 | 0.506209 | 0.663736 | 13.475487 | 5.429428 | 1.093778 | 3.248291 | 13.806786 | 45.204334 | 13.044551 |
5 rows × 21 columns
Ces résultats sont assez logiques ; les départements ruraux ont une part plus importante de leur émission issue de l’agriculture, les départements urbains ont plus d’émissions issues du secteur tertiaire, ce qui est lié à la densité plus importante de ces espaces.
Grâce à ces statistiques on progresse dans la connaissance de notre jeu de données et donc de la nature des émissions de C02 en France. Les statistiques descriptives par groupe nous permettent de mieux saisir l’hétérogénéité spatiale de notre phénomène.
Cependant, on reste limité dans notre capacité à interpréter les statistiques obtenues sans recourir à l’utilisation d’information annexe. Pour donner du sens et de la valeur à une statistique, il faut généralement associer celle-ci à de la connaissance annexe sous peine qu’elle soit désincarnée.
Dans la suite de ce chapitre, nous envisagerons une première voie qui est le croisement avec des données complémentaires. On appelle ceci un enrichissement de données. Ces données peuvent être des observations à un niveau identique à celui de la source d’origine. Par exemple, l’un des croisements les plus communs est d’associer une base client à une base d’achats afin de mettre en regard un comportement d’achat avec des caractéristiques pouvant expliquer celui-ci. Les associations de données peuvent aussi se faire à des niveaux conceptuels différents, en général à un niveau plus agrégé pour contextualiser la donnée plus fine et comparer une observation à des mesures dans un groupe similaire. Par exemple, on peut associer des temps et des modes de transports individuels à ceux d’une même classe d’âge ou de personnes résidant dans la même commune pour pouvoir comparer la différence entre certains individus et un groupe sociodémographique similaire.
4 Restructurer les données
4.1 Principe
Quand on a plusieurs informations pour un même individu ou groupe, on retrouve généralement deux types de structure 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 pris à 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.
Les formats wide sont plutôt pensés pour des tableurs comme Excel
ou on dispose d’un nombre réduit
de lignes à partir duquel faire des tableaux croisés dynamiques.
4.2 Exercice d’application
Les données de l’ADEME, et celles de l’Insee également, sont au format
wide.
Le prochain exercice illustre l’intérêt de faire la conversion long \(\to\) wide
avant de faire un graphique avec la méthode plot
vue au chapitre précédent
Créer une copie des données de l’
ADEME
en faisantdf_wide = emissions_wide.copy()
Restructurer les données au format long pour avoir des données d’émissions par secteur en gardant comme niveau d’analyse la commune (attention aux autres variables identifiantes).
Faire la somme par secteur et représenter graphiquement
Garder, pour chaque département, le secteur le plus polluant
5 Joindre des données
5.1 Principe
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ées2. C’est un besoin quotidien des data scientists d’associer des informations présentes dans plusieurs fichiers. Par exemple, dans des bases de données d’entreprises, les informations clients (adresse, âge, etc.) seront dans un fichier, les ventes dans un autre et les caractéristiques des produits dans un troisième fichier. Afin d’avoir une base complète mettant en regard toutes ces informations, il sera dès lors nécessaire de joindre ces trois fichiers sur la base d’informations communes.
Cette pratique découle du fait que de nombreux systèmes d’information prennent la forme d’un schéma en étoile:
Cette structuration de l’information est très liée au modèle des tables relationnelles des années 1980. Aujourd’hui, il existe des modèles de données plus flexibles où l’information est empilée dans un data lake sans structure a priori. Néanmoins ce modèle du schéma en étoile conserve une pertinence parce qu’il permet de partager l’information qu’à ceux qui en ont besoin laissant le soin à ceux qui ont besoin de lier des données entre elles de le faire.
Puisque la logique du schéma en étoile vient historiquement des bases relationnelles, il est naturel qu’il s’agisse d’une approche intrinsèquement liée à la philosophie du SQL, jusque dans le vocabulaire. On parle souvent de jointure de données, un héritage du terme JOIN
de SQL, et la manière de décrire les jointures (left join, right join…) est directement issue des instructions SQL associées.
On parle généralement de base de gauche et de droite pour illustrer les jointures:
5.2 Mise en oeuvre avec Pandas
En Pandas
, la méthode la plus pratique pour associer des jeux de données à partir de caractéristiques communes est merge
. Ses principaux arguments permettent de contrôler le comportement de jointure. Nous allons les explorer de manière visuelle.
En l’occurrence, pour notre problématique de construction de statistiques
sur les émissions de gaz carbonique, la base de gauche sera le DataFrame emission
et la base de droite le DataFrame filosofi
:
2) emissions.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 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | 309.358195 | 793.156501 | 367.036172 | 01 |
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 |
2) filosofi.head(
CODGEO | LIBGEO | NBMENFISC16 | NBPERSMENFISC16 | MED16 | PIMP16 | TP6016 | TP60AGE116 | TP60AGE216 | TP60AGE316 | ... | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | dep | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 01001 | L'Abergement-Clémenciat | 313.0 | 795.5 | 22679.000000 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 01 |
1 | 01002 | L'Abergement-de-Varey | 101.0 | 248.0 | 24382.083333 | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 01 |
2 rows × 30 columns
On parle de clé(s) de jointure pour nommer la ou les variable(s) nécessaire(s) à la fusion de données. Ce sont les variables communes aux deux jeux de données. Il n’est pas nécessaire qu’elles aient le même nom en revanche elles doivent partager des valeurs communes autrement l’intersection entre ces deux bases est l’ensemble vide.
On peut jouer sur deux dimensions dans la jointure (ceci sera plus clair ensuite avec les exemples graphiques).
- Il existe principalement trois types de fusions: left join et right join ou un combo des deux selon le type de pivot qu’on désire mettre en oeuvre.
- Ensuite, il existe deux manières de fusionner les valeurs une fois qu’on a choisi un pivot: inner ou outer join. Dans le premier cas, on ne conserve que les observations où les clés de jointures sont présentes dans les deux bases, dans le second on conserve toutes les observations de la clé de jointure des variables pivot quitte à avoir des valeurs manquantes si la deuxième base de données n’a pas de telles observations.
Dans les exemples ci-dessous, nous allons utiliser les codes communes et les départements comme variables de jointure. En soi, l’usage du département n’est pas nécessaire puisqu’il se déduit directement du code commune mais cela permet d’illustrer le principe des jointures sur plusieurs variables. A noter que le nom de la commune est volontairement mis de côté pour effectuer des jointures alors que c’est une information commune aux deux bases. Cependant, comme il s’agit d’un champ textuel, dont le formattage peut suivre une norme différente dans les deux bases, ce n’est pas une information fiable pour faire une jointure exacte.
Pour illustrer le principe du pivot à gauche ou à droite, on va créer deux variables identificatrices de la ligne de nos jeux de données de gauche et de droite. Cela nous permettra de trouver facilement les lignes présentes dans un jeu de données mais pas dans l’autre.
= emissions.reset_index(names=["id_left"])
emissions = filosofi.reset_index(names=["id_right"]) filosofi
5.2.1 Left join
Commençons avec la jointure à gauche. Comme son nom l’indique, on va prendre la variable de gauche en pivot:
= emissions.merge(
left_merged =["INSEE commune", "dep"], right_on=["CODGEO", "dep"], how="left"
filosofi, left_on
)3) left_merged.head(
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 1 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 2 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | 2930.354461 | ... | 27.0 | 8.8 | 6.9 | 2.8 | 2.1 | 2.0 | -15.7 | 10457.083333 | 33880.555556 | 3.239962 |
3 rows × 44 columns
Il est recommandé de toujours expliciter les clés de jointures par le biais des arguments left_on
, right_on
ou on
si les noms de variables sont communs dans les deux bases.
Si on a des noms de variables communes entre les bases mais qu’elles ne sont pas définies comme clés de jointures, celles-ci ne seront pas utilisées pour joindre mais seront conservées avec un suffixe qui par défaut est _x
et _y
(paramétrable par le biais de l’argument suffixes
).
La syntaxe Pandas
étant directement inspirée de SQL, on a une traduction assez transparente de l’instruction ci-dessus en SQL:
SELECT *
FROM emissions
LEFT JOIN filosofi
ON emissions.`INSEE commune` = filosofi.CODGEO
AND emissions.dep = filosofi.dep;
En faisant une jointure à gauche, on doit en principe avoir autant de lignes que la base de données à gauche:
0] == emissions.shape[0] left_merged.shape[
True
Autrement, cela est signe qu’il y a une clé dupliquée à droite. Grâce à notre variable id_right
, on peut savoir les codes communes à droite qui n’existent pas à gauche:
"id_right"].isna()].tail(3) left_merged.loc[left_merged[
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
35348 | 35348 | 91182 | COURCOURONNES | 24.548795 | 103.360309 | NaN | 9623.065698 | 111.241872 | 1276.170296 | 3745.877636 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
35360 | 35360 | 91222 | ESTOUCHES | 1790.002871 | NaN | NaN | 113.797978 | 30.548162 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
35687 | 35687 | 95259 | GADANCOURT | 312.298700 | NaN | NaN | 142.113291 | 11.372909 | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 rows × 44 columns
Cela vient du fait que nous utilisons des données qui ne sont pas de la même année de référence du code officiel géographique (2016 vs 2018). Pendant cet intervalle, il y a eu des changements de géographie, notamment des fusions de communes. Par exemple, la commune de Courcouronnes qu’on a vu ci-dessus peut être retrouvée regroupée avec Evry dans le jeu de données filosofi (base de droite):
"LIBGEO"].str.lower().str.contains("courcouronnes")] filosofi.loc[filosofi[
id_right | CODGEO | LIBGEO | NBMENFISC16 | NBPERSMENFISC16 | MED16 | PIMP16 | TP6016 | TP60AGE116 | TP60AGE216 | ... | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | dep | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
34441 | 34441 | 91228 | Évry-Courcouronnes | 23761.0 | 65184.5 | 17107.0 | 47.0 | 26.0 | 30.0 | 26.0 | ... | 4.0 | 10.5 | 4.4 | 3.1 | 2.9 | -15.0 | 9139.090909 | 30888.0 | 3.379767 | 91 |
1 rows × 31 columns
Dans un exercice de construction de statistiques publiques, on ne pourrait donc se permettre cette disjonction des années.
5.2.2 _Right join__
Le principe est le même mais cette fois c’est la base de droite qui est prise sous forme de pivot:
= emissions.merge(
right_merged =["INSEE commune", "dep"], right_on=["CODGEO", "dep"], how="right"
filosofi, left_on
)3) right_merged.head(
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 1.0 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 2.0 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | 2930.354461 | ... | 27.0 | 8.8 | 6.9 | 2.8 | 2.1 | 2.0 | -15.7 | 10457.083333 | 33880.555556 | 3.239962 |
3 rows × 44 columns
L’instruction équivalente en SQL serait
SELECT *
FROM filosofi
RIGHT JOIN emissions
ON filosofi.CODGEO = emissions.`INSEE commune`
AND filosofi.dep = emissions.dep;
On peut, comme précédemment, vérifier la cohérence des dimensions:
0] == filosofi.shape[0] right_merged.shape[
True
Pour vérifier le nombre de lignes des données Filosofi que nous n’avons pas dans notre jeu d’émissions de gaz carbonique, on peut faire
"id_left"].isna().sum() right_merged[
61
C’est un nombre faible. Quelles sont ces observations ?
right_merged.loc["id_left"].isna(),
right_merged[+ emissions.columns.tolist(),
filosofi.columns.tolist() ]
id_right | CODGEO | LIBGEO | NBMENFISC16 | NBPERSMENFISC16 | MED16 | PIMP16 | TP6016 | TP60AGE116 | TP60AGE216 | ... | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | dep | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
4346 | 4346 | 13055 | Marseille | 362971.0 | 815045.5 | 18248.000000 | 47.0 | 26.0 | 34.0 | 30.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 13 |
27115 | 27115 | 69123 | Lyon | 225617.0 | 459305.5 | 22806.000000 | 60.0 | 15.0 | 21.0 | 13.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 69 |
29288 | 29288 | 75056 | Paris | 1027657.0 | 2074629.5 | 26808.000000 | 69.0 | 16.0 | 17.0 | 13.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 75 |
34874 | 34874 | 97201 | L'Ajoupa-Bouillon | 688.0 | 1634.0 | 14116.833333 | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
34875 | 34875 | 97202 | Les Anses-d'Arlet | 1390.0 | 3274.5 | 14645.277778 | 25.0 | 37.0 | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
34927 | 34927 | 97420 | Sainte-Suzanne | 7827.0 | 23118.0 | 15110.666667 | 26.0 | 38.0 | 53.0 | 39.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
34928 | 34928 | 97421 | Salazie | 2358.0 | 6967.0 | 11280.937500 | 12.0 | 58.0 | NaN | 63.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
34929 | 34929 | 97422 | Le Tampon | 27916.0 | 73468.5 | 14243.809524 | 25.0 | 41.0 | 56.0 | 45.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
34930 | 34930 | 97423 | Les Trois-Bassins | 2386.0 | 7105.0 | 14031.785714 | 23.0 | 42.0 | NaN | 42.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
34931 | 34931 | 97424 | Cilaos | 2038.0 | 5603.0 | 12034.375000 | 15.0 | 53.0 | NaN | 60.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 97 |
61 rows × 45 columns
Il est suprenant de voir que Paris, Lyon et Marseille sont présents dans la base des statistiques communales mais pas dans celles des émissions. Pour comprendre pourquoi, recherchons dans nos données d’émissions les observations liées à Marseille:
"Commune"].str.lower().str.contains("MARSEILLE")] emissions.loc[emissions[
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | dep |
---|
Cela vient du fait que le jeu de données des émissions de l’Ademe propose de l’information sur les arrondissements dans les trois plus grandes villes là où le jeu de données de l’Insee ne fait pas cette décomposition.
5.2.3 Inner join
Il s’agit du jeu de données où les clés sont retrouvées à l’intersection des deux tables.
= emissions.merge(
inner_merged =["INSEE commune", "dep"], right_on=["CODGEO", "dep"], how="inner"
filosofi, left_on
)3) inner_merged.head(
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 1 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 2 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | 2930.354461 | ... | 27.0 | 8.8 | 6.9 | 2.8 | 2.1 | 2.0 | -15.7 | 10457.083333 | 33880.555556 | 3.239962 |
3 rows × 44 columns
En SQL, cela donne
SELECT *
FROM emissions
INNER JOIN filosofi
ON emissions.`INSEE commune` = filosofi.CODGEO
AND emissions.dep = filosofi.dep;
Le nombre de lignes dans notre jeu de données peut être comparé au jeu de droite et de gauche:
0] == (left_merged.shape[0] - left_merged["id_right"].isna().sum()) inner_merged.shape[
True
0] == (right_merged.shape[0] - right_merged["id_left"].isna().sum()) inner_merged.shape[
True
5.2.4 Full join
Le full join est un pivot à gauche puis à droite pour les informations qui n’ont pas été trouvées
= emissions.merge(
full_merged =["INSEE commune", "dep"], right_on=["CODGEO", "dep"], how="outer"
filosofi, left_on
)3) full_merged.head(
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | ... | PPEN16 | PPAT16 | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
1 | 1.0 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | 6.911213 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2 | 2.0 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | 2930.354461 | ... | 27.0 | 8.8 | 6.9 | 2.8 | 2.1 | 2.0 | -15.7 | 10457.083333 | 33880.555556 | 3.239962 |
3 rows × 44 columns
Comme d’habitude, la traduction en SQL est presque immédiate:
SELECT *
FROM emissions
FULL OUTER JOIN filosofi
ON emissions.`INSEE commune` = filosofi.CODGEO
AND emissions.dep = filosofi.dep;
Cette fois, on a une combinaison de nos trois jeux de données initiaux:
- Le inner join ;
- Le left join sur les observations sans clé de droite ;
- Le right join sur les observations sans clé de gauche ;
"id_left"].isna().sum() + full_merged["id_right"].isna().sum()) == (
(full_merged["id_right"].isna().sum() + right_merged["id_left"].isna().sum()
left_merged[ )
True
5.2.5 En résumé
5.3 Exemples d’identifiants dans les données françaises
5.3.1 Le Code officiel géographique (COG): l’identifiant des données géographiques
Pour les données géographiques, il existe de nombreux identifiants selon la problématique d’étude. Parmi les besoins principaux, on retrouve le fait d’apparier des données géographiques à partir d’un identifiant administratif commun. Par exemple, associer deux jeux de données au niveau communal.
Pour cela, l’identifiant de référence est le code Insee, issu du Code officiel géographique (COG) que nous utilisons depuis le dernier chapitre et que nous aurons amplement l’occasion d’exploiter au cours des différents chapitres de ce cours. La géographie administrative étant en évolution perpétuelle, la base des code Insee est une base vivante. Le site et les API de l’Insee permettent de récupérer l’historique d’après-guerre afin de pouvoir faire de l’analyse géographique sur longue période.
Les codes postaux ne peuvent être considérés comme un identifiant : ils peuvent regrouper plusieurs communes ou, au contraire, une même commune peut avoir plusieurs codes postaux. Il s’agit d’un système de gestion de la Poste qui n’a pas été construit pour l’analyse statistique.
Pour se convaincre du problème, à partir des données mises à disposition par La Poste, on peut voir que le code postal 11420 correspond à 11 communes:
= pd.read_csv(
codes_postaux "https://datanova.laposte.fr/data-fair/api/v1/datasets/laposte-hexasmal/raw",
=";",
sep="latin1",
encoding={"Code_postal": "str", "#Code_commune_INSEE": "str"},
dtype
)"Code_postal"] == "11420"] codes_postaux.loc[codes_postaux[
#Code_commune_INSEE | Nom_de_la_commune | Code_postal | Libellé_d_acheminement | Ligne_5 | |
---|---|---|---|---|---|
3921 | 11033 | BELPECH | 11420 | BELPECH | NaN |
3944 | 11057 | CAHUZAC | 11420 | CAHUZAC | NaN |
4080 | 11184 | LAFAGE | 11420 | LAFAGE | NaN |
4124 | 11226 | MAYREVILLE | 11420 | MAYREVILLE | NaN |
4134 | 11236 | MOLANDIER | 11420 | MOLANDIER | NaN |
4176 | 11277 | PECHARIC ET LE PY | 11420 | PECHARIC ET LE PY | NaN |
4177 | 11278 | PECH LUNA | 11420 | PECH LUNA | NaN |
4182 | 11283 | PEYREFITTE SUR L HERS | 11420 | PEYREFITTE SUR L HERS | NaN |
4189 | 11290 | PLAIGNE | 11420 | PLAIGNE | NaN |
4264 | 11365 | ST SERNIN | 11420 | ST SERNIN | NaN |
4317 | 11419 | VILLAUTOU | 11420 | VILLAUTOU | NaN |
En anticipant sur les compétences développées lors des prochains chapitres, nous pouvons représenter le problème sous forme cartographique en prenant l’exemple de l’Aude. Le code pour produire la carte des codes communes est donné tel quel, il n’est pas développé car il fait appel à des concepts et librairies qui seront présentés lors du prochain chapitre:
from cartiflette import carti_download
= carti_download(
shp_communes =["11"],
values=4326,
crs="COMMUNE",
borders=50,
simplification="DEPARTEMENT",
filter_by="EXPRESS-COG-CARTO-TERRITOIRE",
source=2022,
year1
)
= shp_communes.merge(
codes_postaux11 ="INSEE_COM", right_on="#Code_commune_INSEE"
codes_postaux, left_on2
)3= codes_postaux11.dissolve(by="Code_postal")
codes_postaux11
4# Carte
= shp_communes.plot(color="white", edgecolor="blue", linewidth=0.5)
ax = codes_postaux11.plot(ax=ax, color="none", edgecolor="black")
ax ax.set_axis_off()
- 1
-
Récupération des contours officiels de l’Aude produits par l’IGN par le biais de la librairie
cartiflette
- 2
- Jointure par le biais du code commune entre les deux sources de données
- 3
- On agrège la géométrie au niveau des codes postaux
- 4
- On crée une carte à partir de nos deux couches
This is an experimental version of cartiflette published on PyPi.
To use the latest stable version, you can install it directly from GitHub with the following command:
pip install git+https://github.com/inseeFrLab/cartiflette.git
ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
5.3.2 Sirene: l’identifiant dans les données d’entreprises
Pour relier les microdonnées d’entreprises françaises, il existe un numéro unique d’identification : le numéro Siren
. Il s’agit d’un numéro d’identification dans un répertoire légal d’entreprise indispensable pour toutes démarches juridiques, fiscales… Pour les entreprises qui possèdent plusieurs établissements - par exemple dans plusieurs villes - il existe un identifiant dérivé qui s’appelle le Siret
: aux 9 chiffres du numéro Sirene s’ajoutent 5 chiffres d’identifications de l’établissement. D’ailleurs, les administrations publiques sont également concernées par le numéro Siren: étant amenées à effectuer des opérations de marchés (achat de matériel, locations de biens, etc.) elles disposent également d’un identifiant Siren. Etant inscrits dans des répertoires légaux pour lesquels les citoyens sont publics, les numéros Siren et les noms des entreprises associées sont disponibles en open data, par exemple sur annuaire-entreprises.data.gouv.fr/ pour une recherche ponctuelle, sur data.gouv.fr.
Cette base Sirene est une mine d’information, parfois comique, sur les entreprises françaises. Par exemple, le site tif.hair/ s’est amusé à répertorier la part des salons de coiffures proposant des jeux de mots dans le nom du salon. Lorsqu’un entrepreneur déclare la création d’une entreprise, il reçoit un numéro Sirene et un code d’activité (le code APE) relié à la description qu’il a déclaré de l’activité de son entreprise. Ce code permet de classer l’activité d’une entreprise dans la Nomenclature d’activités françaises (NAF) ce qui servira à l’Insee pour la publication de statistiques sectorielles. En l’occurrence, pour les coiffeurs, le code dans la NAF est 96.02A
. Il est possible à partir de la base disponible en open data d’avoir en quelques lignes de Python
la liste de tous les coiffeurs puis de s’amuser à explorer ces données (objet du prochain exercice optionnel).
L’exercice suivant, optionnel, propose de s’amuser à reproduire de manière simplifiée le recensement fait par tif.hair/ des jeux de mots dans les salons de coiffure. Il permet de pratiquer quelques méthodes de manipulation textuelle, en avance de phase sur le chapitre consacré aux expressions régulières.
Le jeu de données de l’ensemble des entreprises étant assez volumineux (autour de 4Go en CSV après décompression), il est plus pratique de partir sur un jeu de données au format Parquet
, plus optimisé (plus de détails sur ce format dans le chapitre d’approfondissement qui lui est consacré).
Pour lire ce type de fichiers de manière optimale, il est conseillé d’utiliser la librairie DuckDB
qui permet de ne consommer que les données nécessaires et non de télécharger l’ensemble du fichier pour n’en lire qu’une partie comme ce serait le cas avec Pandas
(voir la fin de ce chapitre, section “Aller au-delà de Pandas
”). La requête SQL suivante se traduit en langage naturel par l’instruction suivante: “A partir du fichier Parquet
, je ne veux que quelques colonnes du fichier pour les coiffeurs (APE: 96.02A) dont le nom de l’entreprise (denominationUsuelleEtablissement
) est renseigné”:
import duckdb
= duckdb.sql(
coiffeurs """
SELECT
siren, siret, dateDebut, enseigne1Etablissement, activitePrincipaleEtablissement, denominationUsuelleEtablissement
FROM
read_parquet('https://minio.lab.sspcloud.fr/lgaliana/data/sirene2024.parquet')
WHERE
activitePrincipaleEtablissement == '96.02A'
AND
denominationUsuelleEtablissement IS NOT NULL
"""
)1= coiffeurs.df() coiffeurs
- 1
- Convert the DuckDB dataframe to a Pandas DataFrame.
3) coiffeurs.head(
siren | siret | dateDebut | enseigne1Etablissement | activitePrincipaleEtablissement | denominationUsuelleEtablissement | |
---|---|---|---|---|---|---|
0 | 024050379 | 02405037900023 | 2016-01-14 | None | 96.02A | SOPHA COIFFURE |
1 | 024076481 | 02407648100019 | 2017-07-17 | None | 96.02A | SAF COIFFURE |
2 | 047142872 | 04714287200036 | 2011-07-31 | None | 96.02A | JENNY |
Dans cet exercice, nous allons considérer exclusivement la variable denominationUsuelleEtablissement
.
- Dans cette base,
[ND]
est un code pour valeur manquante. CommePython
n’a pas de raison de le savoir a priori et donc d’avoir interprété ces valeurs comme étant manquantes, utiliser la méthodereplace
pour remplacer[ND]
par un champ textuel vide. Recoder également les valeurs manquantes sous forme de champ textuel vide afin d’éviter des erreurs ultérieures liées à l’impossibilité d’appliquer certaines méthodes textuelles aux valeurs manquantes. - Rechercher toutes les observations où le terme
tif
apparaît en faisant attention à la capitalisation de la variable. Regarder quelques observations - A partir de cet exemple, normaliser les noms des salons en retirant les caractères spéciaux et compter les jeux de mots les plus fréquents
Avec la question 2, on retrouve une liste de jeux de mots assez imaginatifs à partir du terme tif
:
"ESTHETIFF'S, INSTANT CREA'TIF, SO BEAUTIFUL HAIR / NAIL, A C TIF, TIFANNY COIFFURE, S'TIFANY, QUALITA' TIFF, STEF TOUS TIFS, ESPACE CREATIF, IMAGINA'TIF"
Voici sous une forme plus interactive l’ensemble des coiffeurs qui possèdent les termes tif
dans le nom de leur entreprise déposée dans les données officielles:
Bien sûr, pour aller plus loin, il faudrait mieux normaliser les données, vérifier que l’information recherchée n’est pas à cheval sur plusieurs colonnes et bien sûr faire de l’inspection visuelle pour détecter les jeux de mots cachés. Mais déjà, en quelques minutes, on a des statistiques partielles sur le phénomène des coiffeurs blagueurs.
5.3.3 Le NIR et la question de la confidentialité des identifiants individuels
En ce qui concerne les individus, il existe un identifiant unique permettant de relier ceux-ci dans différentes sources de données : le NIR, aussi connu sous le nom de numéro Insee ou numéro de sécurité sociale. Ce numéro est nécessaire à l’administration pour la gestion des droits à prestations sociales (maladie, vieillesse, famille…). Au-delà de cette fonction qui peut être utile au quotidien, ce numéro est un identifiant individuel unique dans le Répertoire national d’identification des personnes physiques (RNIPP).
Cet identifiant est principalement présent dans des bases de gestion, liées aux fiches de paie, aux prestations sociales, etc. Cependant, a contrario du numéro Sirene, celui-ci contient en lui-même plusieurs informations sensibles - en plus d’être intrinsèquement relié à la problématique sensible des droits à la sécurité sociale.
Pour pallier ce problème, a récémment été mis en oeuvre le code statistique non signifiant (CSNS) ou NIR haché, un identifiant individuel anonyme non identifiant. L’objectif de cet identifiant anonymisé est de réduire la dissémination d’une information personnelle qui permettait certes aux fonctionnaires et chercheurs de relier de manière déterministe de nombreuses bases de données mais donnait une information non indispensable aux analystes sur les personnes en question.
5.4 Exercices d’application
5.4.1 Pourquoi a-t-on besoin d’un code commune quand on a déjà son nom ?
Cet exercice va revenir un peu en arrière afin de saisir pourquoi nous avons pris comme hypothèse ci-dessus que le code commune était la clé de jointure.
On commence par vérifier les dimensions des DataFrames
et la structure de certaines variables clés.
En l’occurrence, les variables fondamentales pour lier nos données sont les variables communales.
Ici, on a deux variables géographiques: un code commune et un nom de commune.
Vérifier les dimensions des DataFrames.
Identifier dans
filosofi
les noms de communes qui correspondent à plusieurs codes communes et sélectionner leurs codes. En d’autres termes, identifier lesLIBGEO
tels qu’il existe des doublons deCODGEO
et les stocker dans un vecteurx
(conseil: faire attention à l’index dex
).
On se focalise temporairement sur les observations où le libellé comporte plus de deux codes communes différents
Question 3. Regarder dans
filosofi
ces observations.Question 4. Pour mieux y voir, réordonner la base obtenue par order alphabétique.
Question 5. Déterminer la taille moyenne (variable nombre de personnes:
NBPERSMENFISC16
) et quelques statistiques descriptives de ces données. Comparer aux mêmes statistiques sur les données où libellés et codes communes coïncident.Question 6. Vérifier les grandes villes (plus de 100 000 personnes), la proportion de villes pour lesquelles un même nom est associé à différents codes commune.
Question 7. Vérifier dans
filosofi
les villes dont le libellé est égal à Montreuil. Vérifier également celles qui contiennent le terme ‘Saint-Denis’.
Ce petit exercice permet donc de se rassurer car les libellés dupliqués sont en fait des noms de commune identiques mais qui ne sont pas dans le même département. Il ne s’agit donc pas d’observations dupliquées. On peut donc se fier aux codes communes, qui eux sont uniques.
5.4.2 Calculer une empreinte carbone grâce à l’association entre des sources
En premier lieu, on va calculer l’empreinte carbone de chaque commune.
Créer une variable
emissions
qui correspond aux émissions totales d’une communeFaire une jointure à gauche entre les données d’émissions et les données de cadrage3.
Calculer l’empreinte carbone (émissions totales / population).
A ce stade nous pourrions avoir envie d’aller vers la modélisation pour essayer d’expliquer les déterminants de l’empreinte carbone à partir de variables communales. Une approche inférentielle nécessite néanmoins pour être pertinente de vérifier en amont des statistiques descriptives.
- Sortir un histogramme en niveau puis en log de l’empreinte carbone communale.
Avec une meilleure compréhension de nos données, nous nous rapprochons de la statistique inférentielle. Néanmoins, nous avons jusqu’à présent construit des statistiques univariées mais n’avons pas cherché à comprendre les résultats en regardant le lien avec d’autres variables. Cela nous amène vers la statistique bivariée, notamment l’analyse des corrélations. Ce travail est important puisque toute modélisation ultérieure consistera à raffiner l’analyse des corrélations pour tenir compte des corrélations croisées entre multiples facteurs. On propose ici de faire cette analyse de manière minimale.
- Regarder la corrélation entre les variables de cadrage et l’empreinte carbone. Certaines variables semblent-elles pouvoir potentiellement influer sur l’empreinte carbone ?
A l’issue de la question 5, le graphique des corrélations est le suivant :
6 Formatter des tableaux de statistiques descriptives
Un dataframe Pandas
est automatiquement mis en forme lorsqu’il est visualisé depuis un notebook sous forme de table HTML à la mise en forme minimaliste.
Cette mise en forme est pratique pour voir
les données, une tâche indispensable pour les data scientists
mais ne permet pas d’aller vraiment au-delà.
Dans une phase exploratoire, il peut être pratique d’avoir un tableau un peu plus complet, intégrant notamment des visualisations minimalistes, pour mieux connaître ses données. Dans la phase finale d’un projet, lorsqu’on communique sur un projet, il est avantageux de disposer d’une visualisation attrative. Pour ces deux besoins, les sorties des notebooks sont une réponse peu satisfaisante, en plus de nécessiter le medium du notebook qui peut en rebuter certains.
Heureusement, le tout jeune package great_tables
permet, simplement, de manière programmatique, la création de tableaux
qui n’ont rien à envier à des productions manuelles fastidieuses faites dans Excel
et difficilement répliquables. Ce package est un portage en Python
du package GT
.
great_tables
construit des tableaux
html ce qui offre une grande richesse dans la mise en forme et permet une excellente intégration avec Quarto
, l’outil de publication reproductible développé par
L’exercice suivant proposera de construire un tableau avec
ce package, pas à pas.
Afin de se concentrer sur la construction du tableau, les préparations de données à faire en amont sont données directement. Nous allons repartir de ce jeu de données:
index_x | id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | ... | PPSOC16 | PPFAM16 | PPMINI16 | PPLOGT16 | PIMPOT16 | D116 | D916 | RD16 | dep_y | empreinte | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 01 | 7.196009 |
1 | 1 | 1 | 01002 | L'ABERGEMENT-DE-VAREY | 475.330205 | NaN | NaN | 140.741660 | 140.675439 | 2.354558 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 01 | 5.378273 |
2 | 2 | 2 | 01004 | AMBERIEU-EN-BUGEY | 499.043526 | 212.577908 | NaN | 10313.446515 | 5314.314445 | 998.332482 | ... | 6.9 | 2.8 | 2.1 | 2.0 | -15.7 | 10457.083333 | 33880.555556 | 3.239962 | 01 | 4.446281 |
3 rows × 49 columns
Pour être sûr d’être en mesure d’effectuer le prochain exercice, voici le dataframe nécessaire pour celui-ci
"emissions"] = emissions.sum(axis=1, numeric_only=True)
emissions[
= emissions.reset_index().merge(
emissions_merged ="INSEE commune", right_on="CODGEO"
filosofi, left_on
)"empreinte"] = (
emissions_merged["emissions"] / emissions_merged["NBPERSMENFISC16"]
emissions_merged[
)"empreinte"] = emissions_merged["empreinte"].astype(float) emissions_merged[
= (
emissions_table
emissions_merged.rename(={"dep_y": "dep", "NBPERSMENFISC16": "population", "MED16": "revenu"}
columns
)"dep")
.groupby(
.agg("empreinte": "sum", "revenu": "median", "population": "sum"}
{# pas vraiment le revenu médian
)
.reset_index()="empreinte")
.sort_values(by )
Dans ce tableau nous allons intégrer des barres horizontales, à la manière des exemples présentés ici. Cela se fait en incluant directement le code html dans la colonne du DataFrame
def create_bar(
float, max_width: int, height: int, color: str = "green"
prop_fill: -> str:
) """Create divs to represent prop_fill as a bar."""
= round(max_width * prop_fill, 2)
width = f"{width}px"
px_width return f"""\
<div style="width: {max_width}px; background-color: lightgrey;">\
<div style="height:{height}px;width:{px_width};background-color:{color};"></div>\
</div>\
"""
= {"empreinte": "green", "revenu": "red", "population": "blue"}
colors
for variable in ["empreinte", "revenu", "population"]:
f"raw_perc_{variable}"] = (
emissions_table[/ emissions_table[variable].max()
emissions_table[variable]
)f"bar_{variable}"] = emissions_table[f"raw_perc_{variable}"].map(
emissions_table[lambda x: create_bar(x, max_width=75, height=20, color=colors[variable])
)
Nous ne gardons que les 5 plus petites empreintes carbone, et les cinq plus importantes.
= (
emissions_min 5)
emissions_table.head(="5 départements les moins pollueurs")
.assign(grp=True)
.reset_index(drop
)= (
emissions_max 5)
emissions_table.tail(="5 départements les plus pollueurs")
.assign(grp=True)
.reset_index(drop
)
= pd.concat([emissions_min, emissions_max]) emissions_table
Enfin, pour pouvoir utiliser quelques fonctions pratiques pour sélectionner des colonnes à partir de motifs, nous allons convertir les données au format Polars
import polars as pl
= pl.from_pandas(emissions_table) emissions_table
En prenant comme base ce tableau
="grp", rowname_col="dep") GT(emissions_table, groupname_col
construire un tableau dans le style de celui ci-dessous
# Start from here
from great_tables import GT
="grp", rowname_col="dep") GT(emissions_table, groupname_col
empreinte | revenu | population | raw_perc_empreinte | bar_empreinte | raw_perc_revenu | bar_revenu | raw_perc_population | bar_population | |
---|---|---|---|---|---|---|---|---|---|
5 départements les moins pollueurs | |||||||||
75 | 184.7103295610044 | 30507.25 | 2074629.5 | 0.0010088939415239168 | 1.0 | 0.8290666738598547 | |||
92 | 414.43945557445903 | 28565.37037037037 | 1583682.0 | 0.0022636820412333665 | 0.9363469460659473 | 0.6328734688250227 | |||
93 | 463.375783649003 | 17269.63636363636 | 1586664.5 | 0.0025309738869691957 | 0.5660830249739442 | 0.6340653401229036 | |||
94 | 651.4230957960531 | 22431.25 | 1345977.0 | 0.0035580945379686127 | 0.7352760409410878 | 0.537881426289304 | |||
06 | 3412.4578448016878 | 21259.45 | 1106770.0 | 0.018638957840141405 | 0.69686549918462 | 0.4422891521728923 | |||
5 départements les plus pollueurs | |||||||||
70 | 120801.03631184295 | 20047.305555555555 | 225876.5 | 0.6598192637871786 | 0.6571325031117375 | 0.09026511893237105 | |||
76 | 122166.44319426689 | 21045.833333333336 | 1208055.0 | 0.6672771614297552 | 0.6898633384960406 | 0.482764821713837 | |||
88 | 123129.82945046567 | 19928.35714285714 | 353370.0 | 0.6725392090885752 | 0.6532334819709131 | 0.14121427008622833 | |||
62 | 125859.88148645309 | 19668.888888888887 | 1439504.0 | 0.6874508437854484 | 0.6447283478153188 | 0.5752568317803041 | |||
80 | 183082.00887994491 | 20038.09523809524 | 535822.5 | 1.0 | 0.6568305972545949 | 0.21412622246732343 |
Le tableau à obtenir:
Empreinte carbone | |||||||||
---|---|---|---|---|---|---|---|---|---|
Premières statistiques descriptives à affiner | |||||||||
Empreinte | Revenu médian | Population | |||||||
Empreinte carbone | (%)* | Revenu | (%)* | Population | (%)* | ||||
5 départements les moins pollueurs | |||||||||
75 | 18.47 | 0.1% | 30.5K | 100.0% | 2.07M | 82.9% | |||
92 | 41.44 | 0.2% | 28.6K | 93.6% | 1.58M | 63.3% | |||
93 | 46.34 | 0.3% | 17.3K | 56.6% | 1.59M | 63.4% | |||
94 | 65.14 | 0.4% | 22.4K | 73.5% | 1.35M | 53.8% | |||
06 | 341.25 | 1.9% | 21.3K | 69.7% | 1.11M | 44.2% | |||
5 départements les plus pollueurs | |||||||||
70 | 12,080.10 | 66.0% | 20.0K | 65.7% | 225.88K | 9.0% | |||
76 | 12,216.64 | 66.7% | 21.0K | 69.0% | 1.21M | 48.3% | |||
88 | 12,312.98 | 67.3% | 19.9K | 65.3% | 353.37K | 14.1% | |||
62 | 12,585.99 | 68.7% | 19.7K | 64.5% | 1.44M | 57.5% | |||
80 | 18,308.20 | 100.0% | 20.0K | 65.7% | 535.82K | 21.4% | |||
*Note: Le revenu médian présenté ici est une approximation du revenu médian du département. | |||||||||
Lecture: Les colonnes (%) présentées ci-dessus sont rapportées à la valeur maximale de la variable | |||||||||
Source: Calculs à partir des données de l'Ademe |
Grâce à celui-ci, on peut déjà comprendre que notre définition de l’empreinte carbone est certainement défaillante. Il apparaît peu plausible que les habitants du 77 aient une empreinte 500 fois supérieure à celle de Paris intra-muros. La raison principale ? On n’est pas sur un concept d’émissions à la consommation mais à la production, ce qui pénalise les espaces industriels ou les espaces avec des aéroports…
Pour aller plus loin sur la construction de tableaux
avec great_tables
, vous pouvez répliquer
cet exercice
de production de tableaux électoraux
que j’ai proposé pour un cours de R
avec gt
, l’équivalent
de great_tables
pour R
.
7 Pandas
: vers la pratique et au-delà
7.1 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
= [[8000, 1000], [9500, np.nan], [5000, 2000]]
data = pd.DataFrame(data, columns=["salaire", "autre_info"])
df "salaire_net"] = df["salaire"] * 0.8 df[
En SQL
on pourrait directement mettre à jour notre base de données avec la nouvelle colonne :
SELECT *, salaire*0.8 AS salaire_net FROM df
L’é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
:
%>% mutate(salaire_net = salaire*0.8) df
Techniquement on pourrait faire ceci avec un assign
en Pandas
1= df.drop("salaire_net", axis="columns")
df = df.assign(salaire_net=lambda s: s["salaire"] * 0.8) df
- 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):
"salaire_net"] = df[col] * taux
df[return df
Ce qui transforme notre chaine de production en
"salaire")) (df.pipe(calcul_salaire_net,
salaire | autre_info | salaire_net | |
---|---|---|---|
0 | 8000 | 1000.0 | 6400.0 |
1 | 9500 | NaN | 7600.0 |
2 | 5000 | 2000.0 | 4000.0 |
7.2 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.groupby("dep").agg({"Agriculture": "mean"}).reset_index()
emissions_moyennes = emissions.merge(
emissions_enrichies ="dep", suffixes=["", "_moyenne_dep"]
emissions_moyennes, on
)"relatives"] = (
emissions_enrichies["Agriculture"] / emissions_enrichies["Agriculture_moyenne_dep"]
emissions_enrichies[
) emissions_enrichies.head()
id_left | INSEE commune | Commune | Agriculture | Autres transports | Autres transports international | CO2 biomasse hors-total | Déchets | Energie | Industrie hors-énergie | Résidentiel | Routier | Tertiaire | dep | emissions | Agriculture_moyenne_dep | relatives | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 01001 | L'ABERGEMENT-CLEMENCIAT | 3711.425991 | NaN | NaN | 432.751835 | 101.430476 | 2.354558 | 6.911213 | 309.358195 | 793.156501 | 367.036172 | 01 | 11448.849883 | 1974.535382 | 1.879645 |
1 | 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 | 2667.623238 | 1974.535382 | 0.240730 |
2 | 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 | 126523.378237 | 1974.535382 | 0.252740 |
3 | 3 | 01005 | AMBERIEUX-EN-DOMBES | 1859.160954 | NaN | NaN | 1144.429311 | 216.217508 | 94.182310 | 276.448534 | 663.683146 | 1756.341319 | 782.404357 | 01 | 13591.734878 | 1974.535382 | 0.941569 |
4 | 4 | 01006 | AMBLEON | 448.966808 | NaN | NaN | 77.033834 | 48.401549 | NaN | NaN | 43.714019 | 398.786800 | 51.681756 | 01 | 2145.169533 | 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["dep"] == "12") & (emissions["Routier"] > 500),
(emissions["INSEE commune", "Commune"],
[5)
].head( )
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>500
Dans le tidyverse (R
) on pourrait aussi faire ceci simplement
%>%
df filter(dep=="12", Routier>500) %>%
select(`INSEE commune`, `Commune`)
8 Les autres paradigmes
Ces deux chapitres ont permis d’explorer en profondeur la richesse de l’écosystème Pandas
qui est un indispensable dans la boite à outil du data scientist. Malgré toutes les limites que nous avons pu é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
.
8.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
= pl.from_pandas(emissions)
emissions_polars
(filter(pl.col("dep") == "12", pl.col("Routier") > 500)
emissions_polars."INSEE commune", "Commune")
.select(5)
.head( )
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.
8.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.
8.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
environment files have been tested on.
Latest built version: 2024-11-20
Python version used:
'3.12.6 | packaged by conda-forge | (main, Sep 30 2024, 18:08:52) [GCC 13.3.0]'
Package | Version |
---|---|
affine | 2.4.0 |
aiobotocore | 2.15.1 |
aiohappyeyeballs | 2.4.3 |
aiohttp | 3.10.8 |
aioitertools | 0.12.0 |
aiosignal | 1.3.1 |
alembic | 1.13.3 |
altair | 5.4.1 |
aniso8601 | 9.0.1 |
annotated-types | 0.7.0 |
appdirs | 1.4.4 |
archspec | 0.2.3 |
asttokens | 2.4.1 |
attrs | 24.2.0 |
babel | 2.16.0 |
bcrypt | 4.2.0 |
beautifulsoup4 | 4.12.3 |
black | 24.8.0 |
blinker | 1.8.2 |
blis | 0.7.11 |
bokeh | 3.5.2 |
boltons | 24.0.0 |
boto3 | 1.35.23 |
botocore | 1.35.23 |
branca | 0.7.2 |
Brotli | 1.1.0 |
bs4 | 0.0.2 |
cachetools | 5.5.0 |
cartiflette | 0.0.2 |
Cartopy | 0.24.1 |
catalogue | 2.0.10 |
cattrs | 24.1.2 |
certifi | 2024.8.30 |
cffi | 1.17.1 |
charset-normalizer | 3.3.2 |
chromedriver-autoinstaller | 0.6.4 |
click | 8.1.7 |
click-plugins | 1.1.1 |
cligj | 0.7.2 |
cloudpathlib | 0.20.0 |
cloudpickle | 3.0.0 |
colorama | 0.4.6 |
comm | 0.2.2 |
commonmark | 0.9.1 |
conda | 24.9.1 |
conda-libmamba-solver | 24.7.0 |
conda-package-handling | 2.3.0 |
conda_package_streaming | 0.10.0 |
confection | 0.1.5 |
contextily | 1.6.2 |
contourpy | 1.3.0 |
cryptography | 43.0.1 |
cycler | 0.12.1 |
cymem | 2.0.8 |
cytoolz | 1.0.0 |
dask | 2024.9.1 |
dask-expr | 1.1.15 |
databricks-sdk | 0.33.0 |
debugpy | 1.8.6 |
decorator | 5.1.1 |
Deprecated | 1.2.14 |
diskcache | 5.6.3 |
distributed | 2024.9.1 |
distro | 1.9.0 |
docker | 7.1.0 |
duckdb | 0.10.1 |
en-core-web-sm | 3.7.1 |
entrypoints | 0.4 |
et_xmlfile | 2.0.0 |
exceptiongroup | 1.2.2 |
executing | 2.1.0 |
fastexcel | 0.11.6 |
fastjsonschema | 2.20.0 |
fiona | 1.10.1 |
Flask | 3.0.3 |
folium | 0.17.0 |
fontawesomefree | 6.6.0 |
fonttools | 4.54.1 |
fr-core-news-sm | 3.7.0 |
frozendict | 2.4.4 |
frozenlist | 1.4.1 |
fsspec | 2023.12.2 |
funcy | 2.0 |
gensim | 4.3.2 |
geographiclib | 2.0 |
geopandas | 1.0.1 |
geoplot | 0.5.1 |
geopy | 2.4.1 |
gitdb | 4.0.11 |
GitPython | 3.1.43 |
google-auth | 2.35.0 |
graphene | 3.3 |
graphql-core | 3.2.4 |
graphql-relay | 3.2.0 |
graphviz | 0.20.3 |
great-tables | 0.12.0 |
greenlet | 3.1.1 |
gunicorn | 22.0.0 |
h11 | 0.14.0 |
h2 | 4.1.0 |
hpack | 4.0.0 |
htmltools | 0.6.0 |
hyperframe | 6.0.1 |
idna | 3.10 |
imageio | 2.36.0 |
importlib_metadata | 8.5.0 |
importlib_resources | 6.4.5 |
inflate64 | 1.0.0 |
ipykernel | 6.29.5 |
ipython | 8.28.0 |
itsdangerous | 2.2.0 |
jedi | 0.19.1 |
Jinja2 | 3.1.4 |
jmespath | 1.0.1 |
joblib | 1.4.2 |
jsonpatch | 1.33 |
jsonpointer | 3.0.0 |
jsonschema | 4.23.0 |
jsonschema-specifications | 2024.10.1 |
jupyter-cache | 1.0.0 |
jupyter_client | 8.6.3 |
jupyter_core | 5.7.2 |
kaleido | 0.2.1 |
kiwisolver | 1.4.7 |
langcodes | 3.5.0 |
language_data | 1.3.0 |
lazy_loader | 0.4 |
libmambapy | 1.5.9 |
locket | 1.0.0 |
lxml | 5.3.0 |
lz4 | 4.3.3 |
Mako | 1.3.5 |
mamba | 1.5.9 |
mapclassify | 2.8.1 |
marisa-trie | 1.2.1 |
Markdown | 3.6 |
markdown-it-py | 3.0.0 |
MarkupSafe | 2.1.5 |
matplotlib | 3.9.2 |
matplotlib-inline | 0.1.7 |
mdurl | 0.1.2 |
menuinst | 2.1.2 |
mercantile | 1.2.1 |
mizani | 0.11.4 |
mlflow | 2.16.2 |
mlflow-skinny | 2.16.2 |
msgpack | 1.1.0 |
multidict | 6.1.0 |
multivolumefile | 0.2.3 |
munkres | 1.1.4 |
murmurhash | 1.0.10 |
mypy-extensions | 1.0.0 |
narwhals | 1.14.1 |
nbclient | 0.10.0 |
nbformat | 5.10.4 |
nest_asyncio | 1.6.0 |
networkx | 3.3 |
nltk | 3.9.1 |
numexpr | 2.10.1 |
numpy | 1.26.4 |
opencv-python-headless | 4.10.0.84 |
openpyxl | 3.1.5 |
opentelemetry-api | 1.16.0 |
opentelemetry-sdk | 1.16.0 |
opentelemetry-semantic-conventions | 0.37b0 |
outcome | 1.3.0.post0 |
OWSLib | 0.28.1 |
packaging | 24.1 |
pandas | 2.2.3 |
paramiko | 3.5.0 |
parso | 0.8.4 |
partd | 1.4.2 |
pathspec | 0.12.1 |
patsy | 0.5.6 |
Pebble | 5.0.7 |
pexpect | 4.9.0 |
pickleshare | 0.7.5 |
pillow | 10.4.0 |
pip | 24.2 |
platformdirs | 4.3.6 |
plotly | 5.24.1 |
plotnine | 0.13.6 |
pluggy | 1.5.0 |
polars | 1.8.2 |
preshed | 3.0.9 |
prometheus_client | 0.21.0 |
prometheus_flask_exporter | 0.23.1 |
prompt_toolkit | 3.0.48 |
protobuf | 4.25.3 |
psutil | 6.0.0 |
ptyprocess | 0.7.0 |
pure_eval | 0.2.3 |
py7zr | 0.20.8 |
pyarrow | 17.0.0 |
pyarrow-hotfix | 0.6 |
pyasn1 | 0.6.1 |
pyasn1_modules | 0.4.1 |
pybcj | 1.0.2 |
pycosat | 0.6.6 |
pycparser | 2.22 |
pycryptodomex | 3.21.0 |
pydantic | 2.9.2 |
pydantic_core | 2.23.4 |
Pygments | 2.18.0 |
pyLDAvis | 3.4.1 |
PyNaCl | 1.5.0 |
pynsee | 0.1.8 |
pyogrio | 0.10.0 |
pyOpenSSL | 24.2.1 |
pyparsing | 3.1.4 |
pyppmd | 1.1.0 |
pyproj | 3.7.0 |
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.1 |
PyYAML | 6.0.2 |
pyzmq | 26.2.0 |
pyzstd | 0.16.2 |
querystring_parser | 1.2.4 |
rasterio | 1.4.2 |
referencing | 0.35.1 |
regex | 2024.9.11 |
requests | 2.32.3 |
requests-cache | 1.2.1 |
retrying | 1.3.4 |
rich | 13.9.4 |
rpds-py | 0.21.0 |
rsa | 4.9 |
Rtree | 1.3.0 |
ruamel.yaml | 0.18.6 |
ruamel.yaml.clib | 0.2.8 |
s3fs | 2023.12.2 |
s3transfer | 0.10.2 |
scikit-image | 0.24.0 |
scikit-learn | 1.5.2 |
scipy | 1.13.0 |
seaborn | 0.13.2 |
selenium | 4.26.1 |
setuptools | 74.1.2 |
shapely | 2.0.6 |
shellingham | 1.5.4 |
six | 1.16.0 |
smart-open | 7.0.5 |
smmap | 5.0.0 |
sniffio | 1.3.1 |
sortedcontainers | 2.4.0 |
soupsieve | 2.5 |
spacy | 3.7.5 |
spacy-legacy | 3.0.12 |
spacy-loggers | 1.0.5 |
SQLAlchemy | 2.0.35 |
sqlparse | 0.5.1 |
srsly | 2.4.8 |
stack-data | 0.6.2 |
statsmodels | 0.14.4 |
tabulate | 0.9.0 |
tblib | 3.0.0 |
tenacity | 9.0.0 |
texttable | 1.7.0 |
thinc | 8.2.5 |
threadpoolctl | 3.5.0 |
tifffile | 2024.9.20 |
toolz | 1.0.0 |
topojson | 1.9 |
tornado | 6.4.1 |
tqdm | 4.66.5 |
traitlets | 5.14.3 |
trio | 0.27.0 |
trio-websocket | 0.11.1 |
truststore | 0.9.2 |
typer | 0.13.1 |
typing_extensions | 4.12.2 |
tzdata | 2024.2 |
Unidecode | 1.3.8 |
url-normalize | 1.4.3 |
urllib3 | 1.26.20 |
wasabi | 1.1.3 |
wcwidth | 0.2.13 |
weasel | 0.4.1 |
webdriver-manager | 4.0.2 |
websocket-client | 1.8.0 |
Werkzeug | 3.0.4 |
wheel | 0.44.0 |
wordcloud | 1.9.3 |
wrapt | 1.16.0 |
wsproto | 1.2.0 |
xgboost | 2.1.1 |
xlrd | 2.0.1 |
xyzservices | 2024.9.0 |
yarl | 1.13.1 |
yellowbrick | 1.5 |
zict | 3.0.0 |
zipp | 3.20.2 |
zstandard | 0.23.0 |
View file history
SHA | Date | Author | Description |
---|---|---|---|
9cf2bde | 2024-10-18 15:49:47 | lgaliana | Reconstruction complète du chapitre de cartographie |
3b6301d | 2024-09-25 21:11:19 | lgaliana | change order |
127d17c | 2024-09-25 19:24:01 | lgaliana | formatting |
f243063 | 2024-09-25 19:21:23 | lgaliana | le bon chemin |
3fc7b54 | 2024-09-25 19:11:23 | lgaliana | build pipeline |
6a00c51 | 2024-09-25 17:28:46 | Lino Galiana | Special callouts for vscode (#558) |
488780a | 2024-09-25 14:32:16 | Lino Galiana | Change badge (#556) |
46f038a | 2024-09-23 15:28:36 | Lino Galiana | Mise à jour du premier chapitre sur les figures (#553) |
59f5803 | 2024-09-22 16:41:46 | Lino Galiana | Update bike count source data for visualisation tutorial (#552) |
4404ec1 | 2024-09-21 18:28:42 | lgaliana | Back to eval true |
72d44dd | 2024-09-21 12:50:38 | lgaliana | Force build for pandas chapters |
0908656 | 2024-08-20 16:30:39 | Lino Galiana | English sidebar (#542) |
5108922 | 2024-08-08 18:43:37 | Lino Galiana | Improve notebook generation and tests on PR (#536) |
f4367f7 | 2024-08-08 11:44:37 | Lino Galiana | Traduction chapitre suite Pandas avec un profile (#534) |
72f42bb | 2024-07-25 19:06:38 | Lino Galiana | Language message on notebooks (#529) |
195dc9e | 2024-07-25 11:59:19 | linogaliana | Switch language button |
fe4edc9 | 2024-07-10 18:37:22 | Lino Galiana | Second pandas chapter translated (#524) |
065b0ab | 2024-07-08 11:19:43 | Lino Galiana | Nouveaux callout dans la partie manipulation (#513) |
c6ffbb6 | 2024-05-08 07:38:24 | lgaliana | Putting badges in advanced Pandas |
c3873ed | 2024-05-07 12:19:12 | lgaliana | Ajoute code commune |
e0d615e | 2024-05-03 11:15:29 | Lino Galiana | Restructure la partie Pandas (#497) |
Notes de bas de page
Ideally, it would be more coherent, for demographic data, to use the legal populations, from the census. However, this base is not yet natively integrated into the
pynsee
library that we will use in this chapter. An open exercise is proposed to construct population aggregates from anonymized individual census data (the detailed files).↩︎Otherwise, we enter the realm of fuzzy matching or probabilistic matching. Fuzzy matching occurs when we no longer have an exact identifier to link two databases but have partially noisy information between two sources to make the connection. For example, in a product database, we might have
Coca Cola 33CL
and in anotherCoca Cola canette
, but these names hide the same product. The chapter on Introduction to Textual Search with ElasticSearch addresses this issue. Probabilistic matching is another approach. In these, observations in two databases are associated not based on an identifier but on the distance between a set of characteristics in both databases. This technique is widely used in medical statistics or in the evaluation of public policies based on propensity score matching.↩︎Ideally, it would be necessary to ensure that this join does not introduce bias. Indeed, since our reference years are not necessarily identical, there may be a mismatch between our two sources. Since the exercise is already long, we will not go down this path. Interested readers can perform such an analysis as an additional exercise.↩︎
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 = {fr}
}