Préparation des données pour construire un modèle

Download nbviewer Onyxia
Binder Open In Colab githubdev

Ce chapitre utilise le jeu de données présenté dans l’introduction de cette partie : les données de vote aux élections présidentielles américaines de 2020 au niveau des comtés croisées à des variables socio-démographiques. Le code de consitution de la base de données est disponible sur Github. L’exercice 1 permet, à ceux qui le désirent, d’essayer de le reconstituer pas à pas.

Le guide utilisateur de scikit est une référence précieuse, à consulter régulièrement. La partie sur le preprocessing est disponible ici.

L’objectif de ce chapitre est de présenter quelques éléments de préparation des données. Il s’agit d’une étape fondamentale, à ne pas négliger. Les modèles reposent sur certaines hypothèses, généralement relatives à la distribution théorique des variables qui y sont intégrées.

Il est nécessaire de faire correspondre la distribution empirique à ces hypothèses ce qui implique un travail de restructuration des données. Celui-ci permettra d’avoir des résultats de modélisation plus pertinents. Nous verrons dans le chapitre sur les pipelines comment industrialiser ces étapes de preprocessing afin de se simplifier la vie pour appliquer un modèle sur un jeu de données différent de celui sur lequel il a été estimé.

Construction de la base de données

Les sources de données étant diverses, le code qui construit la base finale est directement fourni. Le travail de construction d’une base unique est un peu fastidieux mais il s’agit d’un bon exercice, que vous pouvez tenter, pour réviser pandas :

Si vous ne faites pas l’exercice 1, pensez à charger les données en executant la fonction get_data.py :

#!pip install --upgrade xlrd #colab bug verson xlrd
#!pip install geopandas

import requests

url = 'https://raw.githubusercontent.com/linogaliana/python-datascientist/master/content/course/modelisation/get_data.py'
r = requests.get(url, allow_redirects=True)
open('getdata.py', 'wb').write(r.content)

import getdata
votes = getdata.create_votes_dataframes()

Ce code introduit une base nommée votes dans l’environnement. Il s’agit d’une base rassemblant les différentes sources. Elle a l’aspect suivant:

STATEFP COUNTYFP COUNTYNS AFFGEOID GEOID NAME LSAD ALAND AWATER geometry ... share_2008_democrat share_2008_other share_2008_republican share_2012_democrat share_2012_other share_2012_republican share_2016_democrat share_2016_other share_2016_republican winner
0 29 227 00758566 0500000US29227 29227 Worth 06 690564983 493903 POLYGON ((-94.63203 40.57176, -94.53388 40.570... ... 0.363714 0.034072 0.602215 0.325382 0.041031 0.633588 0.186424 0.041109 0.772467 republican
1 31 061 00835852 0500000US31061 31061 Franklin 06 1491355860 487899 POLYGON ((-99.17940 40.35068, -98.72683 40.350... ... 0.284794 0.019974 0.695232 0.250000 0.026042 0.723958 0.149432 0.045427 0.805140 republican
2 36 013 00974105 0500000US36013 36013 Chautauqua 06 2746047476 1139407865 POLYGON ((-79.76195 42.26986, -79.62748 42.324... ... 0.495627 0.018104 0.486269 0.425017 0.115852 0.459131 0.352012 0.065439 0.582550 republican

3 rows × 383 columns

La carte choroplèthe suivante permet de visualiser rapidement les résultats (l’Alaska et Hawaï ont été exclus).

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns


# republican : red, democrat : blue
color_dict = {'republican': '#FF0000', 'democrats': '#0000FF'}

fig, ax = plt.subplots(figsize = (12,12))
grouped = votes.groupby('winner')
for key, group in grouped:
    group.plot(ax=ax, column='winner', label=key, color=color_dict[key])
plt.axis('off')
(-127.6146362, -64.0610978, 23.253819649999997, 50.628669349999996)

Les cartes choroplèthes peuvent donner une impression fallacieuse ce qui exiplique que ce type de carte a servi de justification pour contester les résultats du vote. En effet, un biais connu des représentations choroplèthes est qu’elles donnent une importance visuelle excessive aux grands espaces. Or, ceux-ci sont souvent des espaces peu denses et influencent donc moins la variable d’intérêt (en l’occurrence le taux de vote en faveur des républicains/démocrates). Une représentation à privilégier pour ce type de phénomènes est les ronds proportionnels (voir Insee (2018), “Le piège territorial en cartographie”).

Le GIF “Land does not vote, people do” qui avait eu un certain succès en 2020 propose un autre mode de visualisation. La carte originale a probablement été construite avec JavaScript. Cependant, on dispose avec Python de plusieurs outils pour répliquer, à faible coût, cette carte grâce à l’une des surcouches à JavaScript vue dans la partie visualisation.

En l’occurrence, on peut utiliser plotly pour tenir compte de la population:

La Figure a été obtenue avec le code suivant:

import plotly
import plotly.graph_objects as go
import pandas as pd
import geopandas as gpd


centroids = votes.copy()
centroids.geometry = centroids.centroid
centroids['size'] = centroids['CENSUS_2010_POP'] / 10000  # to get reasonable plotable number

color_dict = {"republican": '#FF0000', 'democrats': '#0000FF'}
centroids["winner"] =  np.where(centroids['votes_gop'] > centroids['votes_dem'], 'republican', 'democrats') 


centroids['lon'] = centroids['geometry'].x
centroids['lat'] = centroids['geometry'].y
centroids = pd.DataFrame(centroids[["county_name",'lon','lat','winner', 'CENSUS_2010_POP',"state_name"]])
groups = centroids.groupby('winner')

df = centroids.copy()

df['color'] = df['winner'].replace(color_dict)
df['size'] = df['CENSUS_2010_POP']/6000
df['text'] = df['CENSUS_2010_POP'].astype(int).apply(lambda x: '<br>Population: {:,} people'.format(x))
df['hover'] = df['county_name'].astype(str) +  df['state_name'].apply(lambda x: ' ({}) '.format(x)) + df['text']

fig_plotly = go.Figure(data=go.Scattergeo(
    locationmode = 'USA-states',
    lon=df["lon"], lat=df["lat"],
    text = df["hover"],
    mode = 'markers',
    marker_color = df["color"],
    marker_size = df['size'],
    hoverinfo="text"
    ))

fig_plotly.update_traces(
  marker = {'opacity': 0.5, 'line_color': 'rgb(40,40,40)', 'line_width': 0.5, 'sizemode': 'area'}
)

fig_plotly.update_layout(
        title_text = "Reproduction of the \"Acres don't vote, people do\" map <br>(Click legend to toggle traces)",
        showlegend = True,
        geo = {"scope": 'usa', "landcolor": 'rgb(217, 217, 217)'}
    )

Les cercles proportionnels permettent ainsi à l’oeil de se concentrer sur les zones les plus denses et non sur les grands espaces.

Explorer la structure des données

La première étape nécessaire à suivre avant de se lancer dans la modélisation est de déterminer les variables à inclure dans le modèle.

Les fonctionnalités de pandas sont, à ce niveau, suffisantes pour explorer des structures simples. Néanmoins, lorsqu’on est face à un jeu de données présentant de nombreuses variables explicatives (features en machine learning, covariates en économétrie), il est souvent judicieux d’avoir une première étape de sélection de variables, ce que nous verrons par la suite dans la partie dédiée.

Avant d’être en mesure de sélectionner le meilleur ensemble de variables explicatives, nous allons en prendre un nombre restreint et arbitraire. La première tâche est de représenter les relations entre les données, notamment la relation des variables explicatives à la variable dépendante (le score du parti républicain) ainsi que les relations entre les variables explicatives.

La matrice construite avec seaborn (question 2) aura l’aspect suivant:

Alors que celle construite directement avec corr de pandas ressemblera plutôt à ce tableau :

  votes_gop Unemployment_rate_2019 Median_Household_Income_2019 Percent of adults with less than a high school diploma, 2015-19 Percent of adults with a bachelor's degree or higher, 2015-19
votes_gop 1.00 -0.08 0.35 -0.11 0.37
Unemployment_rate_2019 -0.08 1.00 -0.43 0.36 -0.36
Median_Household_Income_2019 0.35 -0.43 1.00 -0.51 0.71
Percent of adults with less than a high school diploma, 2015-19 -0.11 0.36 -0.51 1.00 -0.59
Percent of adults with a bachelor's degree or higher, 2015-19 0.37 -0.36 0.71 -0.59 1.00

Le nuage de point obtenu à l’issue de la question 3 ressemblera à :

array([[<AxesSubplot: xlabel='votes_gop', ylabel='votes_gop'>,
        <AxesSubplot: xlabel='Unemployment_rate_2019', ylabel='votes_gop'>,
        <AxesSubplot: xlabel='Median_Household_Income_2019', ylabel='votes_gop'>,
        <AxesSubplot: xlabel='Percent of adults with less than a high school diploma, 2015-19', ylabel='votes_gop'>,
        <AxesSubplot: xlabel="Percent of adults with a bachelor's degree or higher, 2015-19", ylabel='votes_gop'>],
       [<AxesSubplot: xlabel='votes_gop', ylabel='Unemployment_rate_2019'>,
        <AxesSubplot: xlabel='Unemployment_rate_2019', ylabel='Unemployment_rate_2019'>,
        <AxesSubplot: xlabel='Median_Household_Income_2019', ylabel='Unemployment_rate_2019'>,
        <AxesSubplot: xlabel='Percent of adults with less than a high school diploma, 2015-19', ylabel='Unemployment_rate_2019'>,
        <AxesSubplot: xlabel="Percent of adults with a bachelor's degree or higher, 2015-19", ylabel='Unemployment_rate_2019'>],
       [<AxesSubplot: xlabel='votes_gop', ylabel='Median_Household_Income_2019'>,
        <AxesSubplot: xlabel='Unemployment_rate_2019', ylabel='Median_Household_Income_2019'>,
        <AxesSubplot: xlabel='Median_Household_Income_2019', ylabel='Median_Household_Income_2019'>,
        <AxesSubplot: xlabel='Percent of adults with less than a high school diploma, 2015-19', ylabel='Median_Household_Income_2019'>,
        <AxesSubplot: xlabel="Percent of adults with a bachelor's degree or higher, 2015-19", ylabel='Median_Household_Income_2019'>],
       [<AxesSubplot: xlabel='votes_gop', ylabel='Percent of adults with less than a high school diploma, 2015-19'>,
        <AxesSubplot: xlabel='Unemployment_rate_2019', ylabel='Percent of adults with less than a high school diploma, 2015-19'>,
        <AxesSubplot: xlabel='Median_Household_Income_2019', ylabel='Percent of adults with less than a high school diploma, 2015-19'>,
        <AxesSubplot: xlabel='Percent of adults with less than a high school diploma, 2015-19', ylabel='Percent of adults with less than a high school diploma, 2015-19'>,
        <AxesSubplot: xlabel="Percent of adults with a bachelor's degree or higher, 2015-19", ylabel='Percent of adults with less than a high school diploma, 2015-19'>],
       [<AxesSubplot: xlabel='votes_gop', ylabel="Percent of adults with a bachelor's degree or higher, 2015-19">,
        <AxesSubplot: xlabel='Unemployment_rate_2019', ylabel="Percent of adults with a bachelor's degree or higher, 2015-19">,
        <AxesSubplot: xlabel='Median_Household_Income_2019', ylabel="Percent of adults with a bachelor's degree or higher, 2015-19">,
        <AxesSubplot: xlabel='Percent of adults with less than a high school diploma, 2015-19', ylabel="Percent of adults with a bachelor's degree or higher, 2015-19">,
        <AxesSubplot: xlabel="Percent of adults with a bachelor's degree or higher, 2015-19", ylabel="Percent of adults with a bachelor's degree or higher, 2015-19">]],
      dtype=object)

Le résultat de la question 4 devrait, quant à lui, ressembler au graphique suivant :

# Pour inclusion dans le site web
htmlsnip2.write_json("scatter.json")

Transformer les données

Les différences d’échelle ou de distribution entre les variables peuvent diverger des hypothèses sous-jacentes dans les modèles.

Par exemple, dans le cadre de la régression linéaire, les variables catégorielles ne sont pas traitées à la même enseigne que les variables ayant valeur dans $\mathbb{R}$. Une variable discrète (prenant un nombre fini de valeurs) devra être transformées en suite de variables 0/1 par rapport à une modalité de référence pour être en adéquation avec les hypothèses de la régression linéaire. On appelle ce type de transformation one-hot encoding, sur lequel nous reviendrons. Il s’agit d’une transformation, parmi d’autres, disponibles dans scikit pour mettre en adéquation un jeu de données et des hypothèses mathématiques.

L’ensemble de ces tâches s’appelle le preprocessing. L’un des intérêts d’utiliser scikit est qu’on peut considérer qu’une tâche de preprocessing est une tâche d’apprentissage (on apprend des paramètres d’une structure de données) qui est réutilisable pour un jeu de données à la structure similaire:

Nous allons voir deux processus très classiques de preprocessing :

  1. La standardisation transforme des données pour que la distribution empirique suive une loi $\mathcal{N}(0,1)$.

  2. La normalisation transforme les données de manière à obtenir une norme ($\mathcal{l}_1$ ou $\mathcal{l}_2$) unitaire. Autrement dit, avec la norme adéquate, la somme des éléments est égale à 1.

Standardisation

La standardisation consiste à transformer des données pour que la distribution empirique suive une loi $\mathcal{N}(0,1)$. Pour être performants, la plupart des modèles de machine learning nécessitent souvent d’avoir des données dans cette distribution.

Normalisation

La normalisation est l’action de transformer les données de manière à obtenir une norme ($\mathcal{l}_1$ ou $\mathcal{l}_2$) unitaire. Autrement dit, avec la norme adéquate, la somme des éléments est égale à 1. Par défaut, la norme est dans $\mathcal{l}_2$. Cette transformation est particulièrement utilisée en classification de texte ou pour effectuer du clustering.

Encodage des valeurs catégorielles

Les données catégorielles doivent être recodées sous forme de valeurs numériques pour être intégrés aux modèles de machine learning. Cela peut être fait de plusieurs manières :

  • LabelEncoder: transforme un vecteur ["a","b","c"] en vecteur numérique [0,1,2]. Cette approche a l’inconvénient d’introduire un ordre dans les modalités, ce qui n’est pas toujours souhaitable

  • OrdinalEncoder: une version généralisée du LabelEncoder qui a vocation à s’appliquer sur des matrices ($X$), alors que LabelEncoder s’applique plutôt à un vecteur ($y$)

  • pandas.get_dummies effectue une opération de dummy expansion. Un vecteur de taille n avec K catégories sera transformé en matrice de taille $n \times K$ pour lequel chaque colonne sera une variable dummy pour la modalité k. Il y a ici $K$ modalités et il y a donc multicolinéarité. Avec une régression linéaire avec constante, il convient de retirer une modalité avant l’estimation.

  • OneHotEncoder est une version généralisée (et optimisée) de la dummy expansion. Il a plutôt vocation à s’appliquer sur les features ($X$) du modèle

Références

Insee. 2018. “Guide de Sémiologie Cartographique.”

Next