|
| 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