logo

L’utilité d’un factor expliquée à ma fille

Je trouve que ma fille a l’air sonnée. Qu’est-ce qui a pu la secouer ainsi ? Je l’interroge et elle me dit qu’elle a essayé de manipuler des factors sous R. Ce n’est d’ailleurs pas la première fois que ça la met dans cet état…

Le factor sonne toujours deux fois.

 

Commençons par créer deux petits jeux d’essai pour mieux saisir ce qu’est un factor dans R. Il s’agit des nombres d’élèves dans l’enseignement du 1er degré (maternelle + primaire) en France métropolitaine en 2017, par classe et par académie. Ces données sont reprises d’une publication de la Depp https://www.education.gouv.fr/cid57096/reperes-et-references-statistiques.html (tableaux 3.1 et 3.2).

eleves2017 <- data.frame(
  niveau=c("Très petite section", "Petite section", "Moyenne section", "Grande section",
           "CP", "CE1", "CE2", "CM1", "CM2"),
  nb=c(92.9, 788.1, 809.1, 832.3, 838.2, 847.3, 842.9, 845.8, 836.2)*1000
)
niveau nb
Très petite section 92900
Petite section 788100
Moyenne section 809100
Grande section 832300
CP 838200
CE1 847300
CE2 842900
CM1 845800
CM2 836200
acad2017 <- data.frame(
  academie=c("Clermont-Ferrand","Grenoble","Lyon","Besançon","Dijon","Rennes","Orléans-Tours",
             "Corse","Nancy-Metz","Reims","Strasbourg","Amiens","Lille","Créteil","Paris",
             "Versailles","Caen","Rouen","Bordeaux","Limoges","Poitiers","Montpellier",
             "Toulouse","Nantes","Aix-Marseille","Nice"),
  nb_deg1_public=c(100218,299618,292506,105361,132642,199696,228006,
                   24726,202186,115952,170107,183737,370032,486470,127280,
                   588058,112573,173910,272749,55553,141190,236122,
                   245309,254733,263702,181375)
)
academie nb_deg1_public
Clermont-Ferrand 100218
Grenoble 299618
Lyon 292506
Besançon 105361
Dijon 132642
Rennes 199696
Orléans-Tours 228006
Corse 24726
Nancy-Metz 202186
Reims 115952
Strasbourg 170107
Amiens 183737
Lille 370032
Créteil 486470
Paris 127280
Versailles 588058
Caen 112573
Rouen 173910
Bordeaux 272749
Limoges 55553
Poitiers 141190
Montpellier 236122
Toulouse 245309
Nantes 254733
Aix-Marseille 263702
Nice 181375

Qu’est-ce qu’un factor ?

A première vue, les colonnes niveau et académie sont de type caractère. Vérifions :

class(eleves2017$niveau)
## [1] "factor"
str(acad2017)
## 'data.frame':    26 obs. of  2 variables:
##  $ academie      : Factor w/ 26 levels "Aix-Marseille",..: 6 10 13 3 9 22 18 7 15 21 ...
##  $ nb_deg1_public: num  100218 299618 292506 105361 132642 ...

En fait ce sont des factors. C’est à dire que R les a stockées comme des entiers arbitraires (1, 2, 3, et ainsi de suite autant qu’il y aura de valeurs distinctes dans une colonne) puis habillées d’un affichage sous forme de textes. Les correspondances entre valeurs stockées et valeurs affichées sont visibles via la fonction levels. Nous n’avons pas demandé à créer ces factors. C’est le comportement par défaut de la fonction data.frame (de même que les fonctions d’import read.table, read.delim, etc.) ; il existe une option stringsAsFactors=FALSE pour créer de simples colonnes de type texte au lieu des factors.

levels(eleves2017$niveau)
## [1] "CE1"                 "CE2"                 "CM1"                
## [4] "CM2"                 "CP"                  "Grande section"     
## [7] "Moyenne section"     "Petite section"      "Très petite section"
levels(acad2017$academie)
##  [1] "Aix-Marseille"    "Amiens"           "Besançon"        
##  [4] "Bordeaux"         "Caen"             "Clermont-Ferrand"
##  [7] "Corse"            "Créteil"          "Dijon"           
## [10] "Grenoble"         "Lille"            "Limoges"         
## [13] "Lyon"             "Montpellier"      "Nancy-Metz"      
## [16] "Nantes"           "Nice"             "Orléans-Tours"   
## [19] "Paris"            "Poitiers"         "Reims"           
## [22] "Rennes"           "Rouen"            "Strasbourg"      
## [25] "Toulouse"         "Versailles"

Par défaut les niveaux sont attribués par ordre alphabétique des valeurs affichées.

Quels avantages propose un factor ?

Par rapport à une variable de type texte, le factor permet :

  • de gagner de la place, si les valeurs textes sont longues et peu nombreuses
  • de modifier l’ordre d’affichage des valeurs dans des tableaux et des graphiques
  • de choisir la modalité de référence dans un modèle statistique

Convertir un factor : méfiance !

Quand on veut convertir un factor en un vecteur d’un autre type, on utilise les fonctions habituelles : as.character, as.integer, as.numeric ou as.Date. Mais attention, car si la conversion en texte ne pose pas de souci, la conversion en numérique va utiliser les valeurs entières stockées par R.

# d'abord en texte --> aucun problème
as.character(eleves2017$niveau)
## [1] "Très petite section" "Petite section"      "Moyenne section"    
## [4] "Grande section"      "CP"                  "CE1"                
## [7] "CE2"                 "CM1"                 "CM2"
# puis en numérique --> on récupère les codes sous-jacents
as.numeric(eleves2017$niveau)
## [1] 9 8 7 6 5 1 2 3 4

Jusqu’ici, tout paraît assez prévisible. Les ennuis peuvent survenir si les valeurs affichées du factor ne sont composés que de chiffres (par exemple des numéros de département).

# départements bretons - en texte
bretagne <- c("22","29","35","56")

# convertis en factor
bretagne <- as.factor(bretagne)

# tout va bien
bretagne
## [1] 22 29 35 56
## Levels: 22 29 35 56
# convertis en texte --> OK
as.character(bretagne)
## [1] "22" "29" "35" "56"
# convertis en numérique --> non !!!
as.numeric(bretagne)
## [1] 1 2 3 4
# convertis correctement en numérique
as.numeric(as.character(bretagne))
## [1] 22 29 35 56

Factors et tableaux

Les niveaux des factors étant attribués par ordre alphabétique par défaut, c’est également l’ordre qui prévaudra dans un tableau.

library(tables)

tabular(Heading("Niveau") * niveau ~ Heading("Effectif") * nb * Heading() * sum, 
        eleves2017)
Niveau Effectif
CE1 847300
CE2 842900
CM1 845800
CM2 836200
CP 838200
Grande section 832300
Moyenne section 809100
Petite section 788100
Très petite section 92900

L’ordre utilisé ici n’a pas beaucoup de sens. Il est strictement alphabétique. Un factor ordonné (une variante du factor de base) permet de conserver un ordre dans les modalités.

eleves2017$niveau_2  <- ordered(eleves2017$niveau,
                               levels=c("Très petite section", "Petite section", "Moyenne section", "Grande section",
                                        "CP", "CE1", "CE2", "CM1", "CM2"))
str(eleves2017)
## 'data.frame':    9 obs. of  3 variables:
##  $ niveau  : Factor w/ 9 levels "CE1","CE2","CM1",..: 9 8 7 6 5 1 2 3 4
##  $ nb      : num  92900 788100 809100 832300 838200 ...
##  $ niveau_2: Ord.factor w/ 9 levels "Très petite section"<..: 1 2 3 4 5 6 7 8 9
levels(eleves2017$niveau_2)
## [1] "Très petite section" "Petite section"      "Moyenne section"    
## [4] "Grande section"      "CP"                  "CE1"                
## [7] "CE2"                 "CM1"                 "CM2"

Le tableau s’en trouve largement amélioré.

tabular(Heading("Niveau")*niveau_2 ~ Heading("Effectif")*nb * Heading()*sum, 
        eleves2017)
Niveau Effectif
Très petite section 92900
Petite section 788100
Moyenne section 809100
Grande section 832300
CP 838200
CE1 847300
CE2 842900
CM1 845800
CM2 836200

Avec un autre package de construction de tableaux, la problématique est la même.

library(flextable)
library(magrittr)
library(reshape2)

dcast(eleves2017, 
      niveau ~ ., 
      value.var = "nb", 
      fun.aggregate=sum) %>% 
flextable() %>% 
delete_part("header") %>% 
  add_header(niveau="Niveau","."="Effectifs") %>% 
  colformat_int(col_keys=".", big.mark=" ")

Niveau

Effectifs

CE1

847 300

CE2

842 900

CM1

845 800

CM2

836 200

CP

838 200

Grande section

832 300

Moyenne section

809 100

Petite section

788 100

Très petite section

92 900

Beaucoup plus intéressant réordonné avec la colonne niveau_2.

dcast(eleves2017, 
      niveau_2 ~ ., 
      value.var = "nb", 
      fun.aggregate=sum) %>% 
  flextable() %>% 
  delete_part("header") %>% 
  add_header(niveau_2="Niveau","."="Effectifs") %>% 
  colformat_int(col_keys=".", big.mark=" ")

Niveau

Effectifs

Très petite section

92 900

Petite section

788 100

Moyenne section

809 100

Grande section

832 300

CP

838 200

CE1

847 300

CE2

842 900

CM1

845 800

CM2

836 200

Factors et graphiques

Quand on produit un graphique, particulièrement un diagramme en bâtons, l’ordre des barres peut être a) lié au sens métier des données ou b) tel que les barres sont par ordre croissant de longueur. On constate que l’ordre alphabétique est sans intérêt.

library(ggplot2)
library(forcats)
ggplot(acad2017) +
  aes(x=academie, y=nb_deg1_public) +
  geom_bar(stat = "identity") +
  theme_light() +
  xlab("Académie") + 
  ylab("Effectif d'élèves du 1er degré, secteur public") + 
  coord_flip() +
  scale_y_continuous(labels=scales::number)

Le package {forcats} propose de nombreuses fonctions pour manipuler les factors : les réordonner selon une statistique avec fct_reorder…

ggplot(acad2017) +
  aes(x=fct_reorder(academie, nb_deg1_public, sum),
      y=nb_deg1_public) +
  geom_bar(stat = "identity") +
  theme_light() +
  xlab("Académie") + 
  ylab("Effectif d'élèves du 1er degré, secteur public") +
  coord_flip() +
  scale_y_continuous(labels=scales::number)

… ou modifier leurs libellés avec fct_recode. Ici on propose le même libellé pour plusieurs niveaux, ce qui a pour conséquence de les fusionner.

eleves2017$niveau_3 <- fct_recode(eleves2017$niveau,
                                  "maternelle"="Très petite section",
                                  "maternelle"="Petite section",
                                  "maternelle"="Moyenne section",
                                  "maternelle"="Grande section",
                                  "primaire"="CP","primaire"="CE1","primaire"="CE2",
                                  "primaire"="CM1","primaire"="CM2")
ggplot(eleves2017) +
  aes(x=niveau_3, y=nb) +
  geom_bar(stat = "summary", fun.y="sum") +
  theme_light() +
  xlab("niveau agrégé") + 
  ylab("nombre d'élèves en 2017") +
  coord_flip() +
  scale_y_continuous(labels=scales::number)

Factors et modélisation statistique

Enfin, l’ordre des modalités d’une variable texte ou factor est utilisé dans R lors de la construction de modèles statistiques et particulièrement de régressions. Pour l’illustrer, nous utiliserons les données de prix de location des maisons AirBnB sur Paris (une partie des données qui servent aux exemples du livre Le langage R au quotidien chez Dunod).

maisons <- read.delim(url("https://github.com/olivierDecourt/livreR/blob/master/houses.csv?raw=true"),
                      sep=",")

Parmi les multiples informations concernant ces logements, il y a la politique d’annulation.

levels(maisons$cancellation_policy)
## [1] "flexible"        "moderate"        "strict"          "super_strict_30"
## [5] "super_strict_60"

Si on réalise une régression linéaire expliquant le prix par le nombre de personnes accueillies et la politique d’annulation, les coefficients se présentent d’abord ainsi.

library(broom) # affichage des coefficients du modèle sous forme de tibble
library(pander) # affichage markdown de tableaux et tibbles
reg <- lm(price ~ accommodates + cancellation_policy, maisons)
pander(tidy(reg), split.tables=Inf)
term estimate std.error statistic p.value
(Intercept) -18 12.79 -1.407 0.1598
accommodates 41.53 2.2 18.88 2.141e-63
cancellation_policymoderate -33.3 15.02 -2.218 0.02693
cancellation_policystrict 3.002 13.85 0.2167 0.8285
cancellation_policysuper_strict_30 231.1 73.27 3.155 0.001682
cancellation_policysuper_strict_60 1806 61.08 29.56 2.422e-121

Pour le coefficient du nombre de personnes accueillies, pas de souci : chaque place supplémentaire correspond à une hausse du prix de 41,53€ en moyenne. Pour les coefficients correspondant à la politique d’annulation, ils sont construits par rapport à une situation de référence : il s’agit de la 1e modalité du factor. Ici, c’est par rapport à la politique “flexible”. Par exemple, une politique d’annulation modérée entraîne une baisse de 33€ en moyenne par rapport à une politique flexible – à nombre de personnes accueillies identique. Si on souhaite modifier cette référence (et donc se comparer à une autre politique d’annulation), on utilisera la fonction relevel (du package {stats} préinstallé et préactivé).

maisons$cancellation_policy <- relevel(maisons$cancellation_policy,
                                       "strict")
reg2 <- lm(price ~ accommodates + cancellation_policy, maisons)
pander(tidy(reg2), split.tables=Inf)
term estimate std.error statistic p.value
(Intercept) -15 14.43 -1.04 0.2989
accommodates 41.53 2.2 18.88 2.141e-63
cancellation_policyflexible -3.002 13.85 -0.2167 0.8285
cancellation_policymoderate -36.3 14.65 -2.477 0.01349
cancellation_policysuper_strict_30 228.1 73.22 3.116 0.001917
cancellation_policysuper_strict_60 1803 60.52 29.79 1.535e-122

On constate que la constante et les coefficients de la politique d’annulation ont changé, ce qui est logique. En revanche le coefficient du nombre de personnes accueillies n’est pas modifié. Maintenant la situation de référence est la politique d’annulation dite “stricte”.

9 found this helpful