Préparation des données pour construire un modèle
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 US croisées à des variables socio-démographiques. Le code est disponible sur Github mais l’exercice 1 permet, à ceux qui le désirent, d’essayer de reproduire la constitution de la base de données.
Le guide utilisateur de scikit
est une référence précieuse,
à consulter régulièrement. La partie sur le preprocessing est
disponible ici.
Nous verrons dans le chapitre sur les pipelines comment industrialiser ces étapes de pré-processing 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 étant éclatées, le code pour construire une base combinant toutes ces
sources 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
:
Exercice
Exercice 1 : Importer les données des élections US [OPTIONNEL]
- Télécharger et importer le shapefile depuis ce lien
- Exclure les Etats suivants: “02”, “69”, “66”, “78”, “60”, “72”, “15”
- Importer les résultats des élections depuis ce lien
- Importer les bases disponibles sur le site de l’USDA en faisant attention à renommer les variables de code FIPS de manière identique dans les 4 bases
- Merger ces 4 bases dans une base unique de caractéristiques socio-économiques
- Merger aux données électorales à partir du code FIPS
- Merger au shapefile à partir du code FIPS. Faire attention aux 0 à gauche dans certains codes. Il est
recommandé d’utiliser la méthode
str.lstrip
pour les retirer - Importer les données des élections 2000 à 2016 à partir du MIT Election Lab? Les données peuvent être directement requêtées depuis l’url https://dataverse.harvard.edu/api/access/datafile/3641280?gbrecs=false
- Créer une variable
share
comptabilisant la part des votes pour chaque candidat. Ne garder que les colonnes"year", "FIPS", "party", "candidatevotes", "share"
- Faire une conversion
long
towide
avec la méthodepivot_table
pour garder une ligne par comté x année avec en colonnes les résultats de chaque candidat dans cet état. - Merger à partir du code FIPS au reste de la base.
Si vous ne faites pas l’exercice 1, pensez à charger les données en executant la fonction get_data.py
:
#!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()
ERROR 1: PROJ: proj_create_from_database: Open of /miniconda/envs/python-ENSAE/share/proj failed
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 | 37 | 181 | 01008591 | 0500000US37181 | 37181 | Vance | 06 | 653713542 | 42178610 | POLYGON ((-78.49773 36.51467, -78.45728 36.541... | ... | 0.630827 | 0.004743 | 0.364429 | 0.638870 | 0.004891 | 0.356239 | 0.612154 | 0.020824 | 0.367022 | democrats |
4 | 47 | 183 | 01639799 | 0500000US47183 | 47183 | Weakley | 06 | 1503107848 | 3707114 | POLYGON ((-88.94916 36.41010, -88.81642 36.410... | ... | 0.335720 | 0.017458 | 0.646822 | 0.287590 | 0.014914 | 0.697495 | 0.227511 | 0.033158 | 0.739330 | republican |
5 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
color_dict = {'republican': '#FF0000', 'democrats': '#0000FF'}
#votes.plot(column = "winner", figsize = (12,12), c=votes['winner'].map(color_dict))
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')
# plt.show()
(-127.6146362, -64.0610978, 23.253819649999997, 50.628669349999996)
Les cartes choroplèthes peuvent donner une impression fallacieuse ayant 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.
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
pour répliquer, à faible coût, cette approche avec
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)'}
)
# Pour inclusion sur site web
fig_plotly.write_json("people_vote.json")
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 variable, 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.
Exercice
Exercice 2 : Regarder les corrélations entre les variables
-
Créer un DataFrame
df2
plus petit avec les variableswinner
etvotes_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
-
Représenter grâce à un graphique la matrice de corrélation avec
heatmap
deseaborn
.
La matrice construite avec seaborn
aura un aspect comme suit:
g1.figure.get_figure()
Alors que celle construite directement avec pandas
ressemblera plutôt à ce tableau :
g2
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 |
- Choisir quelques variables (pas plus de 4 ou 5) et représenter une matrice de nuages de points
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)
- (optionnel) Refaire ces figures avec
plotly
qui offre également la possibilité de faire une matrice de corrélation. Le résultat devrait ressembler au graphique suivant :
La matrice de corrélation devrait avoir l’aspect suivant:
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:
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.
Warning
Pour un statisticien, le terme normalization
dans le vocable scikit
peut avoir un sens contre-intuitif. On s’attendrait à ce que la normalisation consiste à transformer une variable de manière à ce que $X \sim \mathcal{N}(0,1)$. C’est, en fait, la standardisation en scikit
.
La normalisation consiste à modifier les données de manière à avoir une norme unitaire. La raison est expliquée plus bas.
Exercice
Exercice 3: Standardisation
- Standardiser la variable
Median_Household_Income_2019
(ne pas écraser les valeurs !) et regarder l’histogramme avant/après normalisation.
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)
Note : On obtient bien une distribution centrée à zéro et on pourrait vérifier que la variance empirique soit bien égale à 1. On pourrait aussi vérifier que ceci est vrai également quand on transforme plusieurs colonnes à la fois.
- Créer
scaler
, unTransformer
que vous construisez sur les 1000 premières lignes de votre DataFrame. Vérifier la moyenne et l’écart-type de chaque colonne sur ces mêmes observations.
Note : Les paramètres qui seront utilisés pour une standardisation ultérieure sont stockés dans les attributs .mean_
et .scale_
On peut voir ces attributs comme des paramètres entraînés sur un certain jeu de données et qu’on peut réutiliser sur un autre, à condition que les dimensions coïncident.
- Appliquer
scaler
sur les autres lignes du DataFrame et comparer les distributions obtenues de la variableMedian_Household_Income_2019
.
array([<AxesSubplot:ylabel='Density'>, <AxesSubplot:ylabel='Density'>],
dtype=object)
Note : Une fois appliqués à un autre DataFrame
, on peut remarquer que la distribution n’est pas exactement centrée-réduite dans le DataFrame
sur lequel les paramètres n’ont pas été estimés. C’est normal, l’échantillon initial n’était pas aléatoire, les moyennes et variances de cet échantillon n’ont pas de raison de coïncider avec les moments de l’échantillon complet.
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.
Exercice
Exercice 4 : Normalisation
- Normaliser la variable
Median_Household_Income_2019
(ne pas écraser les valeurs !) et regarder l’histogramme avant/après normalisation.
array([<AxesSubplot:xlabel='Median_Household_Income_2019', ylabel='Density'>,
<AxesSubplot:ylabel='Density'>], dtype=object)
- Vérifier que la norme $\mathcal{l}_2$ est bien égale à 1.
Warning
preprocessing.Normalizer
n’accepte pas les valeurs manquantes, alors que preprocessing.StandardScaler()
s’en accomode (dans la version 0.22
de scikit). Pour pouvoir aisément appliquer le normalizer, il faut
- retirer les valeurs manquantes du DataFrame avec la méthode
dropna
:df.dropna(how = "any")
; - ou les imputer avec un modèle adéquat.
scikit
permet de le faire.
Encodage des valeurs catégorielles
Les données catégorielles doivent être recodées sous forme de valeurs numériques pour être intégrables dans le cadre d’un modèle. 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 duLabelEncoder
qui a vocation à s’appliquer sur des matrices ($X$), alors queLabelEncoder
est plutôt pour 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 multicolliné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
Exercice
Exercice 5 : Encoder des variables catégorielles
-
Créer
df
qui conserve uniquement les variablesstate_name
etcounty_name
dansvotes
. -
Appliquer à
state_name
unLabelEncoder
Note : Le résultat du label encoding est relativement intuitif, notamment quand on le met en relation avec le vecteur initial.
-
Regarder la dummy expansion de
state_name
-
Appliquer un
OrdinalEncoder
àdf[['state_name', 'county_name']]
*Note : Le résultat du *ordinal encoding* est cohérent avec celui du label encoding*
- Appliquer un
OneHotEncoder
àdf[['state_name', 'county_name']]
Note : scikit
optimise l’objet nécessaire pour stocker le résultat d’un modèle de transformation. Par exemple, le résultat de l’encoding One Hot est un objet très volumineux. Dans ce cas, scikit utilise une matrice Sparse.