Introduction

On peut avoir besoin d’echantillonner un jeu de donnees pour differentes raisons :

tout en preservant la distribution de certaines variables.

On utilise R base et le package dplyr (group_by combine a sample_n ou sample_freq) pour realiser les echantillonnages les plus classiques que sont l’echantillonnage simple, l’echantillonnage equilibre et l’echantillonnage stratifie selon une variable qualitative.

Pour les echantillonnages plus complexes, notamment du sur-echantillonnage avec les algorithmes ROSE ou SMOTE dans le cas d’une cible rare, on emploie le package themis qui fait partie de la collection de package tidymodels.

On utilise le jeu de donnees “bankData” du package otvPlots, la modalite cible ‘yes’ de la variable y a une frequence de 12%.

Sur le CRAN, la version 0.1.0 du package themis a un bug dans la fonction step_smote, il a ete corrige dans la version en developpement sur github. En attendant le passage en production sur le CRAN de la nouvelle version, il faut donc installer la version de github avec la commande remotes::install_github("tidymodels/themis").

library("dplyr")
library("recipes")
library("themis")

# remotes::install_github("tidymodels/themis")

data("bankData", package = "otvPlots")
bankData = bankData %>% select(y, everything())

# frequence de la cible y
bankData %>% count(y, name = "nb") %>% 
  mutate(freq = scales::percent(nb / sum(nb)), 
         nb = scales::number(nb, trim = FALSE)) %>% as.data.frame
    y     nb freq
1  no 39 922  88%
2 yes  5 289  12%

Echantillonnage usuel

Echantillonnage simple / equilibre

Echantillonnage simple : 70% pour l’echantillon d’apprentissage, 30% pour l’echantillon test.

bankData = bankData %>% mutate(simple = sample(x = c("train", "test"), 
                                               size = nrow(.),
                                               replace = TRUE,
                                               prob = c(0.7, 0.3)
                                               )
                               )

bankData %>% count(simple) %>% mutate(freq = scales::percent(n / sum(n)))
  simple     n freq
1   test 13616  30%
2  train 31595  70%

Echantillonnage equilibre : 3000 modalites ‘no’ et 3000 modalites ‘yes’.

# echantillon (d'apprentissage) equilibre 3000 / 3000
dtf_train = bankData %>% group_by(y) %>% sample_n(3000) %>% ungroup
dtf_train %>% count(y)
# A tibble: 2 x 2
  y         n
  <chr> <int>
1 no     3000
2 yes    3000

Echantillonnage stratifie

Le probleme : certaines modalites rares peuvent etre presentes dans l’echantillon test mais absentes de l’echantillon d’apprentissage, ce qui declenche une erreur quand on applique le modele predictif a l’echantillon test ou qu’on le met en production.

# zoom sur les modalites de la variable 'job'
bankData %>% group_by(job) %>% summarise(nb = n(), 
                                         freq_cible = scales::percent(mean(y == 'yes'))) %>% 
  mutate(freq_modalite = scales::percent(nb / sum(nb)),
         nb = scales::number(nb, trim = FALSE)) %>% as.data.frame

# echantillon qui perd la modalite 'unknown' car il n'est pas stratifie
set.seed(2014)
dtf_train2 = bankData %>% group_by(y) %>% sample_n(200) %>% ungroup
dtf_train2 %>% count(job)
             job    nb freq_cible freq_modalite
1         admin. 5 171        12%        11.44%
2    blue-collar 9 732         7%        21.53%
3   entrepreneur 1 487         8%         3.29%
4      housemaid 1 240         9%         2.74%
5     management 9 458        14%        20.92%
6        retired 2 264        23%         5.01%
7  self-employed 1 579        12%         3.49%
8       services 4 154         9%         9.19%
9        student   938        29%         2.07%
10    technician 7 597        11%        16.80%
11    unemployed 1 303        16%         2.88%
12       unknown   288        12%         0.64%
# A tibble: 11 x 2
   job               n
   <chr>         <int>
 1 admin.           55
 2 blue-collar      58
 3 entrepreneur     18
 4 housemaid         9
 5 management       80
 6 retired          34
 7 self-employed    13
 8 services         32
 9 student          11
10 technician       74
11 unemployed       16

La stratification preserve les frequences relatives des modalites de la cible et de la variable ‘job’.

dtf_strat = bankData %>% group_by(job, y) %>% sample_frac(0.5)

dtf_strat %>% group_by(job) %>% summarise(nb = n(), 
                                          freq_cible = scales::percent(mean(y == 'yes'))) %>% 
  mutate(freq_modalite = scales::percent(nb / sum(nb)),
         nb = scales::number(nb, trim = FALSE)) %>% as.data.frame
             job    nb freq_cible freq_modalite
1         admin. 2 586        12%        11.44%
2    blue-collar 4 866         7%        21.53%
3   entrepreneur   744         8%         3.29%
4      housemaid   620         9%         2.74%
5     management 4 728        14%        20.92%
6        retired 1 132        23%         5.01%
7  self-employed   790        12%         3.50%
8       services 2 076         9%         9.18%
9        student   468        29%         2.07%
10    technician 3 798        11%        16.80%
11    unemployed   651        16%         2.88%
12       unknown   144        12%         0.64%

Sur-echantillonnage avec ROSE et SMOTE

Les deux algorithmes qu’on a choisi dans le package themis creent de nouvelles cibles artificielles a partir des vraies cibles contenues dans le jeu de donnees initial. On binarise toutes les variables qualitatives (sauf la cible) avec l’etape step_dummy(all_nominal(), - all_outcomes()) car les algorithmes ROSE et SMOTE ne s’appliquent qu’a des predicteurs numeriques.

Note : le sur-echantillonnage par simple replication des cibles peut deboucher sur du surapprentissage, alors que les methodes qui generent des cibles artificielles ajoutent un certain bruit et reduisent ce risque.

Algorithme ROSE

Cet algorithme cree des cibles supplementaires dans le voisinage de chaque cible reelle a partir d’une estimation par noyau de la distribution des autres cibles dans son voisinage.

bankData %>% count(y)

bankData_num = bankData %>% mutate(date = as.integer(date)) %>% recipe(formula = y ~ .) %>% 
  step_dummy(all_nominal(), - all_outcomes()) %>% step_rose(y) %>% prep %>% juice()

bankData_num %>% count(y)
    y     n
1  no 39922
2 yes  5289
# A tibble: 2 x 2
  y         n
  <fct> <int>
1 no    39967
2 yes   39877

Algorithme SMOTE

La methode SMOTE relie chaque cible reelle aux cibles les plus proches par des segments et elle choisit comme cibles artificielles supplementaires des points situes sur ces segments.

bankData_num = bankData %>% mutate(date = as.integer(date)) %>% recipe(formula = y ~ .) %>% 
  step_dummy(all_nominal(), - all_outcomes()) %>% step_smote(y) %>% prep %>% juice()

bankData_num %>% count(y)
# A tibble: 2 x 2
  y         n
  <fct> <int>
1 no    39922
2 yes   39922

retour au debut du document