Skip to content

Commit 671f75a

Browse files
authored
Introduction au Machine Learning (#72)
Premiers éléments sur le site sur le ML et l'econo
1 parent 9d3722e commit 671f75a

File tree

10 files changed

+1971
-5
lines changed

10 files changed

+1971
-5
lines changed

config.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,37 @@ unsafe= true
3737
url = "/visualisation"
3838
weight = 3
3939

40+
[[menu.main]]
41+
name = "Modéliser"
42+
url = "/modelisation"
43+
weight = 4
44+
45+
4046
[[menu.main]]
4147
name = "Git"
4248
url = "/git"
43-
weight = 4
49+
weight = 5
4450

4551

4652
[[menu.main]]
4753
name = "Evaluation"
4854
url = "/evaluation"
49-
weight = 5
55+
weight = 6
5056

5157
[[menu.main]]
5258
name = "Travaux dirigés"
5359
url = "/listeTP"
54-
weight = 6
60+
weight = 7
5561

5662
[[menu.main]]
5763
name = "References"
5864
url = "/references"
59-
weight = 7
65+
weight = 8
6066

6167
[[menu.main]]
6268
name = "Github"
6369
url = "https://github.com/linogaliana/python-datascientist"
64-
weight = 8
70+
weight = 9
6571

6672
[params]
6773
# Source Code repository section
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
jupyter:
3+
jupytext:
4+
text_representation:
5+
extension: .Rmd
6+
format_name: rmarkdown
7+
format_version: '1.2'
8+
jupytext_version: 1.6.0
9+
kernelspec:
10+
display_name: Python 3
11+
language: python
12+
name: python3
13+
title: "Préparation des données pour construire un modèle"
14+
date: 2020-10-15T13:00:00Z
15+
draft: false
16+
weight: 10
17+
output:
18+
html_document:
19+
keep_md: true
20+
self_contained: true
21+
slug: preprocessing
22+
---
23+
24+
```{r setup, include=FALSE}
25+
library(knitr)
26+
library(reticulate)
27+
knitr::knit_engines$set(python = reticulate::eng_python)
28+
knitr::opts_chunk$set(fig.path = "")
29+
knitr::opts_chunk$set(eval = TRUE, echo = FALSE, warning = FALSE, message = FALSE)
30+
31+
# Hook from Maelle Salmon: https://ropensci.org/technotes/2020/04/23/rmd-learnings/
32+
knitr::knit_hooks$set(
33+
plot = function(x, options) {
34+
hugoopts <- options$hugoopts
35+
paste0(
36+
"{", "{<figure src=", # the original code is simpler
37+
# but here I need to escape the shortcode!
38+
'"', x, '" ',
39+
if (!is.null(hugoopts)) {
40+
glue::glue_collapse(
41+
glue::glue('{names(hugoopts)}="{hugoopts}"'),
42+
sep = " "
43+
)
44+
},
45+
">}}\n"
46+
)
47+
}
48+
)
49+
50+
```
51+
52+
```{python, include = FALSE}
53+
import os
54+
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = 'C:/Users/W3CRK9/AppData/Local/r-miniconda/envs/r-reticulate/Library/plugins/platforms'
55+
os.environ["PROJ_LIB"] = r'C:\Users\W3CRK9\AppData\Local\r-miniconda\pkgs\proj4-4.9.3-hfa6e2cd_9\Library\share'
56+
os.environ['GDAL_DATA'] = r"C:\Users\W3CRK9\AppData\Local\r-miniconda\envs\r-reticulate\Library\share\gdal"
57+
```
58+
59+
Pour illustrer le travail de données nécessaire pour construire un modèle de Machine Learning, mais aussi nécessaire pour l'exploration de données avant de faire une régression linéaire, nous allons partir du jeu de données de [résultat des élections US 2016 au niveau des comtés](https://public.opendatasoft.com/explore/dataset/usa-2016-presidential-election-by-county/download/?format=geojson&timezone=Europe/Berlin&lang=fr)
60+
61+
Le guide utilisateur de `scikit` est une référence précieuse, à consulter régulièrement. La partie sur le *preprocessing* est
62+
disponible [ici](https://scikit-learn.org/stable/modules/preprocessing.html).
63+
64+
## Explorer la structure des données
65+
66+
La première étape nécessaire à suivre avant de modéliser est de déterminer les variables à inclure dans le modèle. Les fonctionalité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 [**LIEN**]
67+
68+
{{% panel status="exercise" title="Exercise 1: importer les données" icon="fas fa-pencil-alt" %}}
69+
1. Importer les données (l'appeler `df`) des élections américaines et regarder les informations dont on dispose
70+
2. Créer une variable `republican_winner` égale à `red` quand la variable `rep16_frac` est supérieure à `dep16_frac` (`blue` sinon)
71+
3. (optionnel) Représenter une carte des résultats avec en rouge les comtés où les républicains ont gagné et en bleu ceux où se sont
72+
les démocrates
73+
{{% /panel %}}
74+
75+
```{python}
76+
import numpy as np
77+
import pandas as pd
78+
import geopandas as gpd
79+
import seaborn as sns
80+
import matplotlib.pyplot as plt
81+
df = gpd.read_file("https://public.opendatasoft.com/explore/dataset/usa-2016-presidential-election-by-county/download/?format=geojson&timezone=Europe/Berlin&lang=fr")
82+
df['winner'] = np.where(df['rep16_frac'] > df['dem16_frac'], '#FF0000', '#0000FF')
83+
# df.plot('winner', color = df['winner'], figsize = (20,20))
84+
```
85+
86+
Avant d'être en mesure de sélectionner le meilleur ensemble de variables explicatives, nous allons prendre un nombre restreint et arbitraire de variables. La première tâche est de représenter les relations entre les données, notamment leur relation à la variable que l'on va chercher à expliquer (le score du parti républicain aux élections 2016) ainsi que les relations entre les variables ayant vocation à expliquer la variable dépendante.
87+
88+
{{% panel status="exercise" title="Exercise 2: regarder la corrélation entre les variables" icon="fas fa-pencil-alt" %}}
89+
90+
Créer un DataFrame plus petit avec les variables `rep16_frac` et `unemployment`, `median_age`, `asian`, `black`, `white_not_latino_population`,`latino_population`, `gini_coefficient`, `less_than_high_school`, `adult_obesity`, `median_earnings_2010_dollars` et ensuite :
91+
92+
1. Représenter une matrice de corrélation graphique
93+
1. Choisir quelques variables (pas plus de 4 ou 5) dont `rep16_frac` et représenter une matrice de nuages de points
94+
2. (optionnel) Refaire ces figures avec `plotly`
95+
{{% /panel %}}
96+
97+
La matrice de corrélation donne, avec les fonctionalités de `pandas`:
98+
99+
```{python}
100+
df2 = df[["rep16_frac", "unemployment", "median_age", "asian", "black", "white_not_latino_population","latino_population", "gini_coefficient", "less_than_high_school", "adult_obesity", "median_earnings_2010_dollars"]]
101+
df2.corr()#.style.background_gradient(cmap='coolwarm').set_precision(2)
102+
plt.show()
103+
```
104+
105+
Alors que celle construite avec `seaborn` aura l'aspect suivant:
106+
107+
```{python}
108+
sns.heatmap(df2.corr(), cmap='coolwarm', annot=True, fmt=".2f")
109+
```
110+
111+
112+
La matrice de nuage de point aura, par exemple, l'aspect suivant:
113+
114+
```{python}
115+
ax = pd.plotting.scatter_matrix(df2[["rep16_frac", "unemployment", "median_age", "asian", "black"]], figsize = (15,15))
116+
ax
117+
plt.show()
118+
```
119+
120+
121+
```{python, include = FALSE}
122+
import plotly
123+
import plotly.express as px
124+
htmlsnip2 = px.scatter_matrix(df2[["rep16_frac", "unemployment", "median_age", "asian", "black"]])
125+
htmlsnip2.update_traces(diagonal_visible=False)
126+
# Pour inclusion dans le site web
127+
htmlsnip2 = plotly.io.to_html(htmlsnip2, include_plotlyjs=False)
128+
```
129+
130+
131+
Avec `plotly`, le résultat devrait ressembler au graphique suivant:
132+
133+
{{< rawhtml >}}
134+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
135+
```{r}
136+
tablelight::print_html(py$htmlsnip2)
137+
```
138+
{{< /rawhtml >}}
139+
140+
141+
142+
## Transformer les données
143+
144+
Les différences d'échelle ou de distribution entre les variables peuvent
145+
diverger des hypothèses sous-jacentes dans les modèles. Par exemple, dans le cadre
146+
de la régression linéaire, les variables catégorielles ne sont pas traitées à la même
147+
enseigne que les variables ayant valeur dans $\mathbb{R}$. Il est ainsi
148+
souvent nécessaire d'appliquer des tâches de *preprocessing*, c'est-à-dire
149+
des tâches de modification de la distribution des données pour les rendre
150+
cohérentes avec les hypothèses des modèles.
151+
152+
### Standardisation
153+
154+
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 d'avoir des données dans cette distribution.
155+
156+
{{% panel status="warning" title="Warning" icon="fa fa-exclamation-triangle" %}}
157+
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`.
158+
159+
La **normalisation** consiste à modifier les données de manière à avoir une norme unitaire. La raison est expliquée plus bas
160+
{{% /panel %}}
161+
162+
163+
{{% panel status="exercise" title="Exercice: standardisation" icon="fas fa-pencil-alt" %}}
164+
1. Standardiser la variable `median_earnings_2010_dollars` (ne pas écraser les valeurs !) et regarder l'histogramme avant/après normalisation
165+
2. Créer `scaler`, un `Transformer` 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.
166+
3. Appliquer `scaler` sur les autres lignes du DataFrame et comparer les distributions obtenues de la variable `median_earnings_2010_dollars`.
167+
{{% /panel %}}
168+
169+
La standardisation permet d'obtenir la modification suivante de la distribution:
170+
171+
```{python, message = FALSE, warning = FALSE}
172+
# Question 1
173+
from sklearn import preprocessing
174+
df2['y_standard'] = preprocessing.scale(df2['median_earnings_2010_dollars'])
175+
f, axes = plt.subplots(2, figsize=(10, 10))
176+
sns.distplot(df2["median_earnings_2010_dollars"] , color="skyblue", ax=axes[0])
177+
sns.distplot(df2["y_standard"] , color="olive", ax=axes[1])
178+
```
179+
180+
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
181+
182+
```{python}
183+
# Question 2
184+
scaler = preprocessing.StandardScaler().fit(df2.head(1000))
185+
scaler.transform(df2.head(1000))
186+
print("Moyenne de chaque variable sur 1000 premières observations")
187+
scaler.transform(df2.head(1000)).mean(axis=0)
188+
print("Ecart-type de chaque variable sur 1000 premières observations")
189+
scaler.transform(df2.head(1000)).std(axis=0)
190+
```
191+
192+
Les paramètres qui seront utilisés pour une standardisation ultérieure de la manière suivante sont stockés dans les attributs `.mean_` et `.scale_`
193+
194+
```{python, echo = TRUE}
195+
scaler.mean_
196+
scaler.scale_
197+
```
198+
199+
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.
200+
201+
```{python}
202+
# Question 3
203+
X1 = scaler.transform(df2.head(1000))
204+
X2 = scaler.transform(df2[1000:])
205+
col_pos = df2.columns.get_loc("median_earnings_2010_dollars")
206+
# X2.mean(axis = 0)
207+
# X2.std(axis = 0)
208+
f, axes = plt.subplots(2, figsize=(10, 10))
209+
sns.distplot(X1[:,col_pos] , color="skyblue", ax=axes[0])
210+
sns.distplot(X2[:,col_pos] , color="olive", ax=axes[1])
211+
```
212+
213+
214+
### Normalisation
215+
216+
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*
217+
218+
{{% panel status="exercise" title="Exercice: normalization" icon="fas fa-pencil-alt" %}}
219+
1. Normaliser la variable `median_earnings_2010_dollars` (ne pas écraser les valeurs !) et regarder l'histogramme avant/après normalisation
220+
2. Vérifier que la norme $\mathcal{l}_2$ est bien égale à 1.
221+
{{% /panel %}}
222+
223+
```{python}
224+
scaler = preprocessing.Normalizer().fit(df2.dropna(how = "any").head(1000))
225+
X1 = scaler.transform(df2.dropna(how = "any").head(1000))
226+
227+
f, axes = plt.subplots(2, figsize=(10, 10))
228+
sns.distplot(df2["median_earnings_2010_dollars"] , color="skyblue", ax=axes[0])
229+
sns.distplot(X1[:,col_pos] , color="olive", ax=axes[1])
230+
231+
# Question 2
232+
# np.sqrt(np.sum(X1**2, axis=1))[:5] # L2-norm
233+
```
234+
235+
{{% panel status="warning" title="Warning" icon="fa fa-exclamation-triangle" %}}
236+
` 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
237+
238+
* retirer les valeurs manquantes du DataFrame avec la méthode `dropna`: `df.dropna(how = "any")`;
239+
* ou les imputer avec un modèle adéquat. `scikit` permet de le faire ([info](https://scikit-learn.org/stable/modules/preprocessing.html#imputation-of-missing-values))
240+
{{% /panel %}}
241+
242+
243+
### Encodage des valeurs catégorielles
244+
245+
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:
246+
247+
* `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 désiré
248+
* `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é, il y a donc multicollinéarité. Avec une régression linéaire avec constante, il convient de retirer une modalité avant l'estimation.
249+
* `OrdinalEncoder`: une version généralisée du `LabelEncoder`. `OrdinalEncoder` a vocation à s'appliquer sur des matrices ($X$), alors que `LabelEncoder` est plutôt pour un vecteur ($y$)
250+
* `OneHotEncoder`: 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
251+
252+
253+
{{% panel status="warning" title="Warning" icon="fa fa-exclamation-triangle" %}}
254+
Prendra les variables `state` et `county` dans `df`
255+
1. Appliquer à `state` un `LabelEncoder`
256+
2. Regarder la *dummy expansion* de `state`
257+
3. Appliquer un `OrdinalEncoder` à `df[['state', 'county']]` ainsi qu'un `OneHotEncoder`
258+
{{% /panel %}}
259+
260+
Le résultat du *label encoding* est relativement intuitif, notamment quand on le met en relation avec le vecteur initial
261+
262+
```{python}
263+
# Question 1
264+
label_enc = preprocessing.LabelEncoder().fit(df['state'])
265+
np.column_stack((label_enc.transform(df['state']),df['state']))
266+
```
267+
268+
L'expansion par variables dichotomiques également:
269+
270+
```{python}
271+
# Question 2
272+
pd.get_dummies(df['state'])
273+
```
274+
275+
Le résultat du *ordinal encoding* est cohérent avec celui du *label encoding*:
276+
277+
```{python}
278+
ord_enc = preprocessing.OrdinalEncoder().fit(df[['state', 'county']])
279+
# ord_enc.transform(df[['state', 'county']])
280+
```
281+
282+
```{python}
283+
ord_enc.transform(df[['state', 'county']])[:,0]
284+
```
285+
286+
Enfin, on peut noter que `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*:
287+
288+
```{python}
289+
onehot_enc = preprocessing.OneHotEncoder().fit(df[['state', 'county']])
290+
```
291+
292+
```{python}
293+
onehot_enc.transform(df[['state', 'county']])
294+
```
295+
296+

0 commit comments

Comments
 (0)