forcats, forcats, vous avez dit forcats ?

La « dream team » du tidyverse se compose de 6 packages, qui se lancent avec library(tidyverse) : {ggplot2}, {dplyr}, {tidyr}, {reader}, {purrr} et {tibble}. Pourtant, si on lit dans les petites lignes, le tidyverse regorge d’autres outils incontournables. Parmi eux : {forcats}, sujet de notre billet d’aujourd’hui.

À ne pas confondre avec {forecast} (pour travailler avec des séries temporelles), {forcats} se destine à une gestion simplifiée des facteurs, autrement dit des valeurs catégorielles… « a package [for cat]egorical variables ». (C’est bon, vous l’avez ? 😉 ). Sans oublier : il s’agit d’une anagramme de factors !

R et les facteurs

Bon, commençons par rappeler rapidement ce qu’est un facteur : il s’agit d’un type de variable qui peut prendre comme valeur un nombre fini de modalités. Autrement dit, une variable est considérée comme facteur lorsqu’elle ne peut pas être mesurée directement par un nombre continu : par exemple, une couleur, un métier, une ville, un statut marital… Un facteur peut être ordonné (non satisfait, moyennement satisfait, satisfait, très satisfait) ou non (Paris, Rennes, Montpellier).

L’usage des facteurs en statistiques n’est pas d’hier : vous vous en doutez donc, son utilisation dans R date également. C’est d’ailleurs le comportement historique de R : traiter les chaînes de caractères comme des facteurs, avec l’option stringsAsFactors, câblée sur TRUE par défaut. Car oui, « back in the days », ce comportement faisait sens : en statistiques, les strings sont majoritairement des facteurs. Si nous devons creuser un peu plus, nous pouvons aussi nous rappeler que, en background, R transforme les facteurs en représentations numériques : par exemple, « marié / non marié » sera converti en 1 et 2. Ce qui, en bout de course, fait gagner de la place, les chiffres demandant moins de bytes que les chaînes de caractères :

object.size(1)
48 bytes
object.size("une_modalite_encode_avec_un_nom_super_long")
136 bytes

Un comportement qui en fait aujourd’hui bondir plus d’un, et ne manque pas de faire couler de l’encre, tant dans la littérature que sur les blogs et sur Twitter. Mais bref, nous ne sommes pas ici pour relancer le débat, et avouons le, en stats, les facteurs sont indispensables. Alors, comment les manipuler dans le tidyverse ?

forcats pas-à-pas

# Si besoin 
# install.packages("forcats") 
# ou, pour être plus tranquille :
# install.packages("tidyverse")
library(forcats)
library(tidyverse)

Jouons d’abord avec le jeu de données storms, disponible dans {dplyr}. Nativement, les datasets de ce package sont des tibbles, et les colonnes contenant des strings sont des caractères. C’est le cas par exemple de status.

storms %>%
  count(status) 
# A tibble: 3 x 2
               status     n
                <chr> <int>
1           hurricane  3091
2 tropical depression  2545
3      tropical storm  4374

On voit ici qu’il s’agit d’une variable factorielle. Bien, « time to change »!

as_factor

Première fonction du package, as_factor, utilisée pour transformer un vecteur en facteur. Quelle différence avec {base} ? La version {forcats} créer les niveaux du facteur en fonction de leur apparition dans le jeu de données, à l’inverse de {base}, qui trie en fonction de la configuration de votre locale (et donc, peut donner lieu à des différences d’une machine à l’autre).

Par exemple :

x <- c("a", "z", "g")
as_factor(x)
[1] a z g
Levels: a z g

as.factor(x)
[1] a z g
Levels: a g z

Ici, on voit bien que le premier imprime un ordre a z g, là où le second affiche a g z, en fonction de l’ordre alphabétique (qui n’est pas le même partout, rappelons-le).

storms <- storms %>%
  mutate(status = as_factor(status))

Bon, nous n’avons pas assez de modalités (3), pour pouvoir vraiment manipuler des facteurs… Testons plutôt le jeu de données natif de {forcats}, gss_cat, contenant un sample du General Social Survey.

fct_anon

Si, par exemple, vous bossez dans la recherche médicale, l’anonymisation est primordiale (on vous en parlait déjà ici : forcément, il y a une fonction pour ça !

gss_cat %>%
  mutate(relig = fct_anon(relig)) %>%
  group_by(relig) %>%
  count

# A tibble: 15 x 2
# Groups:   relig [15]
    relig     n
   <fctr> <int>
 1     02    93
 2     03   689
 3     04    15
 4     05    71
 5     06  5124
 6     07   224
 7     08    32
 8     09 10846
 9     10   104
10     11  3523
11     12   109
12     13   388
13     14    95
14     15    23
15     16   147

fct_c

Vous avez utilisé R plus d’une journée, vous connaissez forcément l’opérateur c, indispensable pour concaténer des vecteurs les uns avec les autres. L’équivalent dans {forcats} est fct_c, et vous permet de concaténer les facteurs de la même manière, là où {base} vous renvoie la version « integer » de vos facteurs :

fac_a <- factor(c("a", "b"))
fac_b <- factor(c("c", "d"))
c(fac_a, fac_b)
[1] 1 2 1 2
fct_c(fac_a, fac_b)
[1] a b c d
Levels: a b c d

 

fct_collapse

Cette fonction transforme les niveaux d’un facteur, soit pour les renommer soit pour les regrouper :

gss_cat %>%
  mutate(relig = fct_collapse(relig, 
                               no_answer = c("No answer", "Don't know", "Not applicable", "None"))) %>%
  group_by(relig) %>%
  count

# A tibble: 13 x 2
# Groups:   relig [13]
                     relig     n
                    <fctr> <int>
 1               no_answer  3631
 2 Inter-nondenominational   109
 3         Native american    23
 4               Christian   689
 5      Orthodox-christian    95
 6            Moslem/islam   104
 7           Other eastern    32
 8                Hinduism    71
 9                Buddhism   147
10                   Other   224
11                  Jewish   388
12                Catholic  5124
13              Protestant 10846

fct_count

Une fonction pour compter les entrées dans un facteur. Un équivalent à table de {base} :

gss_cat %>% 
  pull(marital) %>%
  fct_count()

# A tibble: 6 x 2
              f     n
         <fctr> <int>
1     No answer    17
2 Never married  5416
3     Separated   743
4      Divorced  3383
5       Widowed  1807
6       Married 10117

fct_drop

Une fonction qui est là pour vous débarrasser des niveaux de facteurs inutilisés. La différence avec droplevels ? Les valeurs NA ne sont pas retirée ! Vous pouvez aussi sélectionner les modalités à faire disparaître.

gss_status <- factor(gss_cat$marital, 
                        levels =c("No answer", "Never married", "Separated", "Divorced","Widowed", "Married", "plop", "ploum"))
gss_status %>% fct_count()

# A tibble: 8 x 2
              f     n
         <fctr> <int>
1     No answer    17
2 Never married  5416
3     Separated   743
4      Divorced  3383
5       Widowed  1807
6       Married 10117
7          plop     0
8         ploum     0

gss_status %>% fct_drop() %>% fct_count()

# A tibble: 6 x 2
              f     n
         <fctr> <int>
1     No answer    17
2 Never married  5416
3     Separated   743
4      Divorced  3383
5       Widowed  1807
6       Married 10117

# Avec only, vous pouvez ne droper que certains facteurs

gss_status %>% fct_drop(only = "plop") %>% fct_count()

# A tibble: 7 x 2
              f     n
         <fctr> <int>
1     No answer    17
2 Never married  5416
3     Separated   743
4      Divorced  3383
5       Widowed  1807
6       Married 10117
7         ploum     0

fct_expand

Vous avez besoin d’ajouter de nouveaux niveaux à votre facteur ? Direction fct_expand (oui oui, exactement ce que nous venons de faire juste au-dessus) :

gss_cat %>%
  pull(marital) %>%
  fct_expand("plop", "ploum") %>%
  fct_count()

# A tibble: 8 x 2
              f     n
         <fctr> <int>
1     No answer    17
2 Never married  5416
3     Separated   743
4      Divorced  3383
5       Widowed  1807
6       Married 10117
7          plop     0
8         ploum     0

fct_explicit_na

Cette fonction vous permet de rendre les NA explicites, autrement dit de les faire apparaître dans votre jeu de données. Ce que ne fait pas table, qui par défaut n’affiche pas les NA : explicit_NA les transforme en (Missing), afin de les faire ressortir dans le classement.

gss_cat %>%
  pull(marital) %>%
  fct_c(factor(x = c(NA,NA,NA))) %>%
  table()
    No answer Never married     Separated      Divorced       Widowed 
           17          5416           743          3383          1807 
      Married 
        10117 
        
gss_cat %>%
  pull(marital) %>%
  fct_c(factor(x = c(NA,NA,NA))) %>%
  fct_explicit_na() %>%
  table()
    No answer Never married     Separated      Divorced       Widowed 
           17          5416           743          3383          1807 
      Married     (Missing) 
        10117             3 

(Missing) est la valeur par défaut donnée aux données manquantes, mais rassurez-vous : vous pouvez modifier leur petit nom :

gss_cat %>%
  pull(marital) %>%
  fct_c(factor(x = c(NA,NA,NA))) %>%
  fct_explicit_na(na_level = "(Pauvres petites NA) ") %>%
  table()

           No answer         Never married             Separated 
                   17                  5416                   743 
             Divorced               Widowed               Married 
                 3383                  1807                 10117 
(Pauvres petites NA)  
                    3  

fct_infreq et fct_inorder

Ces deux fonctions sont là pour réordonner les facteurs selon leur fréquence, ou en fonction de leur apparition dans le jeu de données. Le paramètre ordered indique si oui ou non, les facteurs sont… ordonnés (unbielievable, n’est-il pas ? 😉 ).

gss_cat %>%
  pull(marital)
...
Levels: No answer Never married Separated Divorced Widowed Married

gss_cat %>%
  pull(marital) %>% 
  fct_infreq()
...
Levels: Married Never married Divorced Widowed Separated No answer

gss_cat %>%
  pull(marital) %>% 
  fct_inorder(ordered = TRUE)
...
Levels: Never married < Divorced < Widowed < Married < Separated < No answer

fct_lump

Vous avez besoin de grouper les facteurs les plus communs ? Les plus rares ? Il y a une fonction pour ça ! Laissez-nous vous présenter fct_lump :

# Avec n positif, la fonction conserve les n facteurs les plus courants
gss_cat %>%
  pull(relig) %>%
  fct_lump(n = 2) %>% 
  levels()
[1] "Catholic"   "Protestant" "Other"     

# Avec n négatif, la fonction conserve les n facteurs les moins courants
gss_cat %>%
  pull(relig) %>%
  fct_lump(n = -2) %>% 
  levels()
[1] "Don't know" "Not applicable" "Other"     

# Avec prop, vous conservez les valeurs qui apparaissent au moins "prop of the time", c'est-à-dire que prop = 0.2 conserve les facteurs qui représentent au moins 20% de votre vecteur.
gss_cat %>%
  pull(relig) %>%
  fct_lump(prop = 0.05) %>% 
  levels()
[1] "None" "Catholic"   "Protestant" "Other"  

# Avec un prop négatif, on définit une marge maximale d'apparition. À noter que vous pouvez définir un "Other" à votre gout avec other_level
gss_cat %>%
  pull(relig) %>%
  fct_lump(prop = -0.01, other_level = "les_autres") %>% 
  levels()
 [1] "No answer"               "Don't know"              "Inter-nondenominational"
 [4] "Native american"         "Orthodox-christian"      "Moslem/islam"           
 [7] "Other eastern"           "Hinduism"                "Buddhism"               
[10] "Not applicable"          "les_autres"   

fct_other

Vous avez envie de remplacer à la mano certaines modalités ? Go fct_other ! Cette fonction vous permet soit de garder une liste de niveaux, soit de droper. Les autres deviennent « other ».

gss_cat %>%
  pull(marital) %>% 
  fct_other(keep = "Married") %>%
  levels()
[1] "Married" "Other" 

gss_cat %>%
  pull(marital) %>% 
  fct_other(drop = c("No answer","Never married")) %>%
  levels()
[1] "Separated" "Divorced"  "Widowed"   "Married"   "Other"

fct_recode

Aller, changer le niveau des facteurs à la main, ça vous branche ?

gss_cat %>%
  pull(marital) %>%
  fct_recode(mar = "Married", sep = "Separated", wid = "Widowed", no_a = "No answer", nev = "Never married", div = "Divorced") %>%
  levels()
[1] "no_a" "nev"  "sep"  "div"  "wid"  "mar" 

fct_relabel

Pour modifier le nom des niveaux de manière globale, fct_relabel est là pour vous : en spécifiant une fonction qui agit sur une chaîne de caractères, tous vos labels sont affectés d’un coup ! Par exemple, mettons les noms en minuscule, et transformons les espaces en « _ » :

gss_cat %>%
  pull(marital) %>%
  fct_relabel(fun = tolower) %>%
  fct_relabel(fun = stringr::str_replace_all, " ", "_") %>%
  levels()
[1] "no_answer"     "never_married" "separated"     "divorced"      "widowed"      
[6] "married" 

fct_relevel

Pour trier les facteurs à la main, plutôt que par ordre ou fréquence, comme nous avons vu plus haut. Si vous connaissez la version de {stats} (relevel), il s’agit là d’une généralisation, utilisable avec plus d’un facteur.

gss_cat %>%
  pull(marital) %>%
  fct_relevel(c("Never married", "Married", "Separated", "Divorced", "Widowed", "No answer")) %>%
  levels()
[1] "Never married" "Married"       "Separated"     "Divorced"      "Widowed"      
[6] "No answer"  

fct_reorder et fct_reorder2

Houla, pourquoi deux fonctions pour réordonner des niveaux ? La première est utilisée lors d’une représentation graphique en 1 dimension, la seconde en 2D (eh oui, tout simplement). Imaginons que nous souhaitions visualiser le nombre moyen d’heures devant la télévision par statuts maritaux :

library(ggplot2)

gss_tv_hours <- gss_cat %>%
  group_by(marital) %>%
  summarise(age = mean(age, na.rm = TRUE),
    tvhours = round(mean(tvhours, na.rm = TRUE), 1))

gss_tv_hours %>%
  ggplot(aes(tvhours, marital)) + 
  geom_point()

Eh oui, ici y est ordonné en fonction du facteur marital. Ce que l’on veut, c’est qu’il soit ordonné par tvhours.

gss_tv_hours %>%
  ggplot(aes(tvhours, fct_reorder(marital, tvhours))) + 
  geom_point()

fct_reorder_2, vous sera utile lorsque vous avez besoin de mapper des couleurs sur une ligne : la légende se trouvera alignée à « l’arrivée » de vos lignes, sur la droite. Comment ça marche ? Les facteurs sont réordonnés avec la valeur y associée à la valeur la plus forte de x.

age_relig <- gss_cat %>%
  filter(!is.na(age)) %>%
  group_by(age, relig) %>%
  count() %>%
  ungroup() %>%
  mutate(prop = n / sum(n))

ggplot(age_relig, aes(age, prop, colour = relig)) +
  geom_line() 

Le problème ici ? Oui, la légende n’est pas dans le même ordre que les lignes… Eh bien, accueillez à bras ouverts fct_reorder2 !

ggplot(age_relig, aes(age, prop, colour = fct_reorder2(relig, age, prop))) +
  geom_line() +
  labs(colour = "Religion")

fct_rev

Inverse l’ordre des facteurs… tout est dans le nom 😉

fct_shift

Décale les facteurs de n crans vers la gauche (et les premiers vont à la fin), ou vers la droite si n est négatif. Une fonction qui peut être utile sur des données cycliques, par exemple avec des dates ! Alors, si vous n’aimez pas la façon dont R ordonne les jours de la semaine :

library(lubridate)
w <- seq(from = ymd("2017-01-01"), 
         to = ymd("2017-01-31"), 
         by = 1) %>%
  wday(label = TRUE)
levels(w)
[1] "Sun"   "Mon"   "Tues"  "Wed"   "Thurs" "Fri"   "Sat"  

Eh oui, ici la semaine commence le dimanche… Alors, comment changer ça ? Vous nous avez vus venir : avec fct_shift

fct_shift(w, 1) %>% levels()
[1] "Mon"   "Tues"  "Wed"   "Thurs" "Fri"   "Sat"   "Sun" 

fct_shuffle

Mélange de manière aléatoire les niveaux.

fct_shuffle(w) %>% levels()
[1] "Fri"   "Tues"  "Sat"   "Thurs" "Mon"   "Wed"   "Sun" 

fct_unify

Vous avez une liste de facteurs sous la main ? Pour les appliquer à tous les éléments, utilisez fct_unify :

plop <- list(w, factor(letters[1:5]))
fct_unify(plop)

[[1]]
 [1] Sun   Mon   Tues  Wed   Thurs Fri   Sat   Sun   Mon   Tues  Wed   Thurs Fri  
[14] Sat   Sun   Mon   Tues  Wed   Thurs Fri   Sat   Sun   Mon   Tues  Wed   Thurs
[27] Fri   Sat   Sun   Mon   Tues 
Levels: Sun < Mon < Tues < Wed < Thurs < Fri < Sat < a < b < c < d < e

[[2]]
[1] a b c d e
Levels: Sun Mon Tues Wed Thurs Fri Sat a b c d e

fct_unique

Enfin, dernier membre de la famille de fct_*, cette fonction permet de renvoyer… les niveaux uniques d’un facteur. La différence avec base::unique ? fct_unique renvoie les valeurs dans l’ordre de leur niveau, pas dans l’ordre de leur apparition.

unique(gss_cat$marital)
[1] Never married Divorced      Widowed       Married       Separated     No answer    
Levels: No answer Never married Separated Divorced Widowed Married

fct_unique(gss_cat$marital)
[1] No answer     Never married Separated     Divorced      Widowed       Married      
Levels: No answer Never married Separated Divorced Widowed Married

lvls_expand, lvls_reorder, lvls_revalue et lvls_union

Ces fonctions sont là pour vous travailler à plus bas niveau : dans la pratique, vous aurez plutôt besoin des fonctions que nous venons de voir plus haut. Mais bon, mettons-les ici quand même !

f <- factor(c("a", "b", "c"))
# Réordonner par position 
lvls_reorder(gss_cat$marital, 6:1) %>% levels()
[1] "Married"       "Widowed"       "Divorced"      "Separated"     "Never married"
[6] "No answer"   
# Réordonner par valeur
lvls_revalue(gss_cat$marital,c("Widowed", "Divorced","Separated","Never married", "No answer" , "Married")) %>% levels()
[1] "Widowed"       "Divorced"      "Separated"     "Never married" "No answer"    
[6] "Married"   
# Ajouter un nouveau facteur
lvls_expand(gss_cat$marital, c("Widowed", "Divorced","Separated","Never married", "No answer" , "Married", "Another level", "And another one")) %>% levels()
[1] "Widowed"         "Divorced"        "Separated"       "Never married"   "No answer"      
[6] "Married"         "Another level"   "And another one"

Et voilà, ce n’était pas de tout repos, mais nous avons parcouru tout {forcats} ! Maintenant, à vous de jouer 😉


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker


Commentaires


À lire également