diff --git a/.gitignore b/.gitignore index 0429e15e6d..ca1f195edf 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ jsconfig.json /.luarc.json **/*.quarto_ipynb +*.quarto_ipynb* +.vscode/ \ No newline at end of file diff --git a/_quarto.yml b/_quarto.yml index fa7b6a64f4..a1dfe949e7 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -5,10 +5,10 @@ project: - 404.qmd - content/getting-started/index.qmd - content/manipulation/index.qmd + - content/manipulation/02_pandas_suite.qmd - content/visualisation/index.qmd - content/getting-started/01_environment.qmd - content/modelisation/index.qmd - - content/modelisation/4_featureselection.qmd - content/NLP/index.qmd - content/annexes/corrections.qmd - content/annexes/evaluation.qmd diff --git a/content/manipulation/02_pandas_suite.qmd b/content/manipulation/02_pandas_suite.qmd index 36fbca37e3..6ed6f5f2d3 100644 --- a/content/manipulation/02_pandas_suite.qmd +++ b/content/manipulation/02_pandas_suite.qmd @@ -73,14 +73,15 @@ eval: false Le [chapitre d'introduction à `Pandas`](/content/manipulation/02_pandas_intro.qmd) 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. -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. +Il est en effet 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. + 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](/content/nlp/index.qmd), 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](https://fr.wikipedia.org/wiki/Inf%C3%A9rence_statistique#:~:text=L'inf%C3%A9rence%20statistique%20est%20l,%3A%20la%20probabilit%C3%A9%20d'erreur.), 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. @@ -100,13 +101,14 @@ Grâce à ce travail, nous allons approfondir notre compréhension d'un phénom ::: {.content-visible when-profile="en"} The [introductory chapter to `Pandas`](/content/manipulation/02_pandas_intro.qmd) presented the concept of data organized in the form of a _DataFrame_ and the practicality of the `Pandas` ecosystem for performing simple operations on a dataset. -It is rare to work exclusively on a raw source. A dataset generally gains value when compared to other sources. For researchers, this allows contextualizing the information present in one source by comparing or associating it with other sources. For data scientists in the private sector, it often involves linking information about the same person in multiple customer databases or comparing customers with each other. - One of the benefits of modern data science tools, especially `Pandas`, is the ease with which they allow restructuring sources to work on multiple datasets in a project. This chapter consolidates the principles previously seen by refining the data processing. It will mainly explore two types of operations: - Group descriptive statistics; - Data merging by common characteristics. +It is indeed rare to work exclusively on a raw source. A dataset generally gains value when compared to other sources. For researchers, this allows contextualizing the information present in one source by comparing or associating it with other sources. For data scientists in the private sector, it often involves linking information about the same person in multiple customer databases or comparing customers with each other. + + Performing this work simply, reliably, and efficiently is essential for data scientists as this task is common. Fortunately, `Pandas` handles this very well with structured data. In the following chapters, and also throughout the [section on text data processing](/content/nlp/index.qmd), we will see how to handle less structured data. Through this work, we will deepen our understanding of a real world phenomenon through detailed descriptive statistics. This is an essential step before moving on to [inferential statistics](https://en.wikipedia.org/wiki/Statistical_inference), the approach that consists of formalizing and generalizing correlations or causal relationships between observed characteristics and a phenomenon. @@ -129,8 +131,7 @@ Through this work, we will deepen our understanding of a real world phenomenon t 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: +Comme expliqué ci-dessous, nous allons interroger directement l'API `Melodi` de l'Insee, via la librairie `requests`, pour récupérer les données utiles à l'enrichissement de notre jeu de données de l'Ademe. Aucune librairie spécifique n'est donc nécessaire pour cette tâche, contrairement à la librairie `great_tables` que nous verrons à la fin de ce chapitre et qu'il convient d'installer au préalable: ::: ::: {.content-visible when-profile="en"} @@ -138,27 +139,21 @@ il est nécessaire de l'installer, comme la librairie `great_tables` que nous ve The previous chapter used almost exclusively the `Pandas` library. In this chapter, we will use other packages in addition to it. -As explained below, we will use a library called `pynsee` to retrieve Insee data useful for enriching our Ademe dataset. This library is not installed by default in `Python`. Before using it, it is necessary to install it, along with the `great_tables` library that we will see at the end of this chapter: +As explained below, we will query the Insee `Melodi` API directly, using the `requests` library, to retrieve the data useful for enriching our Ademe dataset. No dedicated library is therefore needed for this task, unlike the `great_tables` library that we will see at the end of this chapter, which needs to be installed beforehand: ::: ```{python} #| eval: false #| echo: true -!pip install pynsee --quiet !pip install great_tables --quiet ``` ::: {.content-visible when-profile="fr"} -L'instruction `!pip install ` 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 ``) -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: ::: ::: {.content-visible when-profile="en"} -The instruction `!pip install ` is a way to tell `Jupyter`, the execution engine behind notebooks, that the following command (`pip install `) is a system command to be executed outside of `Python` (in the terminal, for example, for a `Linux` system). - The essential packages to start this chapter are as follows: ::: ```{python} @@ -166,8 +161,7 @@ The essential packages to start this chapter are as follows: import numpy as np import pandas as pd import matplotlib.pyplot as plt -import pynsee -import pynsee.download +import requests ``` {{< include "02_pandas_intro/_reproducible.qmd" >}} @@ -181,7 +175,7 @@ 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](https://www.data.gouv.fr/fr/datasets/inventaire-de-gaz-a-effet-de-serre-territorialise/#_) et requêtable directement dans `Python` avec -[cet url](https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert) ; +[cet url](https://data.ademe.fr/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert) ; 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: @@ -190,57 +184,41 @@ Les problématiques d'enrichissement de données (association d'une source à un [code officiel géographique](https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv), 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_](https://www.insee.fr/fr/metadonnees/source/serie/s1172), 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 populations[^poplegales] au niveau communal afin de les mettre en regard de nos données d'émissions. +* Les données [_Filosofi_](https://www.insee.fr/fr/metadonnees/source/serie/s1172), 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 la population[^poplegales] au niveau communal afin de les mettre en regard de nos données d'émissions. -[^poplegales]: Idéalement il serait plus cohérent, pour les données démographiques, d'utiliser les [populations légales](https://www.insee.fr/fr/information/2008354), issues du recensement. Néanmoins cette base n'est pas encore intégrée nativement dans la librairie `pynsee` que nous allons utiliser dans ce chapitre. Un exercice d'ouverture est proposé pour construire des agrégats de population à partir des jeux de données individuels anonymisés du recensement (les [fichiers détails](https://www.insee.fr/fr/information/2383306)). +[^poplegales]: Pour les données démographiques, on utilise directement les [populations légales](https://www.insee.fr/fr/information/2008354) diffusées par l'Insee, issues du recensement, plutôt qu'un indicateur de population fiscale. Un exercice d'ouverture est également proposé pour construire des agrégats de population à partir des jeux de données individuels anonymisés du recensement (les [fichiers détails](https://www.insee.fr/fr/information/2383306)). -Pour faciliter l'import de données Insee, il est recommandé d'utiliser le _package_ -[`pynsee`](https://pynsee.readthedocs.io/en/latest/) qui simplifie l'accès aux principaux jeux de données -de l'Insee disponibles sur le site web [insee.fr](https://www.insee.fr/fr/accueil) -ou via des API. +Pour importer ces données, nous allons directement interroger l'API +[`Melodi`](https://api.insee.fr/catalogue/), le nouveau point d'entrée mis à disposition par l'Insee pour diffuser ses principales sources de données, dont fait partie _Filosofi_. Il ne s'agit donc plus d'un _package_ `Python` dédié mais d'une API classique, interrogeable avec `requests`. Plus d'éléments sur l'exploitation de JSON via Python seront proposés dans le [chapitre consacré aux API](/content/manipulation/04c_API_TP.qmd). :::: {.callout-note} -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](https://www.insee.fr/fr/accueil)) +Une précédente version de ce tutoriel s'appuyait sur le *package* communautaire [`pynsee`](https://github.com/InseeFrLab/pynsee) qui simplifie l'accès aux données officielles de l'institut. -Dans ce chapitre, nous allons exclusivement utiliser cette deuxième -approche. Cela se fera par le module `pynsee.download`. +Cependant, les sources qui nous sont ici utiles ont été intégrées dans une nouvelle API de l'Insee nommée `Melodi`. Celle-ci renvoie des données structurées au format `JSON`: chaque +observation associe des dimensions (par exemple `GEO` pour la zone géographique ou `TIME_PERIOD` pour la période) à une valeur (`OBS_VALUE_NIVEAU`). Plus d'éléments sur l'exploitation de JSON via Python seront proposés dans le [chapitre consacré aux API](/content/manipulation/04c_API_TP.qmd). -La liste des données disponibles depuis ce _package_ est [ici](https://inseefrlab.github.io/DoReMIFaSol/articles/donnees_dispo.html). -La fonction `download_file` attend un identifiant unique -pour savoir quelle base de données aller chercher et -restructurer depuis le -site [insee.fr](https://www.insee.fr/fr/accueil). +Chaque source dispose d'un identifiant unique (par exemple `DS_FILOSOFI_CC` pour les données Filosofi communales, `DS_POPULATIONS_HISTORIQUES` pour les populations légales). Le point d'entrée principal est de la forme `https://api.insee.fr/melodi/data/{identifiant}`, que l'on peut filtrer +par dimension (zone géographique, période, indicateur...). La structure de chaque fichier dépend de la source demandée. Il y a donc généralement un travail propre à chaque source à mettre en oeuvre.
-Connaître la liste des bases disponibles +Explorer la structure d'une source -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 : +Pour connaître la structure d'une source (dimensions, indicateurs +disponibles...), on peut consulter ses métadonnées, également disponibles au format `JSON` : ```{python} #| echo: true #| eval: false -import pynsee -meta = pynsee.get_file_list() -meta.loc[meta['label'].str.contains(r"Filosofi.*2016")] +import requests +requests.get("https://api.insee.fr/melodi/catalog/DS_FILOSOFI_CC").json() ``` -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_") +La liste des sources disponibles sur l'API Melodi est quant à elle consultable sur le [portail des API](https://catalogue-donnees.insee.fr/fr/catalogue/recherche) de l'Insee.
@@ -254,44 +232,46 @@ Ici, `meta['label'].str.contains(r"Filosofi.*2016")` signifie: This tutorial continues the exploration of the dataset from the previous chapter: -* Greenhouse gas emissions estimated at the municipal level by ADEME. The dataset is available on [data.gouv](https://www.data.gouv.fr/fr/datasets/inventaire-de-gaz-a-effet-de-serre-territorialise/#_) and can be directly queried in Python with [this URL](https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert); +* Greenhouse gas emissions estimated at the municipal level by ADEME. The dataset is available on [data.gouv](https://www.data.gouv.fr/fr/datasets/inventaire-de-gaz-a-effet-de-serre-territorialise/#_) and can be directly queried in Python with [this URL](https://data.ademe.fr/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert); The issues of data enrichment (associating one source with another based on common characteristics) will be presented using two sources produced by Insee: * The [official geographic code](https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv), a reference produced by Insee used to identify municipalities with a unique code, unlike the postal code; -* The [_Filosofi_](https://www.insee.fr/fr/metadonnees/source/serie/s1172) data, a source on French income at a fine spatial scale constructed by Insee from tax returns and social benefit information. In this case, we will use income levels and populations[^poplegales] at the municipal level to compare them with our emissions data. +* The [_Filosofi_](https://www.insee.fr/fr/metadonnees/source/serie/s1172) data, a source on French income at a fine spatial scale constructed by Insee from tax returns and social benefit information. In this case, we will use income levels and population[^poplegales] at the municipal level to compare them with our emissions data. -[^poplegales]: Ideally, it would be more coherent, for demographic data, to use the [legal populations](https://www.insee.fr/fr/information/2008354), 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](https://www.insee.fr/fr/information/2383306)). +[^poplegales]: For demographic data, we directly use the [legal populations](https://www.insee.fr/fr/information/2008354) published by Insee, derived from the census, rather than a fiscal-population proxy. An open exercise is also proposed to construct population aggregates from anonymized individual census data (the [detailed files](https://www.insee.fr/fr/information/2383306)). -To facilitate the import of Insee data, it is recommended to use the [`pynsee`](https://pynsee.readthedocs.io/en/latest/) package, which simplifies access to the main Insee datasets available on the [insee.fr](https://www.insee.fr/fr/accueil) website or via APIs. +To import this data, we will directly query the [`Melodi`](https://api.insee.fr/catalogue/) API, +Insee's new entry point for disseminating its main data sources, including _Filosofi_. This is no +longer a dedicated `Python` package but a standard API, queried with `requests`. More details on +handling JSON with Python will be given in the [chapter dedicated to APIs](/content/manipulation/04c_API_TP.qmd). :::: {.callout-note} -The `pynsee` package has two main entry points: - -- The Insee APIs, which will be illustrated in the dedicated chapter. -- Some datasets directly from the Insee website ([insee.fr](https://www.insee.fr/fr/accueil)) +A previous version of this tutorial relied on the community *package* [`pynsee`](https://github.com/InseeFrLab/pynsee), which simplifies access to the institute's official data. -In this chapter, we will exclusively use the second approach through the `pynsee.download` module. +However, the sources useful to us here have been integrated into a new Insee API named `Melodi`. It returns structured `JSON` data: each +observation associates dimensions (for example `GEO` for the geographic area or `TIME_PERIOD` for the period) with a value (`OBS_VALUE_NIVEAU`). More details on handling JSON with Python will be given in the [chapter dedicated to APIs](/content/manipulation/04c_API_TP.qmd). -The list of available data from this package is [here](https://inseefrlab.github.io/DoReMIFaSol/articles/donnees_dispo.html). The `download_file` function expects a unique identifier to know which database to fetch and restructure from the [insee.fr](https://www.insee.fr/fr/accueil) website. +Each source has a unique identifier (for example `DS_FILOSOFI_CC` for municipal Filosofi data, `DS_POPULATIONS_HISTORIQUES` for legal populations). The main entry point has the form `https://api.insee.fr/melodi/data/{identifier}`, which can be filtered +by dimension (geographic area, period, indicator...). The structure of each file depends on the source requested, so some source-specific work is generally needed.
-Knowing the list of available databases +Exploring the structure of a source -To know the list of available databases, you can use the `meta = pynsee.get_file_list()` function after importing `pynsee`. This returns a `DataFrame` in which you can search, for example, using a keyword search: +To know the structure of a source (dimensions, available indicators...), you can look at its +metadata, also available in `JSON` format: ```{python} #| echo: true #| eval: false -import pynsee -meta = pynsee.get_file_list() -meta.loc[meta['label'].str.contains(r"Filosofi.*2016")] +import requests +requests.get("https://api.insee.fr/melodi/catalog/DS_FILOSOFI_CC").json() ``` -Here, meta['label'].str.contains(r"Filosofi.*2016") means: "pandas find me all labels containing the terms Filosofi and 2016." (.* means "no matter the number of words or characters in between"). +The list of sources available on the Melodi API can be browsed on Insee's [API portal](https://catalogue-donnees.insee.fr/fr/catalogue/recherche).
@@ -322,7 +302,7 @@ As explained in the previous chapter, these data can be imported very simply wit #| echo: true import pandas as pd -url = "https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert" +url = "https://data.ademe.fr/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert" emissions = pd.read_csv(url) emissions.head(2) ``` @@ -337,6 +317,7 @@ We will already keep the names of the emitting sectors present in the database t ```{python} #| echo: true secteurs = emissions.select_dtypes(include='number').columns +secteurs ``` ::: {.content-visible when-profile="fr"} @@ -355,39 +336,91 @@ emissions['dep'] = emissions["INSEE commune"].str[: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` +On va utiliser les données Filosofi (données de revenus) au niveau communal, dans leur +millésime le plus récent (2023 au moment de la rédaction de ce cours). Ce n'est pas la même +année que les données d'émission de l'Ademe (régulièrement mises à jour, elles portent sur 2025), +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`. +Au niveau communal, la source Melodi `DS_FILOSOFI_CC` ne diffuse que le niveau de vie médian +(`MED_SL`) et le taux de pauvreté (`PR_MD60`) ; les autres indicateurs (déciles, indice de +Gini, décomposition du revenu...) ne sont disponibles qu'aux niveaux département, région et +France entière. On y ajoute la population municipale, disponible sur la source Melodi +`DS_POPULATIONS_HISTORIQUES`, ainsi que le nom des communes, issu du code officiel géographique. -Le code pour télécharger les données est le suivant : +Le code pour récupérer ces données est le suivant : ::: ::: {.content-visible when-profile="en"} -We will use the Filosofi data (income data) at the municipal level for 2016. It is not the same year as the CO2 emissions data, so it is not perfectly rigorous, but it will still illustrate the main functionalities of `Pandas`. +We will use the Filosofi data (income data) at the municipal level, in its most recent vintage +(2023 at the time of writing). This is not the same year as the Ademe emissions data (regularly +updated, it now covers 2025), so it is not perfectly rigorous, but it will still illustrate the +main functionalities of `Pandas`. -The main entry point for the `pynsee` function is the `download_file` function. +At the municipal level, the Melodi source `DS_FILOSOFI_CC` only disseminates the median +standard of living (`MED_SL`) and the poverty rate (`PR_MD60`); the other indicators (deciles, +Gini index, breakdown of disposable income...) are only available at the département, region +and metropolitan France levels. We add the municipal population, available from the Melodi +source `DS_POPULATIONS_HISTORIQUES`, as well as municipality names, taken from the official +geographic code. -The code to download the data is as follows: +The code to retrieve this data is as follows: ::: ```{python} #| echo: true #| output: false -#| eval: false -from pynsee.download import download_file -filosofi = download_file("FILOSOFI_COM_2016") -``` - -```{python} -#| echo: false -#| output: false +import requests import pandas as pd -backup_filosofi = "https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/filosofi.parquet" -filosofi = pd.read_parquet(backup_filosofi) + +def get_melodi(dataset, dimension, code, value_name, time_period, geo_level="COM"): + url = f"https://api.insee.fr/melodi/data/{dataset}" + params = { + "GEO": geo_level, + dimension: code, + "TIME_PERIOD": time_period, + "maxResult": 40000, + } + response = requests.get(url, params=params, timeout=60) + response.raise_for_status() + observations = response.json()["observations"] + return pd.DataFrame( + { + "CODGEO": obs["dimensions"]["GEO"].split("-")[-1], + value_name: obs["measures"]["OBS_VALUE_NIVEAU"].get("value"), + } + for obs in observations + ) + +niveau_vie = get_melodi("DS_FILOSOFI_CC", "FILOSOFI_MEASURE", "MED_SL", "NIVVIE_MEDIAN", 2023) +taux_pauvrete = get_melodi("DS_FILOSOFI_CC", "FILOSOFI_MEASURE", "PR_MD60", "TAUX_PAUVRETE", 2023) +population = get_melodi("DS_POPULATIONS_HISTORIQUES", "POPREF_MEASURE", "PMUN", "POPULATION", 2023) + +from io import StringIO + +url_cog_2023 = "https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv" +url_backup = "https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/cog_2023.csv" +try: + response = requests.get(url_cog_2023) + response.raise_for_status() + cog_2023 = pd.read_csv(StringIO(response.text)) +except requests.exceptions.Timeout: + cog_2023 = pd.read_csv(url_backup) + +noms_communes = ( + cog_2023 + .loc[cog_2023["TYPECOM"] == "COM", ["COM", "LIBELLE"]] + .rename(columns={"COM": "CODGEO", "LIBELLE": "LIBGEO"}) +) + +filosofi = ( + niveau_vie + .merge(taux_pauvrete, on="CODGEO", how="outer") + .merge(population, on="CODGEO", how="outer") + .merge(noms_communes, on="CODGEO", how="left") + [["CODGEO", "LIBGEO", "POPULATION", "NIVVIE_MEDIAN", "TAUX_PAUVRETE"]] +) ``` @@ -425,6 +458,7 @@ filosofi = ( {c: "float" for c in filosofi.columns[2:]} ) ) +filosofi["dep"] = filosofi["CODGEO"].str[:2] ``` ::: {.content-visible when-profile="fr"} @@ -435,1133 +469,1155 @@ 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 +Un jeu de données se suffit cependant rarement à lui-même. Pour donner du sens et de la valeur à une statistique, il faut généralement l'associer à de la connaissance annexe, sous peine qu'elle reste désincarnée. Ce type de croisement, qu'on appelle un enrichissement de données, est l'une des opérations les plus fréquentes en _data science_. Il peut se faire à un niveau identique à celui de la source d'origine (par exemple associer une base client à une base d'achats afin de mettre en regard un comportement d'achat avec des caractéristiques pouvant l'expliquer) ou à des niveaux conceptuels différents, en général plus agrégés, pour contextualiser une donnée plus fine (par exemple associer des temps de transport individuels à ceux d'une même classe d'âge ou de personnes résidant dans la même commune). + +Notre objectif immédiat va donc être de relier l'information contenue entre nos +deux jeux de données. Sans cela, 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`. +issue de `filosofi`. C'est cet enrichissement, par le biais d'une jointure, que nous allons mettre en oeuvre dès maintenant ; les statistiques descriptives par groupe, elles, viendront ensuite s'appliquer sur des données déjà enrichies. ::: ::: {.content-visible when-profile="en"} A quick glance at the data gives a fairly precise idea of how the data are organized. We notice that some variables in `filosofi` seem to have many missing values (statistical secrecy), while others seem complete. If we want to exploit `filosofi`, we need to pay attention to the chosen variable. -Our ultimate goal will be to link the information contained between these two datasets. Otherwise, we risk being frustrated: we will want to know more about carbon emissions but will be very limited in the possibilities of analysis without adding additional information from `filosofi`. +A dataset, however, is rarely sufficient on its own. To give meaning and value to a statistic, it generally needs to be associated with additional knowledge, otherwise it remains detached. This kind of pairing, known as data enrichment, is one of the most common operations in data science. It can happen at the same level as the original source (for example, associating a customer database with a purchase database to relate purchasing behavior to characteristics that may explain it) or at different, generally more aggregated, conceptual levels to contextualize finer data (for example, associating individual travel times with those of the same age group or people living in the same municipality). + +Our immediate goal is therefore to link the information contained in our two +datasets. Otherwise, we risk being frustrated: we will want to know more about carbon emissions but will be very limited in the possibilities of analysis without adding additional information from `filosofi`. This enrichment, through a join, is what we will implement right away; descriptive statistics by group will come afterwards, applied to already-enriched data. ::: ::: {.content-visible when-profile="fr"} -# Statistiques descriptives par groupe + +# Joindre des données ## 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). +Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation +où une information permet d'apparier de manière exacte deux bases de données[^flou]. +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. -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](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html). +Cette pratique découle du fait que de nombreux systèmes d'information prennent la forme d'un schéma en étoile: -L'image suivante, issue de -[ce site](https://unlhcc.github.io/r-novice-gapminder/16-plyr/), -représente bien la manière dont fonctionne l'approche -`split`-`apply`-`combine`: +![Illustration du schéma en étoile (Source: [Databricks](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png))](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png){fig-width="80%"} -![Split-apply-combine (Source: [unlhcc.github.io](https://unlhcc.github.io/r-novice-gapminder/16-plyr/))](https://unlhcc.github.io/r-novice-gapminder/fig/12-plyr-fig1.png){fig-width=70%} +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. -En `Pandas`, on utilise `groupby` pour découper les données selon un ou -plusieurs axes (ce [tutoriel](https://realpython.com/pandas-groupby/) 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. +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. -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_: ::: ::: {.content-visible when-profile="en"} -# Descriptive statistics by group +# Joining data ## Principle -In the previous chapter, we saw how to obtain an aggregated statistic easily with `Pandas`. However, it is common to have data with intermediate analysis strata that are relevant: geographical variables, membership in socio-demographic groups related to recorded characteristics, temporal period indicators, etc. To better understand the structure of the data, data scientists are often led to construct descriptive statistics on sub-groups present in the data. For example, we previously constructed emission statistics at the national level. But what about the emission profiles of different departments? To answer this question, it will be useful to aggregate our data at the departmental level. This will give us different information from the initial dataset (municipal level) and the most aggregated level (national level). +Here we will focus on the most favorable case, which is the situation where information allows for an exact match between two databases[^fuzzy]. It is a daily necessity for data scientists to merge information present in multiple files. For example, in business databases, customer information (address, age, etc.) will be in one file, sales in another, and product characteristics in a third file. To have a complete base that compares all this information, it will be necessary to join these three files based on common information. -In `SQL`, it is very simple to segment data to perform operations on coherent blocks and recollect results in the appropriate dimension. The underlying logic is that of *split-apply-combine*, which is adopted by data manipulation languages, including `pandas` [which is no exception](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html). +This practice stems from the fact that many information systems take the form of a star schema: -The following image, from [this site](https://unlhcc.github.io/r-novice-gapminder/16-plyr/), well represents how the `split`-`apply`-`combine` approach works: +![Illustration of the star schema (Source: [Databricks](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png))](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png){fig-width="80%"} -![Split-apply-combine (Source: [unlhcc.github.io](https://unlhcc.github.io/r-novice-gapminder/16-plyr/))](https://unlhcc.github.io/r-novice-gapminder/fig/12-plyr-fig1.png){fig-width=70%} +This structuring of information is closely related to the model of relational tables from the 1980s. Today, there are more flexible data models where information is stacked in a data lake without an a priori structure. Nevertheless, this star schema model retains relevance because it allows sharing information only with those who need it, leaving it to those who need to link data to do so. -In `Pandas`, we use `groupby` to segment the data according to one or more axes (this [tutorial](https://realpython.com/pandas-groupby/) on the subject is particularly useful). All the aggregation operations (counting, averages, etc.) that we saw earlier can be applied by group. +Since the logic of the star schema historically comes from relational databases, it is natural that it is an approach intrinsically linked to the philosophy of SQL, even in the vocabulary. The term "data join" is often used, inherited from the SQL `JOIN` term, and the way to describe joins (left join, right join...) comes directly from the associated SQL instructions. -Technically, this operation involves creating an association between labels (values of group variables) and observations. Using the `groupby` method does not trigger operations until a statistic is implemented; it simply creates a formal relationship between observations and groupings that will be used later: ::: -```{python} -#| echo: true -filosofi["dep"] = filosofi["CODGEO"].str[:2] -filosofi.groupby('dep').__class__ -``` - ::: {.content-visible when-profile="fr"} -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 : -::: - -::: {.content-visible when-profile="en"} -As long as we do not call an action on a `DataFrame` by group, such as `head` or `display`, `pandas` performs no operations. This is called *lazy evaluation*. For example, the result of `df.groupby('dep')` is a transformation that has not yet been evaluated: -::: - -```{python} -#| echo: true -filosofi.groupby('dep') -``` +## Mise en oeuvre avec `Pandas` -::: {.content-visible when-profile="fr"} +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. -## Illustration 1: dénombrement par groupe +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`: -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`: ::: ::: {.content-visible when-profile="en"} -## Illustration 1: counting by group +## Implementation with `Pandas` -To illustrate the application of this principle to counting, we can count the number of municipalities by department in 2023 (this statistic changes every year due to municipal mergers). For this, we simply take the reference of French municipalities from the official geographical code (COG) and count by department using `count`: +In `Pandas`, the most practical method to join datasets based on common characteristics is `merge`. Its main arguments allow for controlling the join behavior. We will explore them visually. +In our case, for constructing statistics on carbon emissions, the left base will be the `emissions` DataFrame, and the right base will be the `filosofi` DataFrame: ::: ```{python} #| echo: true -import requests -from io import StringIO -import pandas as pd - -url_cog_2023 = "https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv" -url_backup = "https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/cog_2023.csv" +emissions.head(2) +``` -# Try-except clause to avoid timout issue sometimes -# Without timeout problem, pd.read_csv(url_cog_2023) would be sufficient -try: - response = requests.get(url_cog_2023) - response.raise_for_status() - cog_2023 = pd.read_csv(StringIO(response.text)) -except requests.exceptions.Timeout: - print("Failing back to backup") - cog_2023 = pd.read_csv(url_backup) +```{python} +#| echo: true +filosofi.head(2) ``` +::: {.content-visible when-profile="fr"} +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). -::: {.content-visible when-profile="fr"} +* 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. -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: +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. -```{python} -#| echo: true -communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> -communes.loc[:, ['COM', 'DEP', 'REG']].nunique() -``` -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. ::: ::: {.content-visible when-profile="en"} +We refer to join keys as the variable(s) necessary for merging data. These are the variables common to both datasets. They do not need to have the same name, but they must share common values; otherwise, the intersection between these two datasets is the empty set. -With this dataset, without resorting to group statistics, we can already know how many municipalities, departments, and regions we have in France, respectively: +We can manipulate two dimensions in the join (this will be clearer later with graphical examples): + +* There are mainly three types of merges: left join, right join, or a combination of the two, depending on the type of pivot we want to implement. +* Then, there are two ways to merge the values once we have chosen a pivot: inner or outer join. In the first case, we only keep the observations where the join keys are present in both datasets; in the second, we keep all observations of the pivot key variables, even if the second dataset does not have such observations, resulting in missing values. + +In the examples below, we will use the commune codes and departments as join keys. Using the department is not necessary since it is directly deduced from the commune code, but it helps illustrate the principle of joins on multiple variables. Note that the name of the commune is intentionally set aside for joins, even though it is common information to both datasets. However, as it is a textual field, which may follow different formatting norms in the two datasets, it is not reliable for an exact join. + +To illustrate the principle of the left or right pivot, we will create two identifier variables for the row in our left and right datasets. This will allow us to easily find rows present in one dataset but not in the other. +::: ```{python} #| echo: true -communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> -communes.loc[:, ['COM', 'DEP', 'REG']].nunique() +emissions = emissions.reset_index(names = ['id_left']) +filosofi = filosofi.reset_index(names = ['id_right']) ``` -1. We restrict to the status "Commune" because this file also contains Insee codes for other statuses, such as the "Municipal Arrondissements" of Paris, Lyon, and Marseille. -::: +### _Left join_ ::: {.content-visible when-profile="fr"} -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`: +Commençons avec la jointure à gauche. Comme son nom l'indique, on va prendre la variable de gauche en pivot: ::: - ::: {.content-visible when-profile="en"} -Now, let's look at the departments with the most municipalities. It is the same counting function where we play, this time, on the group from which the statistic is calculated. - -Calculating this statistic is quite straightforward when you understand the principle of calculating statistics with `Pandas`: +Let's start with the left join. As its name indicates, we will take the left variable as the pivot: ::: +![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/left_join.png) + + ```{python} #| echo: true -communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> -communes.groupby('DEP').agg({'COM': 'nunique'}) +left_merged = emissions.merge( + filosofi, + left_on = ["INSEE commune", "dep"], + right_on = ["CODGEO", "dep"], + how = "left" +) +left_merged.head(3) ``` ::: {.content-visible when-profile="fr"} -En SQL, on utiliserait la requête suivante: +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: ::: + ::: {.content-visible when-profile="en"} -In SQL, we would use the following query: +It is recommended to always explicitly specify the join keys using the `left_on`, `right_on`, or `on` arguments if the variable names are common between the two datasets. +If there are common variable names between the datasets that are not defined as join keys, they will not be used for the join but will be retained with a suffix that defaults to `_x` and `_y` (configurable using the `suffixes` argument). + +The `Pandas` syntax is directly inspired by SQL, so we have a fairly transparent translation of the above instruction into SQL: ::: ```sql -SELECT dep, COUNT DISTINCT "COM" AS COM -FROM communes -GROUP BY dep -WHERE TYPECOM == 'COM'; +SELECT * +FROM emissions +LEFT JOIN filosofi + ON emissions.`INSEE commune` = filosofi.CODGEO + AND emissions.dep = filosofi.dep; ``` ::: {.content-visible when-profile="fr"} -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: +En faisant une jointure à gauche, on doit en principe avoir autant de lignes que la base de données à gauche: ::: + ::: {.content-visible when-profile="en"} -The output is an indexed `Series`. This is not very convenient as we mentioned in the previous chapter. It is more practical to transform this object into a `DataFrame` with `reset_index`. Finally, with `sort_values`, we obtain the desired statistic: +By performing a left join, we should, in principle, have as many rows as in the left dataset: ::: - ```{python} #| echo: true -( - communes - .groupby('DEP') - .agg({'COM': 'nunique'}) - .reset_index() - .sort_values('COM', ascending = False) -) +left_merged.shape[0] == emissions.shape[0] ``` ::: {.content-visible when-profile="fr"} - -## 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 : - +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: ::: - ::: {.content-visible when-profile="en"} -## Illustration 2: aggregates by group - -To illustrate aggregates by group, we can use the Insee `filosofi` dataset and count the population using the variable `NBPERSMENFISC16`. - -To calculate the total for the whole of France, we can do it in two ways: - +Otherwise, it indicates that there is a duplicate key on the right. Thanks to our `id_right` variable, we can identify the commune codes on the right that do not exist on the left: ::: ```{python} #| echo: true -filosofi['NBPERSMENFISC16'].sum()* 1e-6 +left_merged.loc[left_merged['id_right'].isna()].tail(3) ``` +::: {.content-visible when-profile="fr"} +Cela vient du fait que les deux jeux de données ne reposent pas sur la même version du code officiel géographique. Les données Filosofi, diffusées par l'API Melodi, reflètent le découpage communal le plus récent (2025), alors que les données d'émissions de l'Ademe, bien que mises à jour en 2025, n'ont pas encore répercuté certaines fusions de communes récentes. Par exemple, la commune de Courcouronnes est toujours présente telle quelle dans les données d'émissions (base de gauche): +::: +::: {.content-visible when-profile="en"} +This is because the two datasets do not rely on the same version of the official geographical code. The Filosofi data, disseminated via the Melodi API, reflects the most recent municipal boundaries (2025), whereas the Ademe emissions data, although updated in 2025, has not yet caught up with some recent commune mergers. For example, the commune of Courcouronnes is still present as such in the emissions data (left base): +::: + ```{python} #| echo: true -filosofi.agg({"NBPERSMENFISC16": "sum"}).div(1e6) +left_merged.loc[ + left_merged['id_right'].isna() + & left_merged['Commune'].str.contains("COURCOURONNES", na=False) +] ``` ::: {.content-visible when-profile="fr"} -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: +Cette commune a pourtant fusionné avec Evry pour former la commune nouvelle d'Évry-Courcouronnes, seule forme sous laquelle elle apparaît désormais dans les données Filosofi (base de droite): +::: +::: {.content-visible when-profile="en"} +Yet this commune has since merged with Evry to form the new commune of Évry-Courcouronnes, the only form under which it now appears in the Filosofi data (right base): +::: ```{python} #| echo: true -filosofi.groupby('dep')['NBPERSMENFISC16'].sum() #<1> -``` -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 +filosofi.loc[ + filosofi['LIBGEO'] + .str.lower() + .str.contains("courcouronnes", na=False) +] +``` + +::: {.content-visible when-profile="fr"} +Dans un exercice de construction de statistiques publiques, on ne pourrait donc se permettre cette disjonction des années. ::: +::: {.content-visible when-profile="en"} +In a public statistics construction exercise, we could not afford this discrepancy in years. +::: + + +### _Right join_ + + +![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/right_join.png) +::: {.content-visible when-profile="fr"} +Le principe est le même mais cette fois c'est la base de droite qui est prise sous forme de pivot: +::: ::: {.content-visible when-profile="en"} -where the results are reported in millions of people. The logic is the same when doing group statistics, it's just a matter of replacing `filosofi` with `filosofi.groupby('dep')` to create a partitioned version of our dataset by department: +The principle is the same, but this time it is the right base that is taken as the pivot: +::: + ```{python} #| echo: true -filosofi.groupby('dep')['NBPERSMENFISC16'].sum() #<1> +right_merged = emissions.merge( + filosofi, + left_on = ["INSEE commune", "dep"], + right_on = ["CODGEO", "dep"], + how = "right" +) +right_merged.head(3) ``` -1. With this approach, you need to pay attention to the order of operations: first, perform the `groupby` and then select the column of interest + +::: {.content-visible when-profile="fr"} +L'instruction équivalente en SQL serait +::: +::: {.content-visible when-profile="en"} +The equivalent instruction in SQL would be: ::: +```sql +SELECT * +FROM filosofi +RIGHT JOIN emissions + ON filosofi.CODGEO = emissions.`INSEE commune` + AND filosofi.dep = emissions.dep; +``` + +::: {.content-visible when-profile="fr"} +On peut, comme précédemment, vérifier la cohérence des dimensions: +::: +::: {.content-visible when-profile="en"} +We can, as before, check the consistency of the dimensions: +::: ```{python} #| echo: true -filosofi.groupby('dep').agg({"NBPERSMENFISC16": "sum"}) +right_merged.shape[0] == filosofi.shape[0] ``` ::: {.content-visible when-profile="fr"} -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. +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 ::: - ::: {.content-visible when-profile="en"} -The second approach is more practical because it directly gives a `Pandas` `DataFrame` and not an unnamed indexed series. From this, a few basic manipulations can suffice to have a shareable table on departmental demographics. However, this table would be somewhat rudimentary as we currently only have the department numbers. To get the names of the departments, we would need to use a second dataset and merge the common information between them (in this case, the department number). This is the subject of the next part. +To check the number of rows in the Filosofi data that we do not have in our greenhouse gas emissions dataset, we can do: ::: - -## Exercice d'application +```{python} +#| echo: true +right_merged['id_left'].isna().sum() +``` ::: {.content-visible when-profile="fr"} -Cet exercice d'application s'appuie sur le jeu de données de l'Ademe nommé `emissions` précédemment. +C'est un nombre faible. Quelles sont ces observations ? ::: - ::: {.content-visible when-profile="en"} -This application exercise uses the `Ademe` dataset named `emissions` previously discussed. +It's a small number. What are these observations? ::: -{{< include "02_pandas_suite/_exo1.qmd" >}} -{{< include "02_pandas_suite/_exo1_solution.qmd" >}} +```{python} +#| echo: true +right_merged.loc[ + right_merged['id_left'].isna(), + filosofi.columns.tolist() + emissions.columns.tolist() +] +``` ::: {.content-visible when-profile="fr"} -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. +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: ::: - ::: {.content-visible when-profile="en"} -These results are quite logical; rural departments have a larger share of their emissions from agriculture, while urban departments have higher emissions from the tertiary sector, which is related to the higher density of these areas. - -With these statistics, we progress in understanding our dataset and, consequently, the nature of CO2 emissions in France. Descriptive statistics by group help us better grasp the spatial heterogeneity of our phenomenon. +It is surprising to see that Paris, Lyon, and Marseille are present in the communal statistics dataset but not in the emissions dataset. To understand why, let's search in our emissions data for observations related to Marseille: +::: -However, we remain limited in our ability to interpret the obtained statistics without using additional information. To give meaning and value to a statistic, it is generally necessary to associate it with additional knowledge; otherwise, it remains detached. +```{python} +#| echo: true +emissions.loc[ + emissions["Commune"] + .str.upper() + .str.contains('MARSEILLE-') +] +``` -In the rest of this chapter, we will consider a primary approach which is the merging of complementary data. This process is called data enrichment. These data can be observations at the same level as the original source. For example, one of the most common merges is associating a customer database with a purchase database to relate purchasing behavior to characteristics that may explain it. Data merges can also occur at different conceptual levels, generally at a more aggregated level to contextualize finer data and compare an observation to measures within a similar group. For instance, we can associate individual travel times and modes with those of the same age group or people living in the same municipality to compare the differences between certain individuals and a similar sociodemographic group. +::: {.content-visible when-profile="fr"} +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. +::: +::: {.content-visible when-profile="en"} +This is because the Ademe emissions dataset provides information on districts in the three largest cities, whereas the Insee dataset does not have this breakdown. ::: -::: {.content-visible when-profile="fr"} -# Restructurer les données -## 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 : +### _Inner join_ -* 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 +![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/inner.png) -Un exemple de la distinction entre les deux peut être pris à l'ouvrage de référence d'Hadley Wickham, [*R for Data Science*](https://r4ds.hadley.nz/): +::: {.content-visible when-profile="fr"} +Il s'agit du jeu de données où les clés sont retrouvées à l'intersection des deux tables. +::: +::: {.content-visible when-profile="en"} +This is the dataset where the keys are found at the intersection of the two tables. +::: -![Données _long_ et _wide_ (Source: [*R for Data Science*](https://r4ds.hadley.nz/))](https://d33wubrfki0l68.cloudfront.net/3aea19108d39606bbe49981acda07696c0c7fcd8/2de65/images/tidy-9.png) -L'aide mémoire suivante aidera à se rappeler les fonctions à appliquer si besoin : +```{python} +#| echo: true +inner_merged = emissions.merge( + filosofi, + left_on = ["INSEE commune", "dep"], + right_on = ["CODGEO", "dep"], + how = "inner" +) +inner_merged.head(3) +``` -![](https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/reshape.png){fig-width=60%} +::: {.content-visible when-profile="fr"} +En SQL, cela donne +::: +::: {.content-visible when-profile="en"} +In SQL, this would be: +::: -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. +```sql +SELECT * +FROM emissions +INNER JOIN filosofi + ON emissions.`INSEE commune` = filosofi.CODGEO + AND emissions.dep = filosofi.dep; +``` -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. +::: {.content-visible when-profile="fr"} +Le nombre de lignes dans notre jeu de données peut être comparé au jeu de droite et de gauche: ::: - ::: {.content-visible when-profile="en"} -# Restructuring datasets +The number of rows in our dataset can be compared to the left and right datasets: +::: -## Principle +```{python} +#| echo: true +inner_merged.shape[0] == ( + left_merged.shape[0] - left_merged['id_right'].isna().sum() +) +``` -When we have multiple pieces of information for the same individual or group, we generally find two types of data structures: +```{python} +#| echo: true +inner_merged.shape[0] == ( + right_merged.shape[0] - right_merged['id_left'].isna().sum() +) +``` -* __Wide__ format: the data contains repeated observations for the same individual (or group) in different columns. -* __Long__ format: the data contains repeated observations for the same individual in different rows, with a column distinguishing the observation levels. -An example of the distinction between the two can be taken from Hadley Wickham's reference book, [*R for Data Science*](https://r4ds.hadley.nz/): +### _Full join_ -![Wide and Long Data Formats (Source: [*R for Data Science*](https://r4ds.hadley.nz/))](https://d33wubrfki0l68.cloudfront.net/3aea19108d39606bbe49981acda07696c0c7fcd8/2de65/images/tidy-9.png) +::: {.content-visible when-profile="fr"} +Le _full join_ est un pivot à gauche puis à droite pour les informations qui n'ont pas été trouvées +::: +::: {.content-visible when-profile="en"} +The full join is a pivot to the left and then to the right for the information that was not found. +::: -The following cheat sheet will help remember the functions to apply if needed: -![](https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/reshape.png){fig-width=60%} +![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/full_join.png) -Switching from a *wide* format to a *long* format (or vice versa) can be extremely practical because certain functions are more suitable for one form of data than the other. +```{python} +#| echo: true +full_merged = emissions.merge( + filosofi, + left_on = ["INSEE commune", "dep"], + right_on = ["CODGEO", "dep"], + how = "outer" +) +full_merged.head(3) +``` -Generally, with `Python` as with `R`, __long formats are often preferable__. Wide formats are rather designed for spreadsheets like `Excel`, where we have a limited number of rows to create pivot tables from. +::: {.content-visible when-profile="fr"} +Comme d'habitude, la traduction en SQL est presque immédiate: ::: +::: {.content-visible when-profile="en"} +As usual, the translation to SQL is almost immediate: +::: + +```sql +SELECT * +FROM emissions +FULL OUTER JOIN filosofi + ON emissions.`INSEE commune` = filosofi.CODGEO + AND emissions.dep = filosofi.dep; +``` ::: {.content-visible when-profile="fr"} -## Exercice d'application +Cette fois, on a une combinaison de nos trois jeux de données initiaux: -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 +* Le _inner join_ ; +* Le _left join_ sur les observations sans clé de droite ; +* Le _right join_ sur les observations sans clé de gauche ; ::: ::: {.content-visible when-profile="en"} -## Application +This time, we have a combination of our three initial datasets: -The ADEME data, and the Insee data as well, are in the _wide_ format. The next exercise illustrates the benefit of converting from _long_ to _wide_ before creating a plot with the `plot` method seen in the previous chapter. +* The inner join; +* The left join on observations without the right key; +* The right join on observations without the left key; ::: -{{< include "02_pandas_suite/_exo2.qmd" >}} -{{< include "02_pandas_suite/_exo2_solution.qmd" >}} +```{python} +#| echo: true +( + full_merged['id_left'].isna().sum() + full_merged['id_right'].isna().sum() +) == ( + left_merged['id_right'].isna().sum() + right_merged['id_left'].isna().sum() +) +``` ::: {.content-visible when-profile="fr"} +Les colonnes `id_left` et `id_right` n'avaient d'utilité que pour illustrer les jointures ci-dessus. On les retire de nos jeux de données avant de poursuivre, pour ne pas polluer les traitements ultérieurs de ce chapitre: +::: -# Joindre des données +::: {.content-visible when-profile="en"} +The `id_left` and `id_right` columns were only useful to illustrate the joins above. We drop them from our datasets before moving on, so they do not interfere with later processing in this chapter: +::: -## Principe +```{python} +#| echo: true +emissions = emissions.drop(columns = ['id_left']) +filosofi = filosofi.drop(columns = ['id_right']) +``` -Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation -où une information permet d'apparier de manière exacte deux bases de données[^flou]. -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. +::: {.content-visible when-profile="fr"} +### En résumé +::: -Cette pratique découle du fait que de nombreux systèmes d'information prennent la forme d'un schéma en étoile: +::: {.content-visible when-profile="en"} +### In summary +::: -![Illustration du schéma en étoile (Source: [Databricks](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png))](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png){fig-width="80%"} +![](https://external-preview.redd.it/yOLzCR0qSzul2WpjQorxINB0xpU3_N9twmFVsgbGJwQ.jpg?auto=webp&s=4feedc91302ba635b3028a21b98d047def5cdc2b){fig-align="center"} -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. +::: {.content-visible when-profile="fr"} +## Exemples d'identifiants dans les données françaises -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. +### 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. -On parle généralement de base de gauche et de droite pour illustrer les jointures: +Pour cela, l'identifiant de référence est le code Insee, issu du [Code officiel géographique (COG)](https://www.insee.fr/fr/information/2560452) 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. -![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/join_initial.png){fig-width="50%"} +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: ::: ::: {.content-visible when-profile="en"} -# Joining data - -## Principle - -Here we will focus on the most favorable case, which is the situation where information allows for an exact match between two databases[^fuzzy]. It is a daily necessity for data scientists to merge information present in multiple files. For example, in business databases, customer information (address, age, etc.) will be in one file, sales in another, and product characteristics in a third file. To have a complete base that compares all this information, it will be necessary to join these three files based on common information. - -This practice stems from the fact that many information systems take the form of a star schema: +## Examples of identifiers in French data -![Illustration of the star schema (Source: [Databricks](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png))](https://www.databricks.com/wp-content/uploads/2022/04/star-schema-erd.png){fig-width="80%"} +### The Official Geographic Code (COG): The identifier for geographic data -This structuring of information is closely related to the model of relational tables from the 1980s. Today, there are more flexible data models where information is stacked in a data lake without an a priori structure. Nevertheless, this star schema model retains relevance because it allows sharing information only with those who need it, leaving it to those who need to link data to do so. +For geographic data, there are many identifiers depending on the study problem. +Among the main needs is the ability to match geographic data using a common administrative identifier. For example, associating two datasets at the municipal level. -Since the logic of the star schema historically comes from relational databases, it is natural that it is an approach intrinsically linked to the philosophy of SQL, even in the vocabulary. The term "data join" is often used, inherited from the SQL `JOIN` term, and the way to describe joins (left join, right join...) comes directly from the associated SQL instructions. +For this, the reference identifier is the Insee code, derived from the [Official Geographic Code (COG)](https://www.insee.fr/fr/information/2560452), which we have been using since the last chapter and will extensively use throughout the different chapters of this course. +Given that the administrative geography is constantly evolving, the Insee code database is a living base. The Insee website and APIs provide access to the post-war historical data for long-term geographic analysis. -We generally speak of left and right bases to illustrate the joins: +Postal codes cannot be considered as an identifier: they can group several municipalities or, conversely, one municipality can have several postal codes. It is a system managed by La Poste that was not designed for statistical analysis. -![Joins](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/join_initial.png){fig-width="50%"} +To illustrate the problem, from the data provided by La Poste, we can see that postal code 11420 corresponds to 11 municipalities: ::: -::: {.content-visible when-profile="fr"} -## Mise en oeuvre avec `Pandas` +```{python} +#| echo: true +codes_postaux = pd.read_csv( + "https://datanova.laposte.fr/data-fair/api/v1/datasets/laposte-hexasmal/raw", + sep = ";", encoding = "latin1", + dtype = {"Code_postal": "str", "#Code_commune_INSEE": "str"} +) +codes_postaux.loc[codes_postaux['Code_postal'] == "11420"] +``` -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. +::: {.content-visible when-profile="fr"} +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: -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`: +```{python} +#| echo: true +!pip install cartiflette --quiet +``` -::: +```{python} +#| echo: true +#| fig-cap: Géographie des codes postaux et des communes dans l'Aude (11) +from cartiflette import carti_download +shp_communes = carti_download( + values = ["11"], + crs = 4326, + borders = "COMMUNE", + simplification=50, + filter_by="DEPARTEMENT", + source="EXPRESS-COG-CARTO-TERRITOIRE", + year=2022) #<1> -::: {.content-visible when-profile="en"} -## Implementation with `Pandas` +codes_postaux11 = shp_communes.merge( + codes_postaux, + left_on = "INSEE_COM", + right_on = "#Code_commune_INSEE" +) #<2> +codes_postaux11 = codes_postaux11.dissolve(by = "Code_postal") #<3> -In `Pandas`, the most practical method to join datasets based on common characteristics is `merge`. Its main arguments allow for controlling the join behavior. We will explore them visually. +# Carte #<4> +ax = shp_communes.plot(color='white', edgecolor='blue', linewidth = 0.5) +ax = codes_postaux11.plot(ax = ax, color='none', edgecolor='black') +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`](https://github.com/InseeFrLab/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 -In our case, for constructing statistics on carbon emissions, the left base will be the `emissions` DataFrame, and the right base will be the `filosofi` DataFrame: ::: +::: {.content-visible when-profile="en"} +Anticipating on the skills developed in the upcoming chapters, we can represent the problem cartographically by taking the example of the Aude department. The code to produce the map of commune codes is given as is, not developed, as it uses concepts and libraries that will be presented in the next chapter: ```{python} #| echo: true -emissions.head(2) -``` +#| fig-cap: Geography of postal codes and municipalities in Aude (11) +from cartiflette import carti_download +shp_communes = carti_download( + values = ["11"], + crs = 4326, + borders = "COMMUNE", + simplification=50, + filter_by="DEPARTEMENT", + source="EXPRESS-COG-CARTO-TERRITOIRE", + year=2022) #<1> -```{python} -#| echo: true -filosofi.head(2) +codes_postaux11 = shp_communes.merge( + codes_postaux, + left_on = "INSEE_COM", + right_on = "#Code_commune_INSEE" +) #<2> +codes_postaux11 = codes_postaux11.dissolve(by = "Code_postal") #<3> + +# Map #<4> +ax = shp_communes.plot(color='white', edgecolor='blue', linewidth = 0.5) +ax = codes_postaux11.plot(ax = ax, color='none', edgecolor='black') +ax.set_axis_off() ``` +1. Downloading the official contours of Aude produced by IGN using the [`cartiflette`](https://github.com/InseeFrLab/cartiflette) library +2. Joining using the commune code between the two data sources +3. Aggregating the geometry at the postal code level +4. Creating a map from our two layers +::: ::: {.content-visible when-profile="fr"} -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. +### Sirene: l'identifiant dans les données d'entreprises -On peut jouer sur deux dimensions dans la jointure (ceci sera plus clair ensuite avec les exemples graphiques). +Pour relier les microdonnées d'entreprises françaises, il existe un numéro unique d'identification : le [numéro `Siren`](https://entreprendre.service-public.fr/vosdroits/F32135). 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`](https://www.economie.gouv.fr/cedef/numero-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/](https://annuaire-entreprises.data.gouv.fr/) pour une recherche ponctuelle, sur [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/). -* 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. +Cette base Sirene est une mine d'information, parfois comique, sur les entreprises françaises. Par exemple, le site [tif.hair/](https://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](https://entreprendre.service-public.fr/vosdroits/F33050)) 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)](https://www.insee.fr/fr/information/2406147) 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`](https://www.insee.fr/fr/metadonnees/nafr2/sousClasse/96.02A?champRecherche=false). 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). -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. +L'exercice suivant, optionnel, propose de s'amuser à reproduire de manière simplifiée le recensement fait par [tif.hair/](https://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](/content/manipulation/04b_regex_TP.qmd). -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. +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](/content/modern-ds/s3.qmd) 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é"_: ::: ::: {.content-visible when-profile="en"} -We refer to join keys as the variable(s) necessary for merging data. These are the variables common to both datasets. They do not need to have the same name, but they must share common values; otherwise, the intersection between these two datasets is the empty set. -We can manipulate two dimensions in the join (this will be clearer later with graphical examples): +### Sirene: the identifier in business data -* There are mainly three types of merges: left join, right join, or a combination of the two, depending on the type of pivot we want to implement. -* Then, there are two ways to merge the values once we have chosen a pivot: inner or outer join. In the first case, we only keep the observations where the join keys are present in both datasets; in the second, we keep all observations of the pivot key variables, even if the second dataset does not have such observations, resulting in missing values. +To connect French business microdata, there is a unique identification number: the [Siren number](https://entreprendre.service-public.fr/vosdroits/F32135). It is an identification number in a legal business directory essential for all legal, fiscal, and other procedures. For companies that have multiple establishments—for example, in several cities—there is a derived identifier called the [Siret](https://www.economie.gouv.fr/cedef/numero-siret): the 9 digits of the Siren number are followed by 5 establishment identification digits. Moreover, public administrations are also concerned with the Siren number: being involved in market operations (purchasing equipment, renting goods, etc.), they also have a Siren identifier. As they are registered in legal directories whose information is public, the Siren numbers and the associated company names are available in open data, for example, on [annuaire-entreprises.data.gouv.fr/](https://annuaire-entreprises.data.gouv.fr/) for occasional searches, or on [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/). -In the examples below, we will use the commune codes and departments as join keys. Using the department is not necessary since it is directly deduced from the commune code, but it helps illustrate the principle of joins on multiple variables. Note that the name of the commune is intentionally set aside for joins, even though it is common information to both datasets. However, as it is a textual field, which may follow different formatting norms in the two datasets, it is not reliable for an exact join. +This Sirene database is a treasure trove of information, sometimes amusing, about French companies. For example, the site [tif.hair/](https://tif.hair/) cataloged the proportion of hair salons with puns in their names. When an entrepreneur declares the creation of a business, they receive a Siren number and an activity code (the [APE code](https://entreprendre.service-public.fr/vosdroits/F33050)) related to the description of their business activity. This code allows the classification of a business activity in the [French Classification of Activities (NAF)](https://www.insee.fr/fr/information/2406147), which will be used by Insee for the publication of sectoral statistics. In the case of hairdressers, the code in the NAF is [96.02A](https://www.insee.fr/fr/metadonnees/nafr2/sousClasse/96.02A?champRecherche=false). From the open data available, it is possible, in a few lines of `Python`, to get the list of all hairdressers and then explore this data (the subject of the next optional exercise). -To illustrate the principle of the left or right pivot, we will create two identifier variables for the row in our left and right datasets. This will allow us to easily find rows present in one dataset but not in the other. -::: +The following optional exercise suggests replicating, in a simplified manner, the survey done by [tif.hair/](https://tif.hair/) on puns in hair salon names. It allows practicing some text manipulation methods, ahead of the chapter dedicated to [regular expressions](/content/manipulation/04b_regex_TP.qmd). -```{python} -#| echo: true -emissions = emissions.reset_index(names = ['id_left']) -filosofi = filosofi.reset_index(names = ['id_right']) -``` +Since the dataset of all companies is quite large (around 4GB in CSV after decompression), it is more practical to use a dataset in `Parquet` format, which is more optimized (more details on this format in the [advanced chapter](/content/modern-ds/s3.qmd) dedicated to it). -### _Left join_ +To read this type of file optimally, it is recommended to use the `DuckDB` library, which allows consuming only the necessary data instead of downloading the entire file to read only a part of it as would be the case with `Pandas` (see the end of this chapter, section "Beyond `Pandas`"). The following SQL query translates into natural language as: _"From the `Parquet` file, I only want a few columns of the file for hairdressers (APE: 96.02A) whose business name (`denominationUsuelleEtablissement`) is provided"_: -::: {.content-visible when-profile="fr"} -Commençons avec la jointure à gauche. Comme son nom l'indique, on va prendre la variable de gauche en pivot: -::: -::: {.content-visible when-profile="en"} -Let's start with the left join. As its name indicates, we will take the left variable as the pivot: ::: -![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/left_join.png) +{{< include "02_pandas_suite/_exo_optionnel.qmd" >}} + +{{< include "02_pandas_suite/_exo_optionnel_solution.qmd" >}} ```{python} -#| echo: true -left_merged = emissions.merge( - filosofi, - left_on = ["INSEE commune", "dep"], - right_on = ["CODGEO", "dep"], - how = "left" -) -left_merged.head(3) +#| echo: false +#| output: false +ojs_define(coiffeurs = coiffeurs_tif) ``` -::: {.content-visible when-profile="fr"} -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: -::: - -::: {.content-visible when-profile="en"} -It is recommended to always explicitly specify the join keys using the `left_on`, `right_on`, or `on` arguments if the variable names are common between the two datasets. -If there are common variable names between the datasets that are not defined as join keys, they will not be used for the join but will be retained with a suffix that defaults to `_x` and `_y` (configurable using the `suffixes` argument). +```{ojs} +function underlineTif(x) { + // Use a regular expression to find all occurrences of "tif" + const modx = x.replace(/TIF/g, match => + `${match}` + ); -The `Pandas` syntax is directly inspired by SQL, so we have a fairly transparent translation of the above instruction into SQL: -::: + console.log(modx) -```sql -SELECT * -FROM emissions -LEFT JOIN filosofi - ON emissions.`INSEE commune` = filosofi.CODGEO - AND emissions.dep = filosofi.dep; + // Return the modified string wrapped in an HTML template + return html`${modx}`; +} ``` ::: {.content-visible when-profile="fr"} -En faisant une jointure à gauche, on doit en principe avoir autant de lignes que la base de données à gauche: -::: -::: {.content-visible when-profile="en"} -By performing a left join, we should, in principle, have as many rows as in the left dataset: +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: + ::: -```{python} -#| echo: true -left_merged.shape[0] == emissions.shape[0] -``` -::: {.content-visible when-profile="fr"} -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: -::: ::: {.content-visible when-profile="en"} -Otherwise, it indicates that there is a duplicate key on the right. Thanks to our `id_right` variable, we can identify the commune codes on the right that do not exist on the left: + +In a more interactive form, here's a list of all the hairdressers who have the word `tif` in the name of their registered business in the official data: + ::: +::: {.content-visible when-format="html"} -```{python} -#| echo: true -left_merged.loc[left_merged['id_right'].isna()].tail(3) +```{ojs} +viewof coiffeursSearch = Inputs.search( + transpose(coiffeurs), + {label: "Nom du salon"} +) ``` -::: {.content-visible when-profile="fr"} -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): -::: -::: {.content-visible when-profile="en"} -This is because we are using data that are not from the same reference year of the official geographical code (2016 vs 2018). During this interval, there were geographical changes, notably commune mergers. For example, the commune of Courcouronnes seen above can be found merged with Evry in the filosofi dataset (right base): -::: -```{python} -#| echo: true -filosofi.loc[ - filosofi['LIBGEO'] - .str.lower() - .str.contains("courcouronnes") -] +```{ojs} +Inputs.table(coiffeursSearch, { + columns: [ + "siren", + "denominationUsuelleEtablissement", + "enseigne1Etablissement" + ], + format: { + denominationUsuelleEtablissement: x => underlineTif(x), + enseigne1Etablissement: x => underlineTif(x) + } +}) ``` -::: {.content-visible when-profile="fr"} -Dans un exercice de construction de statistiques publiques, on ne pourrait donc se permettre cette disjonction des années. -::: -::: {.content-visible when-profile="en"} -In a public statistics construction exercise, we could not afford this discrepancy in years. ::: +::: {.content-visible when-profile="fr"} -### _Right join_ +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. +::: -![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/right_join.png) +::: {.content-visible when-profile="en"} -::: {.content-visible when-profile="fr"} -Le principe est le même mais cette fois c'est la base de droite qui est prise sous forme de pivot: -::: -::: {.content-visible when-profile="en"} -The principle is the same, but this time it is the right base that is taken as the pivot: -::: - - -```{python} -#| echo: true -right_merged = emissions.merge( - filosofi, - left_on = ["INSEE commune", "dep"], - right_on = ["CODGEO", "dep"], - how = "right" -) -right_merged.head(3) -``` +Of course, to go further, it would be better to normalize the data more thoroughly, check that the information sought is not spread across multiple columns, and conduct visual inspections to detect hidden puns. But already, in just a few minutes, we have partial statistics on the phenomenon of punny hairdressers. -::: {.content-visible when-profile="fr"} -L'instruction équivalente en SQL serait -::: -::: {.content-visible when-profile="en"} -The equivalent instruction in SQL would be: ::: +::: {.content-visible when-profile="fr"} -```sql -SELECT * -FROM filosofi -RIGHT JOIN emissions - ON filosofi.CODGEO = emissions.`INSEE commune` - AND filosofi.dep = emissions.dep; -``` +### Le NIR et la question de la confidentialité des identifiants individuels -::: {.content-visible when-profile="fr"} -On peut, comme précédemment, vérifier la cohérence des dimensions: -::: -::: {.content-visible when-profile="en"} -We can, as before, check the consistency of the dimensions: -::: +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](https://www.cnil.fr/fr/definition/nir-numero-dinscription-au-repertoire), 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)](https://www.insee.fr/fr/metadonnees/definition/c1602). -```{python} -#| echo: true -right_merged.shape[0] == filosofi.shape[0] -``` +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. -::: {.content-visible when-profile="fr"} -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 -::: -::: {.content-visible when-profile="en"} -To check the number of rows in the Filosofi data that we do not have in our greenhouse gas emissions dataset, we can do: -::: +![Le numéro de sécurité sociale (Source: [Améli](https://www.ameli.fr/assure/droits-demarches/principes/numero-securite-sociale))](https://www.ameli.fr/sites/default/files/styles/webp_ckeditor/public/thumbnails/image/infographie_assures-regle-identification-assures.gif.webp?itok=j2owVDrB){fig-width="80%"} -```{python} -#| echo: true -right_merged['id_left'].isna().sum() -``` +Pour pallier ce problème, a récémment été mis en oeuvre le [code statistique non signifiant (CSNS)](https://www.insee.fr/fr/information/7635825?sommaire=7635842) 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. -::: {.content-visible when-profile="fr"} -C'est un nombre faible. Quelles sont ces observations ? -::: -::: {.content-visible when-profile="en"} -It's a small number. What are these observations? -::: -```{python} -#| echo: true -right_merged.loc[ - right_merged['id_left'].isna(), - filosofi.columns.tolist() + emissions.columns.tolist() -] -``` +[^flou]: Autrement, on rentre dans le monde des appariements flous ou des appariements probabilistes. Les appariements flous sont des situations où on ne dispose plus d'un identifiant exact pour associer deux bases mais d'une information partiellement bruitée entre deux sources pour faire cette mise en relation. Par exemple, dans une base de données produit on aura `Coca Cola 33CL` et dans une autre `Coca Cola canette` mais sous ces deux noms sont cachés le même produit. Le chapitre d'[ouverture aux enjeux de recherche textuelle avec `ElasticSearch`](/content/modern-ds/elastic.qmd) est consacré à cette problématique. Les appariements probabilistes sont un autre type d'approche. Dans ceux-ci, on associe des observations dans deux bases non pas sur la base d'un identifiant mais sur la distance entre un ensemble de caractéristiques dans les deux bases. Cette technique est très utilisée dans les statistiques médicales ou dans l'évaluation de politiques publiques sur la base du [_propensity score matching_](https://en.wikipedia.org/wiki/Propensity_score_matching). -::: {.content-visible when-profile="fr"} -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: ::: + ::: {.content-visible when-profile="en"} -It is surprising to see that Paris, Lyon, and Marseille are present in the communal statistics dataset but not in the emissions dataset. To understand why, let's search in our emissions data for observations related to Marseille: -::: -```{python} -#| echo: true -emissions.loc[ - emissions["Commune"] - .str.upper() - .str.contains('MARSEILLE-') -] -``` +### The social security number and the issue of individual identifiers' confidentiality -::: {.content-visible when-profile="fr"} -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. -::: -::: {.content-visible when-profile="en"} -This is because the Ademe emissions dataset provides information on districts in the three largest cities, whereas the Insee dataset does not have this breakdown. -::: +For individuals, there exists a unique identifier that allows linking them across different data sources: the [NIR](https://www.cnil.fr/fr/definition/nir-numero-dinscription-au-repertoire), also known as the INSEE number or social security number. +This number is necessary for the administration to manage social benefits (health, retirement, family...). Beyond this function, which can be useful daily, this number is a unique individual identifier in the [National Register of Physical Persons (RNIPP)](https://www.insee.fr/fr/metadonnees/definition/c1602). +This identifier is mainly present in management databases related to payroll, social benefits, etc. However, unlike the Sirene number, it contains several sensitive pieces of information and is inherently linked to the sensitive issue of social security rights. +![Social security number (Source: [Améli](https://www.ameli.fr/assure/droits-demarches/principes/numero-securite-sociale))](https://www.ameli.fr/sites/default/files/styles/webp_ckeditor/public/thumbnails/image/infographie_assures-regle-identification-assures.gif.webp?itok=j2owVDrB){fig-width="80%"} -### _Inner join_ +To address this problem, the [non-significant statistical code (CSNS)](https://www.insee.fr/fr/information/7635825?sommaire=7635842) or hashed NIR, a non-identifying anonymous individual identifier, was recently implemented. The goal of this anonymized identifier is to reduce the dissemination of personal information that, although allowing civil servants and researchers to deterministically link numerous databases, provided analysts with non-essential information about the individuals in question. -![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/inner.png) +[^flou]: 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 another `Coca Cola canette`, but these names hide the same product. The chapter on [Introduction to Textual Search with ElasticSearch](/content/modern-ds/elastic.qmd) 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_](https://en.wikipedia.org/wiki/Propensity_score_matching). -::: {.content-visible when-profile="fr"} -Il s'agit du jeu de données où les clés sont retrouvées à l'intersection des deux tables. -::: -::: {.content-visible when-profile="en"} -This is the dataset where the keys are found at the intersection of the two tables. ::: +::: {.content-visible when-profile="fr"} -```{python} -#| echo: true -inner_merged = emissions.merge( - filosofi, - left_on = ["INSEE commune", "dep"], - right_on = ["CODGEO", "dep"], - how = "inner" -) -inner_merged.head(3) -``` +## Exercices d'application -::: {.content-visible when-profile="fr"} -En SQL, cela donne -::: -::: {.content-visible when-profile="en"} -In SQL, this would be: -::: +### Pourquoi a-t-on besoin d'un code commune quand on a déjà son nom ? -```sql -SELECT * -FROM emissions -INNER JOIN filosofi - ON emissions.`INSEE commune` = filosofi.CODGEO - AND emissions.dep = filosofi.dep; -``` +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. -::: {.content-visible when-profile="fr"} -Le nombre de lignes dans notre jeu de données peut être comparé au jeu de droite et de gauche: ::: + ::: {.content-visible when-profile="en"} -The number of rows in our dataset can be compared to the left and right datasets: -::: -```{python} -#| echo: true -inner_merged.shape[0] == ( - left_merged.shape[0] - left_merged['id_right'].isna().sum() -) -``` +### Why do we need a commune code when we already have its name? -```{python} -#| echo: true -inner_merged.shape[0] == ( - right_merged.shape[0] - right_merged['id_left'].isna().sum() -) -``` +This exercise will take a step back to understand why we assumed above that the commune code was the key for joining data. +::: -### _Full join_ +{{< include "02_pandas_suite/_exo3.qmd" >}} +{{< include "02_pandas_suite/_exo3_solution.qmd" >}} ::: {.content-visible when-profile="fr"} -Le _full join_ est un pivot à gauche puis à droite pour les informations qui n'ont pas été trouvées -::: -::: {.content-visible when-profile="en"} -The full join is a pivot to the left and then to the right for the information that was not found. -::: - -![](https://minio.lab.sspcloud.fr/lgaliana/python-ENSAE/inputs/merge_pandas/full_join.png) - -```{python} -#| echo: true -full_merged = emissions.merge( - filosofi, - left_on = ["INSEE commune", "dep"], - right_on = ["CODGEO", "dep"], - how = "outer" -) -full_merged.head(3) -``` +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. -::: {.content-visible when-profile="fr"} -Comme d'habitude, la traduction en SQL est presque immédiate: ::: + ::: {.content-visible when-profile="en"} -As usual, the translation to SQL is almost immediate: -::: -```sql -SELECT * -FROM emissions -FULL OUTER JOIN filosofi - ON emissions.`INSEE commune` = filosofi.CODGEO - AND emissions.dep = filosofi.dep; -``` +This small exercise reassures us as the duplicated labels are actually the same commune names but in different departments. So, these are not duplicated observations. We can thus rely on the commune codes, which are unique. + +::: ::: {.content-visible when-profile="fr"} -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 ; +### Calculer une empreinte carbone grâce à l'association entre des sources + ::: ::: {.content-visible when-profile="en"} -This time, we have a combination of our three initial datasets: -* The inner join; -* The left join on observations without the right key; -* The right join on observations without the left key; -::: - -```{python} -#| echo: true -( - full_merged['id_left'].isna().sum() + full_merged['id_right'].isna().sum() -) == ( - left_merged['id_right'].isna().sum() + right_merged['id_left'].isna().sum() -) -``` +### Associating different sources to compute carbon footprints -::: {.content-visible when-profile="fr"} -### En résumé ::: -::: {.content-visible when-profile="en"} -### In summary -::: -![](https://external-preview.redd.it/yOLzCR0qSzul2WpjQorxINB0xpU3_N9twmFVsgbGJwQ.jpg?auto=webp&s=4feedc91302ba635b3028a21b98d047def5cdc2b){fig-align="center"} +{{< include "02_pandas_suite/_exo4.qmd" >}} +{{< include "02_pandas_suite/_exo4_solution.qmd" >}} + ::: {.content-visible when-profile="fr"} -## Exemples d'identifiants dans les données françaises +# Statistiques descriptives par groupe -### Le Code officiel géographique (COG): l'identifiant des données géographiques +## Principe -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. +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). -Pour cela, l'identifiant de référence est le code Insee, issu du [Code officiel géographique (COG)](https://www.insee.fr/fr/information/2560452) 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. +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](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html). -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. +L'image suivante, issue de +[ce site](https://unlhcc.github.io/r-novice-gapminder/16-plyr/), +représente bien la manière dont fonctionne l'approche +`split`-`apply`-`combine`: -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: +![Split-apply-combine (Source: [unlhcc.github.io](https://unlhcc.github.io/r-novice-gapminder/16-plyr/))](https://unlhcc.github.io/r-novice-gapminder/fig/12-plyr-fig1.png){fig-width=70%} + +En `Pandas`, on utilise `groupby` pour découper les données selon un ou +plusieurs axes (ce [tutoriel](https://realpython.com/pandas-groupby/) 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_: ::: ::: {.content-visible when-profile="en"} -## Examples of identifiers in French data +# Descriptive statistics by group -### The Official Geographic Code (COG): The identifier for geographic data +## Principle -For geographic data, there are many identifiers depending on the study problem. -Among the main needs is the ability to match geographic data using a common administrative identifier. For example, associating two datasets at the municipal level. +In the previous chapter, we saw how to obtain an aggregated statistic easily with `Pandas`. However, it is common to have data with intermediate analysis strata that are relevant: geographical variables, membership in socio-demographic groups related to recorded characteristics, temporal period indicators, etc. To better understand the structure of the data, data scientists are often led to construct descriptive statistics on sub-groups present in the data. For example, we previously constructed emission statistics at the national level. But what about the emission profiles of different departments? To answer this question, it will be useful to aggregate our data at the departmental level. This will give us different information from the initial dataset (municipal level) and the most aggregated level (national level). -For this, the reference identifier is the Insee code, derived from the [Official Geographic Code (COG)](https://www.insee.fr/fr/information/2560452), which we have been using since the last chapter and will extensively use throughout the different chapters of this course. -Given that the administrative geography is constantly evolving, the Insee code database is a living base. The Insee website and APIs provide access to the post-war historical data for long-term geographic analysis. +In `SQL`, it is very simple to segment data to perform operations on coherent blocks and recollect results in the appropriate dimension. The underlying logic is that of *split-apply-combine*, which is adopted by data manipulation languages, including `pandas` [which is no exception](https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html). -Postal codes cannot be considered as an identifier: they can group several municipalities or, conversely, one municipality can have several postal codes. It is a system managed by La Poste that was not designed for statistical analysis. +The following image, from [this site](https://unlhcc.github.io/r-novice-gapminder/16-plyr/), well represents how the `split`-`apply`-`combine` approach works: -To illustrate the problem, from the data provided by La Poste, we can see that postal code 11420 corresponds to 11 municipalities: -::: +![Split-apply-combine (Source: [unlhcc.github.io](https://unlhcc.github.io/r-novice-gapminder/16-plyr/))](https://unlhcc.github.io/r-novice-gapminder/fig/12-plyr-fig1.png){fig-width=70%} + +In `Pandas`, we use `groupby` to segment the data according to one or more axes (this [tutorial](https://realpython.com/pandas-groupby/) on the subject is particularly useful). All the aggregation operations (counting, averages, etc.) that we saw earlier can be applied by group. +Technically, this operation involves creating an association between labels (values of group variables) and observations. Using the `groupby` method does not trigger operations until a statistic is implemented; it simply creates a formal relationship between observations and groupings that will be used later: +::: ```{python} #| echo: true -codes_postaux = pd.read_csv( - "https://datanova.laposte.fr/data-fair/api/v1/datasets/laposte-hexasmal/raw", - sep = ";", encoding = "latin1", - dtype = {"Code_postal": "str", "#Code_commune_INSEE": "str"} -) -codes_postaux.loc[codes_postaux['Code_postal'] == "11420"] +filosofi.groupby('dep').__class__ ``` ::: {.content-visible when-profile="fr"} -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: +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 : +::: + +::: {.content-visible when-profile="en"} +As long as we do not call an action on a `DataFrame` by group, such as `head` or `display`, `pandas` performs no operations. This is called *lazy evaluation*. For example, the result of `df.groupby('dep')` is a transformation that has not yet been evaluated: +::: ```{python} #| echo: true -!pip install cartiflette --quiet +filosofi.groupby('dep') ``` -```{python} -#| echo: true -#| fig-cap: Géographie des codes postaux et des communes dans l'Aude (11) -from cartiflette import carti_download -shp_communes = carti_download( - values = ["11"], - crs = 4326, - borders = "COMMUNE", - simplification=50, - filter_by="DEPARTEMENT", - source="EXPRESS-COG-CARTO-TERRITOIRE", - year=2022) #<1> -codes_postaux11 = shp_communes.merge( - codes_postaux, - left_on = "INSEE_COM", - right_on = "#Code_commune_INSEE" -) #<2> -codes_postaux11 = codes_postaux11.dissolve(by = "Code_postal") #<3> +::: {.content-visible when-profile="fr"} -# Carte #<4> -ax = shp_communes.plot(color='white', edgecolor='blue', linewidth = 0.5) -ax = codes_postaux11.plot(ax = ax, color='none', edgecolor='black') -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`](https://github.com/InseeFrLab/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 +## 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`: ::: ::: {.content-visible when-profile="en"} -Anticipating on the skills developed in the upcoming chapters, we can represent the problem cartographically by taking the example of the Aude department. The code to produce the map of commune codes is given as is, not developed, as it uses concepts and libraries that will be presented in the next chapter: - -```{python} -#| echo: true -#| fig-cap: Geography of postal codes and municipalities in Aude (11) -from cartiflette import carti_download -shp_communes = carti_download( - values = ["11"], - crs = 4326, - borders = "COMMUNE", - simplification=50, - filter_by="DEPARTEMENT", - source="EXPRESS-COG-CARTO-TERRITOIRE", - year=2022) #<1> +## Illustration 1: counting by group -codes_postaux11 = shp_communes.merge( - codes_postaux, - left_on = "INSEE_COM", - right_on = "#Code_commune_INSEE" -) #<2> -codes_postaux11 = codes_postaux11.dissolve(by = "Code_postal") #<3> +To illustrate the application of this principle to counting, we can count the number of municipalities by department in 2023 (this statistic changes every year due to municipal mergers). For this, we simply take the reference of French municipalities from the official geographical code (COG) and count by department using `count`: -# Map #<4> -ax = shp_communes.plot(color='white', edgecolor='blue', linewidth = 0.5) -ax = codes_postaux11.plot(ax = ax, color='none', edgecolor='black') -ax.set_axis_off() -``` -1. Downloading the official contours of Aude produced by IGN using the [`cartiflette`](https://github.com/InseeFrLab/cartiflette) library -2. Joining using the commune code between the two data sources -3. Aggregating the geometry at the postal code level -4. Creating a map from our two layers ::: -::: {.content-visible when-profile="fr"} -### Sirene: l'identifiant dans les données d'entreprises +```{python} +#| echo: true +import requests +from io import StringIO +import pandas as pd -Pour relier les microdonnées d'entreprises françaises, il existe un numéro unique d'identification : le [numéro `Siren`](https://entreprendre.service-public.fr/vosdroits/F32135). 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`](https://www.economie.gouv.fr/cedef/numero-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/](https://annuaire-entreprises.data.gouv.fr/) pour une recherche ponctuelle, sur [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/). +url_cog_2023 = "https://www.insee.fr/fr/statistiques/fichier/6800675/v_commune_2023.csv" +url_backup = "https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/cog_2023.csv" -Cette base Sirene est une mine d'information, parfois comique, sur les entreprises françaises. Par exemple, le site [tif.hair/](https://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](https://entreprendre.service-public.fr/vosdroits/F33050)) 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)](https://www.insee.fr/fr/information/2406147) 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`](https://www.insee.fr/fr/metadonnees/nafr2/sousClasse/96.02A?champRecherche=false). 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). +# Try-except clause to avoid timout issue sometimes +# Without timeout problem, pd.read_csv(url_cog_2023) would be sufficient +try: + response = requests.get(url_cog_2023) + response.raise_for_status() + cog_2023 = pd.read_csv(StringIO(response.text)) +except requests.exceptions.Timeout: + print("Failing back to backup") + cog_2023 = pd.read_csv(url_backup) +``` -L'exercice suivant, optionnel, propose de s'amuser à reproduire de manière simplifiée le recensement fait par [tif.hair/](https://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](/content/manipulation/04b_regex_TP.qmd). -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](/content/modern-ds/s3.qmd) 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é"_: +::: {.content-visible when-profile="fr"} + +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: + +```{python} +#| echo: true +communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> +communes.loc[:, ['COM', 'DEP', 'REG']].nunique() +``` +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. ::: ::: {.content-visible when-profile="en"} -### Sirene: the identifier in business data +With this dataset, without resorting to group statistics, we can already know how many municipalities, departments, and regions we have in France, respectively: -To connect French business microdata, there is a unique identification number: the [Siren number](https://entreprendre.service-public.fr/vosdroits/F32135). It is an identification number in a legal business directory essential for all legal, fiscal, and other procedures. For companies that have multiple establishments—for example, in several cities—there is a derived identifier called the [Siret](https://www.economie.gouv.fr/cedef/numero-siret): the 9 digits of the Siren number are followed by 5 establishment identification digits. Moreover, public administrations are also concerned with the Siren number: being involved in market operations (purchasing equipment, renting goods, etc.), they also have a Siren identifier. As they are registered in legal directories whose information is public, the Siren numbers and the associated company names are available in open data, for example, on [annuaire-entreprises.data.gouv.fr/](https://annuaire-entreprises.data.gouv.fr/) for occasional searches, or on [data.gouv.fr](https://www.data.gouv.fr/fr/datasets/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/). +```{python} +#| echo: true +communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> +communes.loc[:, ['COM', 'DEP', 'REG']].nunique() +``` +1. We restrict to the status "Commune" because this file also contains Insee codes for other statuses, such as the "Municipal Arrondissements" of Paris, Lyon, and Marseille. +::: -This Sirene database is a treasure trove of information, sometimes amusing, about French companies. For example, the site [tif.hair/](https://tif.hair/) cataloged the proportion of hair salons with puns in their names. When an entrepreneur declares the creation of a business, they receive a Siren number and an activity code (the [APE code](https://entreprendre.service-public.fr/vosdroits/F33050)) related to the description of their business activity. This code allows the classification of a business activity in the [French Classification of Activities (NAF)](https://www.insee.fr/fr/information/2406147), which will be used by Insee for the publication of sectoral statistics. In the case of hairdressers, the code in the NAF is [96.02A](https://www.insee.fr/fr/metadonnees/nafr2/sousClasse/96.02A?champRecherche=false). From the open data available, it is possible, in a few lines of `Python`, to get the list of all hairdressers and then explore this data (the subject of the next optional exercise). -The following optional exercise suggests replicating, in a simplified manner, the survey done by [tif.hair/](https://tif.hair/) on puns in hair salon names. It allows practicing some text manipulation methods, ahead of the chapter dedicated to [regular expressions](/content/manipulation/04b_regex_TP.qmd). +::: {.content-visible when-profile="fr"} +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. -Since the dataset of all companies is quite large (around 4GB in CSV after decompression), it is more practical to use a dataset in `Parquet` format, which is more optimized (more details on this format in the [advanced chapter](/content/modern-ds/s3.qmd) dedicated to it). +Calculer cette statistique se fait de manière assez transparente lorsqu'on connaît le principe d'un calcul de statistiques avec `Pandas`: +::: -To read this type of file optimally, it is recommended to use the `DuckDB` library, which allows consuming only the necessary data instead of downloading the entire file to read only a part of it as would be the case with `Pandas` (see the end of this chapter, section "Beyond `Pandas`"). The following SQL query translates into natural language as: _"From the `Parquet` file, I only want a few columns of the file for hairdressers (APE: 96.02A) whose business name (`denominationUsuelleEtablissement`) is provided"_: +::: {.content-visible when-profile="en"} +Now, let's look at the departments with the most municipalities. It is the same counting function where we play, this time, on the group from which the statistic is calculated. +Calculating this statistic is quite straightforward when you understand the principle of calculating statistics with `Pandas`: ::: +```{python} +#| echo: true +communes = cog_2023.loc[cog_2023['TYPECOM']=="COM"] #<1> +communes.groupby('DEP').agg({'COM': 'nunique'}) +``` -{{< include "02_pandas_suite/_exo_optionnel.qmd" >}} - -{{< include "02_pandas_suite/_exo_optionnel_solution.qmd" >}} +::: {.content-visible when-profile="fr"} +En SQL, on utiliserait la requête suivante: +::: +::: {.content-visible when-profile="en"} +In SQL, we would use the following query: +::: -```{python} -#| echo: false -#| output: false -ojs_define(coiffeurs = coiffeurs_tif) +```sql +SELECT dep, COUNT DISTINCT "COM" AS COM +FROM communes +GROUP BY dep +WHERE TYPECOM == 'COM'; ``` -```{ojs} -function underlineTif(x) { - // Use a regular expression to find all occurrences of "tif" - const modx = x.replace(/TIF/g, match => - `${match}` - ); +::: {.content-visible when-profile="fr"} +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: +::: +::: {.content-visible when-profile="en"} +The output is an indexed `Series`. This is not very convenient as we mentioned in the previous chapter. It is more practical to transform this object into a `DataFrame` with `reset_index`. Finally, with `sort_values`, we obtain the desired statistic: +::: - console.log(modx) - // Return the modified string wrapped in an HTML template - return html`${modx}`; -} +```{python} +#| echo: true +( + communes + .groupby('DEP') + .agg({'COM': 'nunique'}) + .reset_index() + .sort_values('COM', ascending = False) +) ``` ::: {.content-visible when-profile="fr"} -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: +## 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 sommer la variable `POPULATION`. + +Pour calculer le total au niveau France entière nous pouvons faire de deux manières : +::: ::: {.content-visible when-profile="en"} +## Illustration 2: aggregates by group -In a more interactive form, here's a list of all the hairdressers who have the word `tif` in the name of their registered business in the official data: +To illustrate aggregates by group, we can use the Insee `filosofi` dataset and sum the `POPULATION` variable. + +To calculate the total for the whole of France, we can do it in two ways: ::: -::: {.content-visible when-format="html"} -```{ojs} -viewof coiffeursSearch = Inputs.search( - transpose(coiffeurs), - {label: "Nom du salon"} -) +```{python} +#| echo: true +filosofi['POPULATION'].sum()* 1e-6 ``` - -```{ojs} -Inputs.table(coiffeursSearch, { - columns: [ - "siren", - "denominationUsuelleEtablissement", - "enseigne1Etablissement" - ], - format: { - denominationUsuelleEtablissement: x => underlineTif(x), - enseigne1Etablissement: x => underlineTif(x) - } -}) +```{python} +#| echo: true +filosofi.agg({"POPULATION": "sum"}).div(1e6) ``` -::: - ::: {.content-visible when-profile="fr"} +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: -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. - +```{python} +#| echo: true +filosofi.groupby('dep')['POPULATION'].sum() #<1> +``` +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 ::: ::: {.content-visible when-profile="en"} +where the results are reported in millions of people. The logic is the same when doing group statistics, it's just a matter of replacing `filosofi` with `filosofi.groupby('dep')` to create a partitioned version of our dataset by department: -Of course, to go further, it would be better to normalize the data more thoroughly, check that the information sought is not spread across multiple columns, and conduct visual inspections to detect hidden puns. But already, in just a few minutes, we have partial statistics on the phenomenon of punny hairdressers. - +```{python} +#| echo: true +filosofi.groupby('dep')['POPULATION'].sum() #<1> +``` +1. With this approach, you need to pay attention to the order of operations: first, perform the `groupby` and then select the column of interest ::: -::: {.content-visible when-profile="fr"} - -### 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](https://www.cnil.fr/fr/definition/nir-numero-dinscription-au-repertoire), 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)](https://www.insee.fr/fr/metadonnees/definition/c1602). -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. +```{python} +#| echo: true +filosofi.groupby('dep').agg({"POPULATION": "sum"}) +``` -![Le numéro de sécurité sociale (Source: [Améli](https://www.ameli.fr/assure/droits-demarches/principes/numero-securite-sociale))](https://www.ameli.fr/sites/default/files/styles/webp_ckeditor/public/thumbnails/image/infographie_assures-regle-identification-assures.gif.webp?itok=j2owVDrB){fig-width="80%"} +::: {.content-visible when-profile="fr"} +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 le principe des jointures que nous avons mis en oeuvre un peu plus haut dans ce chapitre. +::: -Pour pallier ce problème, a récémment été mis en oeuvre le [code statistique non signifiant (CSNS)](https://www.insee.fr/fr/information/7635825?sommaire=7635842) 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. +::: {.content-visible when-profile="en"} +The second approach is more practical because it directly gives a `Pandas` `DataFrame` and not an unnamed indexed series. From this, a few basic manipulations can suffice to have a shareable table on departmental demographics. However, this table would be somewhat rudimentary as we currently only have the department numbers. To get the names of the departments, we would need to use a second dataset and merge the common information between them (in this case, the department number): this is exactly the join principle we implemented earlier in this chapter. +::: -[^flou]: Autrement, on rentre dans le monde des appariements flous ou des appariements probabilistes. Les appariements flous sont des situations où on ne dispose plus d'un identifiant exact pour associer deux bases mais d'une information partiellement bruitée entre deux sources pour faire cette mise en relation. Par exemple, dans une base de données produit on aura `Coca Cola 33CL` et dans une autre `Coca Cola canette` mais sous ces deux noms sont cachés le même produit. Le chapitre d'[ouverture aux enjeux de recherche textuelle avec `ElasticSearch`](/content/modern-ds/elastic.qmd) est consacré à cette problématique. Les appariements probabilistes sont un autre type d'approche. Dans ceux-ci, on associe des observations dans deux bases non pas sur la base d'un identifiant mais sur la distance entre un ensemble de caractéristiques dans les deux bases. Cette technique est très utilisée dans les statistiques médicales ou dans l'évaluation de politiques publiques sur la base du [_propensity score matching_](https://en.wikipedia.org/wiki/Propensity_score_matching). +## Exercice d'application +::: {.content-visible when-profile="fr"} +Cet exercice d'application s'appuie sur le jeu de données de l'Ademe nommé `emissions` précédemment. ::: ::: {.content-visible when-profile="en"} +This application exercise uses the `Ademe` dataset named `emissions` previously discussed. +::: -### The social security number and the issue of individual identifiers' confidentiality +{{< include "02_pandas_suite/_exo1.qmd" >}} +{{< include "02_pandas_suite/_exo1_solution.qmd" >}} -For individuals, there exists a unique identifier that allows linking them across different data sources: the [NIR](https://www.cnil.fr/fr/definition/nir-numero-dinscription-au-repertoire), also known as the INSEE number or social security number. -This number is necessary for the administration to manage social benefits (health, retirement, family...). Beyond this function, which can be useful daily, this number is a unique individual identifier in the [National Register of Physical Persons (RNIPP)](https://www.insee.fr/fr/metadonnees/definition/c1602). +::: {.content-visible when-profile="fr"} +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. -This identifier is mainly present in management databases related to payroll, social benefits, etc. However, unlike the Sirene number, it contains several sensitive pieces of information and is inherently linked to the sensitive issue of social security rights. +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. -![Social security number (Source: [Améli](https://www.ameli.fr/assure/droits-demarches/principes/numero-securite-sociale))](https://www.ameli.fr/sites/default/files/styles/webp_ckeditor/public/thumbnails/image/infographie_assures-regle-identification-assures.gif.webp?itok=j2owVDrB){fig-width="80%"} +Cependant, on resterait limité dans notre capacité à interpréter ces statistiques sans recourir à de l'information annexe : un département pourrait sembler peu polluant simplement parce qu'il est peu peuplé. C'est exactement le type de limite que nous avons levée un peu plus haut dans ce chapitre en enrichissant nos données grâce à une jointure avec les données Filosofi (voir notamment le calcul d'une empreinte carbone par habitant). +::: -To address this problem, the [non-significant statistical code (CSNS)](https://www.insee.fr/fr/information/7635825?sommaire=7635842) or hashed NIR, a non-identifying anonymous individual identifier, was recently implemented. The goal of this anonymized identifier is to reduce the dissemination of personal information that, although allowing civil servants and researchers to deterministically link numerous databases, provided analysts with non-essential information about the individuals in question. +::: {.content-visible when-profile="en"} +These results are quite logical; rural departments have a larger share of their emissions from agriculture, while urban departments have higher emissions from the tertiary sector, which is related to the higher density of these areas. -[^flou]: 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 another `Coca Cola canette`, but these names hide the same product. The chapter on [Introduction to Textual Search with ElasticSearch](/content/modern-ds/elastic.qmd) 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_](https://en.wikipedia.org/wiki/Propensity_score_matching). +With these statistics, we progress in understanding our dataset and, consequently, the nature of CO2 emissions in France. Descriptive statistics by group help us better grasp the spatial heterogeneity of our phenomenon. +However, we would remain limited in our ability to interpret these statistics without additional information: a département could look like a low emitter simply because it is sparsely populated. This is exactly the kind of limitation we resolved earlier in this chapter by enriching our data with a join against the Filosofi data (see in particular the computation of a carbon footprint per capita). ::: ::: {.content-visible when-profile="fr"} +# Restructurer les données -## Exercices d'application +## Principe -### Pourquoi a-t-on besoin d'un code commune quand on a déjà son nom ? +Quand on a plusieurs informations pour un même individu ou groupe, on +retrouve généralement deux types de structure de données : -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. +* 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*](https://r4ds.hadley.nz/): -::: {.content-visible when-profile="en"} +![Données _long_ et _wide_ (Source: [*R for Data Science*](https://r4ds.hadley.nz/))](https://d33wubrfki0l68.cloudfront.net/3aea19108d39606bbe49981acda07696c0c7fcd8/2de65/images/tidy-9.png) -### Why do we need a commune code when we already have its name? +L'aide mémoire suivante aidera à se rappeler les fonctions à appliquer si besoin : -This exercise will take a step back to understand why we assumed above that the commune code was the key for joining data. +![](https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/reshape.png){fig-width=60%} + +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. ::: +::: {.content-visible when-profile="en"} +# Restructuring datasets -{{< include "02_pandas_suite/_exo3.qmd" >}} -{{< include "02_pandas_suite/_exo3_solution.qmd" >}} +## Principle -::: {.content-visible when-profile="fr"} +When we have multiple pieces of information for the same individual or group, we generally find two types of data structures: -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. +* __Wide__ format: the data contains repeated observations for the same individual (or group) in different columns. +* __Long__ format: the data contains repeated observations for the same individual in different rows, with a column distinguishing the observation levels. -::: +An example of the distinction between the two can be taken from Hadley Wickham's reference book, [*R for Data Science*](https://r4ds.hadley.nz/): -::: {.content-visible when-profile="en"} +![Wide and Long Data Formats (Source: [*R for Data Science*](https://r4ds.hadley.nz/))](https://d33wubrfki0l68.cloudfront.net/3aea19108d39606bbe49981acda07696c0c7fcd8/2de65/images/tidy-9.png) -This small exercise reassures us as the duplicated labels are actually the same commune names but in different departments. So, these are not duplicated observations. We can thus rely on the commune codes, which are unique. +The following cheat sheet will help remember the functions to apply if needed: + +![](https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/reshape.png){fig-width=60%} +Switching from a *wide* format to a *long* format (or vice versa) can be extremely practical because certain functions are more suitable for one form of data than the other. + +Generally, with `Python` as with `R`, __long formats are often preferable__. Wide formats are rather designed for spreadsheets like `Excel`, where we have a limited number of rows to create pivot tables from. ::: ::: {.content-visible when-profile="fr"} +## Exercice d'application -### Calculer une empreinte carbone grâce à l'association entre des sources - +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 ::: ::: {.content-visible when-profile="en"} +## Application -### Associating different sources to compute carbon footprints - +The ADEME data, and the Insee data as well, are in the _wide_ format. The next exercise illustrates the benefit of converting from _long_ to _wide_ before creating a plot with the `plot` method seen in the previous chapter. ::: - -{{< include "02_pandas_suite/_exo4.qmd" >}} -{{< include "02_pandas_suite/_exo4_solution.qmd" >}} - +{{< include "02_pandas_suite/_exo2.qmd" >}} +{{< include "02_pandas_suite/_exo2_solution.qmd" >}} ::: {.content-visible when-profile="fr"} diff --git a/content/manipulation/02_pandas_suite/_exo3.qmd b/content/manipulation/02_pandas_suite/_exo3.qmd index 7d2b01899f..1c235433d4 100644 --- a/content/manipulation/02_pandas_suite/_exo3.qmd +++ b/content/manipulation/02_pandas_suite/_exo3.qmd @@ -16,7 +16,7 @@ On se focalise temporairement sur les observations où le libellé comporte plus * _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. +* _Question 5_. Déterminer la taille moyenne (variable nombre de personnes: `POPULATION`) 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), @@ -46,7 +46,7 @@ We temporarily focus on observations where the label involves more than two diff * _Question 4_. To get a better view, reorder the obtained dataset alphabetically. -* _Question 5_. Determine the average size (variable number of people: `NBPERSMENFISC16`) and some descriptive statistics of this data. Compare it to the same statistics on the data where labels and commune codes coincide. +* _Question 5_. Determine the average size (variable number of people: `POPULATION`) and some descriptive statistics of this data. Compare it to the same statistics on the data where labels and commune codes coincide. * _Question 6_. Check the major cities (more than 100,000 people) for the proportion of cities where the same name is associated with different commune codes. diff --git a/content/manipulation/02_pandas_suite/_exo3_solution.qmd b/content/manipulation/02_pandas_suite/_exo3_solution.qmd index 5ce81bbe7a..a3c13c7dfb 100644 --- a/content/manipulation/02_pandas_suite/_exo3_solution.qmd +++ b/content/manipulation/02_pandas_suite/_exo3_solution.qmd @@ -39,13 +39,13 @@ filosofi.loc[ print(10*"--" + "Communes dupliquées" + 10*"--") print( filosofi.loc[ - filosofi['LIBGEO'].isin(doublons['LIBGEO']), 'NBPERSMENFISC16' + filosofi['LIBGEO'].isin(doublons['LIBGEO']), 'POPULATION' ].describe() ) print(10*"--" + "Communes non dupliquées" + 10*"--") print( filosofi.loc[ - ~filosofi['LIBGEO'].isin(doublons['LIBGEO']), 'NBPERSMENFISC16' + ~filosofi['LIBGEO'].isin(doublons['LIBGEO']), 'POPULATION' ].describe() ) ``` @@ -53,16 +53,16 @@ print( ```{python} #| output: false # Question 6 -emissions_big_city = filosofi.loc[filosofi['NBPERSMENFISC16']>100000].copy() +emissions_big_city = filosofi.loc[filosofi['POPULATION']>100000].copy() emissions_big_city['probleme'] = emissions_big_city['LIBGEO'].isin(doublons['LIBGEO']) -emissions_big_city['probleme'].mean() +emissions_big_city['probleme'].mean() emissions_big_city[emissions_big_city['probleme']] -print(100*emissions_big_city['probleme'].mean()) #8,33 % +print(100*emissions_big_city['probleme'].mean()) #9,52 % ``` ```{python} #| output: false # Question 7 filosofi[filosofi['LIBGEO'] == 'Montreuil'] -filosofi[filosofi['LIBGEO'].str.contains('Saint-Denis')].head(10) +filosofi[filosofi['LIBGEO'].str.contains('Saint-Denis', na=False)].head(10) ``` diff --git a/content/manipulation/02_pandas_suite/_exo4_solution.qmd b/content/manipulation/02_pandas_suite/_exo4_solution.qmd index e54629138d..cb665e24c1 100644 --- a/content/manipulation/02_pandas_suite/_exo4_solution.qmd +++ b/content/manipulation/02_pandas_suite/_exo4_solution.qmd @@ -2,7 +2,7 @@ #| output: false # Question 1 -emissions['emissions'] = emissions.sum(axis = 1, numeric_only = True) +emissions_totales_commune = emissions.sum(axis = 1, numeric_only = True) ``` ```{python} @@ -10,7 +10,8 @@ emissions['emissions'] = emissions.sum(axis = 1, numeric_only = True) # Question 2 emissions_merged = ( - emissions.reset_index() + emissions.assign(emissions = emissions_totales_commune) + .reset_index() .merge(filosofi, left_on = "INSEE commune", right_on = "CODGEO") ) ``` @@ -19,7 +20,8 @@ emissions_merged = ( #| output: false # Question 3 -emissions_merged['empreinte'] = emissions_merged['emissions']/emissions_merged['NBPERSMENFISC16'] +emissions_merged = emissions_merged.loc[emissions_merged['POPULATION'] > 0] +emissions_merged['empreinte'] = emissions_merged['emissions']/emissions_merged['POPULATION'] emissions_merged['empreinte'] = emissions_merged['empreinte'].astype(float) ``` diff --git a/content/manipulation/02_pandas_suite/_exo5_preliminary.qmd b/content/manipulation/02_pandas_suite/_exo5_preliminary.qmd index e130d07c5d..b56f6ed76a 100644 --- a/content/manipulation/02_pandas_suite/_exo5_preliminary.qmd +++ b/content/manipulation/02_pandas_suite/_exo5_preliminary.qmd @@ -10,21 +10,23 @@ To ensure you are able to complete the next exercise, here is the dataframe requ ```{python} #| echo: true -emissions['emissions'] = emissions.sum(axis = 1, numeric_only = True) +emissions_totales_commune = emissions.sum(axis = 1, numeric_only = True) emissions_merged = ( - emissions.reset_index() + emissions.assign(emissions = emissions_totales_commune) + .reset_index() .merge(filosofi, left_on = "INSEE commune", right_on = "CODGEO") ) -emissions_merged['empreinte'] = emissions_merged['emissions']/emissions_merged['NBPERSMENFISC16'] +emissions_merged = emissions_merged.loc[emissions_merged['POPULATION'] > 0] +emissions_merged['empreinte'] = emissions_merged['emissions']/emissions_merged['POPULATION'] emissions_merged['empreinte'] = emissions_merged['empreinte'].astype(float) -``` +``` ```{python} #| echo: true emissions_table = ( emissions_merged - .rename(columns={"dep_y": "dep", "NBPERSMENFISC16": "population", "MED16": "revenu"}) + .rename(columns={"dep_y": "dep", "POPULATION": "population", "NIVVIE_MEDIAN": "revenu"}) .groupby("dep") .agg({"empreinte": "sum", "revenu": "median", "population": "sum"}) #pas vraiment le revenu médian .reset_index()