Skip to content

Commit e072890

Browse files
committed
Continue le cleaning
1 parent f3bbddc commit e072890

File tree

1 file changed

+172
-71
lines changed

1 file changed

+172
-71
lines changed

content/modelisation/0_preprocessing.qmd

Lines changed: 172 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,6 @@ fig_plotly.show()
262262

263263

264264

265-
266-
267-
268265
# La démarche générale
269266

270267
Dans ce chapitre, nous allons nous focaliser sur la préparation
@@ -312,15 +309,11 @@ notamment la relation des variables explicatives
312309
à la variable dépendante (le score du parti républicain)
313310
ainsi que les relations entre les variables explicatives.
314311

315-
::: {.cell .markdown}
316-
```{=html}
317-
<div class="alert alert-success" role="alert">
318-
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 2 : Regarder les corrélations entre les variables</h3>
319-
```
312+
::: {.exercise}
313+
## Exercice 2 (optionnel) : Regarder les corrélations entre les variables
320314

321315
__Cet exercice est OPTIONNEL__
322316

323-
324317
1. Créer un DataFrame `df2` plus petit avec les variables `winner`, `votes_gop`, `Unemployment_rate_2019`,
325318
`Median_Household_Income_2019`,
326319
`Percent of adults with less than a high school diploma, 2015-19`,
@@ -329,9 +322,6 @@ __Cet exercice est OPTIONNEL__
329322
3. Représenter une matrice de nuages de points des variables de la base `df2` avec `pd.plotting.scatter_matrix`
330323
4. (optionnel) Refaire ces figures avec `Plotly` qui offre également la possibilité de faire une matrice de corrélation.
331324

332-
```{=html}
333-
</div>
334-
```
335325
:::
336326

337327
```{python}
@@ -345,27 +335,52 @@ df2 = votes.set_index("GEOID").loc[: , ["winner", "votes_gop",
345335
"Percent of adults with a bachelor's degree or higher, 2015-19"]]
346336
```
347337

338+
La matrice construite avec `seaborn` (question 2) aura l'aspect suivant :
339+
348340
```{python}
349-
#| output: false
350-
#| echo: true
341+
import numpy as np
342+
import matplotlib.pyplot as plt
351343
import seaborn as sns
352344
353-
# 2. Matrice de corrélation graphique
354-
g1 = sns.heatmap(df2.drop("winner", axis = 1).corr(), cmap='coolwarm', annot=True, fmt=".2f")
345+
corr = df2.drop("winner", axis = 1).corr()
346+
347+
mask = np.zeros_like(corr, dtype=bool)
348+
mask[np.triu_indices_from(mask)] = True
349+
350+
# Set up the matplotlib figure
351+
fig = plt.figure()
352+
353+
# Generate a custom diverging colormap
354+
cmap = sns.diverging_palette(220, 10, as_cmap=True)
355+
356+
# Draw the heatmap with the mask and correct aspect ratio
357+
# More details at https://seaborn.pydata.org/generated/seaborn.heatmap.html
358+
sns.heatmap(
359+
corr, # The data to plot
360+
mask=mask, # Mask some cells
361+
cmap=cmap, # What colors to plot the heatmap as
362+
annot=True, # Should the values be plotted in the cells?
363+
vmax=.3, # The maximum value of the legend. All higher vals will be same color
364+
vmin=-.3, # The minimum value of the legend. All lower vals will be same color
365+
center=0, # The center value of the legend. With divergent cmap, where white is
366+
square=True, # Force cells to be square
367+
linewidths=.5, # Width of lines that divide cells
368+
cbar_kws={"shrink": .5} # Extra kwargs for the legend; in this case, shrink by 50%
369+
)
355370
356-
# Construction directement avec pandas également possible
357-
g2 = df2.drop("winner", axis = 1).corr().style.background_gradient(cmap='coolwarm').format('{:.2f}')
371+
plt.show(fig)
358372
```
359373

360-
La matrice construite avec `seaborn` (question 2) aura l'aspect suivant :
374+
Alors que celle construite directement avec `corr` de `Pandas`
375+
ressemblera plutôt à ce tableau :
361376

362377
```{python}
363-
g1
378+
#| output: false
379+
#| echo: true
380+
# Construction directement avec pandas également possible
381+
g2 = df2.drop("winner", axis = 1).corr().style.background_gradient(cmap='coolwarm').format('{:.2f}')
364382
```
365383

366-
Alors que celle construite directement avec `corr` de `Pandas`
367-
ressemblera plutôt à ce tableau :
368-
369384
```{python}
370385
g2
371386
```
@@ -375,12 +390,9 @@ Le nuage de point obtenu à l'issue de la question 3 ressemblera à :
375390
```{python}
376391
#| echo: true
377392
# 3. Matrice de nuages de points
378-
ax = pd.plotting.scatter_matrix(df2, figsize = (15,15))
393+
pd.plotting.scatter_matrix(df2)
379394
```
380395

381-
```{python}
382-
ax
383-
```
384396

385397
Le résultat de la question 4 devrait, quant à lui,
386398
ressembler au graphique suivant :
@@ -405,58 +417,49 @@ Par exemple, dans le cadre
405417
de la régression linéaire, les variables catégorielles ne sont pas traitées à la même
406418
enseigne que les variables ayant valeur dans $\mathbb{R}$. Une variable
407419
discrète (prenant un nombre fini de valeurs) devra être transformée en suite de
408-
variables 0/1 par rapport à une modalité de référence pour être en adéquation
420+
variables 0/1 (des _dummies_) par rapport à une modalité de référence pour être en adéquation
409421
avec les hypothèses de la régression linéaire.
410422
On appelle ce type de transformation
411423
*one-hot encoding*, sur laquelle nous reviendrons. Il s'agit d'une transformation,
412-
parmi d'autres, disponibles dans `scikit` pour mettre en adéquation un jeu de
424+
parmi d'autres, disponibles dans `Scikit` pour mettre en adéquation un jeu de
413425
données et des hypothèses mathématiques.
414426

415-
L'ensemble de ces tâches s'appelle le *preprocessing*. L'un des intérêts
427+
L'ensemble de ces tâches de préparation de données s'appelle le *preprocessing* ou le _feature engineering_. L'un des intérêts
416428
d'utiliser `Scikit` est qu'on peut considérer qu'une tâche de _preprocessing_
417429
est, en fait, une tâche d'apprentissage. En effet, le _preprocessing_
418430
consiste à apprendre des paramètres d'une structure
419431
de données (par exemple estimer moyennes et variances pour les retrancher à chaque
420432
observation) et on peut très bien appliquer ces paramètres
421433
à des observations qui n'ont pas servi à construire
422-
ceux-ci. Ainsi, en gardant en tête l'approche générale avec `Scikit`,
434+
ceux-ci. Autrement dit, cette préparation de données s'intègre très bien dans le _pipeline_ @fig-ml-pipeline.
423435

424-
![](https://minio.lab.sspcloud.fr/lgaliana/generative-art/pythonds/scikit_predict.png)
436+
## _Preprocessing_ de variables continues
425437

426-
nous allons voir deux processus très classiques de *preprocessing* :
438+
Nous allons voir deux processus très classiques de *preprocessing* pour des variables continues :
427439

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

430442
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.
431443

432-
::: {.cell .markdown}
433-
```{=html}
434-
<div class="alert alert-danger" role="alert">
435-
<h3 class="alert-heading"><i class="fa-solid fa-triangle-exclamation"></i> Warning</h3>
436-
```
437-
Pour un statisticien,
444+
Il en existe d'autres, par exemple le `MinMaxScaler` pour renormaliser les variables en fonction des bornes minimales et maximales des valeurs observées. Le choix de la méthode a mettre en oeuvre dépend du type d'algorithmes choisis par la suite: les hypothèses des k plus proches voisins (knn) seront différentes de celles d'une _random forest_. C'est pour cette raison que, normalement, on définit des _pipelines_ complets, intégrant à la fois _preprocessing_ et apprentissage. Ce sera l'objet des prochains chapitres.
445+
446+
::: {.caution}
447+
Pour les statisticiens.ennes,
438448
le terme _normalization_ dans le vocable `Scikit` peut avoir un sens contre-intuitif.
439449
On s'attendrait à ce que la normalisation consiste à transformer une variable de manière à ce que $X \sim \mathcal{N}(0,1)$.
440450
C'est, en fait, la **standardisation** en `Scikit` qui fait cela.
441451

442-
```{=html}
443-
</div>
444-
```
445452
:::
446453

447454

448-
## Standardisation
449-
450-
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.
455+
### Standardisation
451456

452-
::: {.cell .markdown}
453-
```{=html}
454-
<div class="alert alert-success" role="alert">
455-
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 3: Standardisation</h3>
456-
```
457+
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. Même lorsque ce n'est pas indispensable, par exemple avec des régressions logistiques, cela peut accélérer la vitesse de convergence des algorithmes.
457458

459+
::: {.exercise}
460+
## Exercice 3: Standardisation
458461

459-
1. Standardiser la variable `Median_Household_Income_2019` (ne pas écraser les valeurs !) et regarder l'histogramme avant/après normalisation.
462+
1. Standardiser la variable `Median_Household_Income_2019` (ne pas écraser les valeurs !) et regarder l'histogramme avant/après normalisation. Cette transformation est à appliquer à toute la colonne ; les prochaines questions se préoccuperont du sujet de découpage d'échantillon et d'extrapolation.
460463

461464
*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.*
462465

@@ -474,45 +477,141 @@ dimensions coïncident.
474477
*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.*
475478

476479

477-
```{=html}
478-
</div>
479-
```
480480
:::
481481

482+
Avant standardisation, notre variable a cette distribution:
483+
484+
```{python}
485+
(
486+
ggplot(df2, aes(x = "Median_Household_Income_2019")) +
487+
geom_histogram() +
488+
theme_minimal() +
489+
labs(
490+
x = "2019 Median household income (standardized)",
491+
y = "Density (number observations)"
492+
)
493+
)
494+
```
495+
496+
Après standardisation, l'échelle de la variable a changé.
497+
482498
```{python}
483499
# 1. Standardisation de Median_Household_Income_2019 et histogramme
484500
import matplotlib.pyplot as plt
485501
from sklearn import preprocessing
486502
487-
df2['y_standard'] = preprocessing.scale(df2['Median_Household_Income_2019'])
488-
f, axes = plt.subplots(2, figsize=(10, 10))
489-
sns.histplot(df2["Median_Household_Income_2019"] , color="skyblue", ax=axes[0])
490-
sns.histplot(df2["y_standard"] , color="olive", ax=axes[1])
503+
df2['y_standard'] = preprocessing.scale(
504+
df2['Median_Household_Income_2019']
505+
)
506+
507+
(
508+
ggplot(df2, aes(x = "y_standard")) +
509+
geom_histogram() +
510+
theme_minimal() +
511+
labs(
512+
x = "2019 Median household income (standardized)",
513+
y = "Density (number observations)"
514+
)
515+
)
491516
```
492517

518+
On obtient bien une moyenne égale à 0 et une variance égale à 1, aux approximations numériques prêt :
519+
520+
```{python}
521+
pd.DataFrame(
522+
{
523+
"Statistique": ["Mean", "Variance"],
524+
"Valeur": [df2['y_standard'].mean().round(), df2['y_standard'].var()]
525+
}
526+
)
527+
```
528+
529+
A la question 2, si on essaie de représenter les statistiques obtenues dans un tableau lisible, on obtient
530+
493531
```{python}
494532
# 2. Créer un scaler
495-
df2 = df2.drop("winner", axis = 1)
496-
print("Moyenne de chaque variable sur 1000 premières observations avant : ", np.array(df2.head(1000).mean(axis=0)))
497-
print("Ecart-type de chaque variable sur 1000 premières observations avant : ", np.array(df2.head(1000).std(axis=0)))
498-
scaler = preprocessing.StandardScaler().fit(df2.head(1000))
499-
scaler.transform(df2.head(1000))
500-
print("Moyenne de chaque variable sur 1000 premières observations après : ", scaler.transform(df2.head(1000)).mean(axis=0))
501-
print("Ecart-type de chaque variable sur 1000 premières observations après : ", scaler.transform(df2.head(1000)).std(axis=0))
533+
534+
df2 = df2.drop("winner", axis=1)
535+
536+
first_rows = df2.head(1000)
537+
538+
# Calculate mean and standard deviation before scaling
539+
mean_before = np.array(first_rows.mean(axis=0))
540+
std_before = np.array(first_rows.std(axis=0))
541+
542+
# Initialize and apply the scaler
543+
scaler = preprocessing.StandardScaler().fit(first_rows)
544+
scaled_data = scaler.transform(first_rows)
545+
546+
# Calculate mean and standard deviation after scaling
547+
mean_after = scaled_data.mean(axis=0)
548+
std_after = scaled_data.std(axis=0)
549+
550+
# Create DataFrame to store results
551+
result_df = pd.DataFrame({
552+
"Variable": df2.columns,
553+
"Mean before Scaling": mean_before,
554+
"Std before Scaling": std_before,
555+
"Mean after Scaling": mean_after,
556+
"Std after Scaling": std_after
557+
})
558+
```
559+
560+
```{python}
561+
from great_tables import *
562+
(
563+
GT(result_df)
564+
.fmt_nanoplot("Mean before Scaling", options = {"interactive_data_values": False})
565+
.fmt_nanoplot("Std before Scaling")
566+
.fmt_nanoplot("Mean after Scaling")
567+
.fmt_nanoplot("Std after Scaling")
568+
)
502569
```
503570

571+
On voit très clairement dans ce tableau que la standardisation a bien fonctionné.
572+
573+
Maintenant, si on construit un _transformer_ formel pour nos variables
504574

505575
```{python}
506576
# 3. Appliquer le scaler à toutes les autres lignes
507-
X1 = scaler.transform(df2.head(1000))
508-
X2 = scaler.transform(df2[1000:])
509-
col_pos = df2.columns.get_loc("Median_Household_Income_2019")
577+
standarisation = scaler.fit(df2.head(1000))
578+
standarisation
579+
```
510580

511-
f, axes = plt.subplots(2, figsize=(10, 10))
512-
sns.histplot(X1[:,col_pos] , color="skyblue", ax=axes[0])
513-
sns.histplot(X2[:,col_pos] , color="olive", ax=axes[1])
581+
On peut extrapoler notre standardiseur à un ensemble plus large de données. Si on regarde la distribution obtenue sur les 1000 premières lignes, on retrouve une échelle cohérente avec une loi $\mathcal{N(0,1)}$
582+
583+
```{python}
584+
X1 = pd.DataFrame(scaler.fit_transform(df2[1000:]))
585+
X1.columns = df2.columns
586+
587+
X2 = pd.DataFrame(scaler.transform(df2[:1000]))
588+
X2.columns = df2.columns
589+
590+
(
591+
ggplot(X1, aes(x = "Unemployment_rate_2019")) +
592+
geom_histogram() +
593+
labs(x = "Unemployment rate (standardized), 1000 first rows")
594+
)
514595
```
515596

597+
En revanche on voit que cette distribution ne correspond pas à celle qui permettrait de normaliser vraiment le reste des données. C'est un problème classique en _machine learning_, le _data drift_ lorsqu'on essaie d'extrapoler à des données dont la distribution ne correspond plus à celle des données d'apprentissage, typiquement des données non stationnaires en série temporelle.
598+
599+
```{python}
600+
(
601+
ggplot(X2, aes(x = "Unemployment_rate_2019")) +
602+
geom_histogram() +
603+
labs(x = "Unemployment rate (standardized), other rows")
604+
)
605+
606+
```
607+
608+
::: {.important}
609+
Le data drift désigne un changement dans la distribution des données au fil du temps, entraînant une dégradation des performances d’un modèle de _machine learning_ qui, par construction, a été entraîné sur des données passées.
610+
611+
Ce phénomène peut survenir à cause de variations dans la population cible, de changements dans les caractéristiques des données ou de facteurs externes.
612+
613+
Il est crucial de détecter le data drift pour ajuster ou réentraîner le modèle, afin de maintenir sa pertinence et sa précision. Les techniques de détection incluent des tests statistiques et le suivi de métriques spécifiques.
614+
:::
516615

517616
## Normalisation
518617

@@ -537,6 +636,7 @@ Cette transformation est particulièrement utilisée en classification de texte
537636
:::
538637

539638
```{python}
639+
#| eval: false
540640
# 1. Normalisation de Median_Household_Income_2019 et histogrammes
541641
scaler = preprocessing.Normalizer().fit(df2.head(1000))
542642
X1 = scaler.transform(df2.dropna(how = "any").head(1000))
@@ -547,6 +647,7 @@ sns.histplot(X1[:,col_pos] , color="olive", ax=axes[1])
547647
```
548648

549649
```{python}
650+
#| eval: false
550651
# 2. Vérification de la norme L2
551652
np.sqrt(np.sum(X1**2, axis=1))[:10] # L2-norm
552653
```

0 commit comments

Comments
 (0)