Créer des tableaux descriptifs synthétiques avec R et les exporter vers Excel

, par Joël Girès

Dans cet article, je propose une méthode pour créer des tableaux descriptifs synthétiques avec R, et les exporter mis en forme vers Excel. Ces tableaux ont l’objectif de produire rapidement une vue lisible de la structuration d’un jeu de données et de préparer à un travail d’analyse plus poussé.

Ci-dessous, nous voyons à quoi ressemble le tableau produit par la procédure présentée. Il permet de calculer les valeurs de différents indicateurs choisis pour les différents groupes qui composent un échantillon issu d’une enquête - par exemple la proportion des individus qui ont un diplôme du supérieur ou l’âge moyen selon différentes lignes de partage : occupation, sexe, etc. :

1) Préparations

J’utilise dans la suite les données issues de l’enquête Histoire de vie - Construction des identités menée par l’INSEE. J’utilise ces données mises en formes de la manière décrite par un article précédent de ce site : vous devez donc au préalable suivre cette procédure pour avoir sous la main les données chargées dans un objet, qu’il faut nommer d pour que les opérations présentées ci-après puissent s’exécuter. Un fichier reprenant l’ensemble du script présenté dans cette page et les données utilisées est téléchargeable en fin d’article.

Par facilité d’écriture dans R, nous allons créer des fonctions pour les indicateurs que nous désirons calculer. Dans l’encadré suivant, je crée la fonction pourcentage qui calcule la proportion de la modalité y au sein de la variable x, et la fonction moyenne qui calcule la moyenne d’une variable numérique x, dont l’unité est y. Pour la proportion, j’utilise le package scale afin de formater le résultat en jolis pourcentages (22,1% au lieu de 0,221). Pour les deux fonctions, je définis le résultat pour qu’il comprenne une seule décimale après la virgule :

library(scales) # Package pour afficher de jolis pourcentages
 
# Je crée des fonctions pour calculer le pourcentage d'une modalité et la moyenne d'une variable
pourcentage <- function(x, y) {
  prop <- scales::percent((sum(x == y, na.rm = TRUE)/sum(!is.na(x))), accuracy = 0.1)
  return(prop)
}
 
moyenne <- function(x, y) {
  moy <- paste(round(mean(x, na.rm = TRUE), digits = 1),
               y
               )
  return(moy)
}

Ces fonctions permettent ainsi de calculer facilement le temps moyen que les individus enquêtés passent à regarder la télévision par jour et la proportion d’entre eux qui ont fait des études supérieures dans l’échantillon, par exemple. Il est bien sur possible de calculer toute une série d’autres indicateurs descriptifs : la médiane, l’écart-type...

moyenne(d$heures.tv, "heures")
[1] "2.6 heures"
 
pourcentage(d$nivetud, "Enseignement supérieur")
[1] "22.0%"

2) Premier calcul des indices (sans boucle)

Imaginons que nous voulons calculer la proportion de femmes, ainsi que celle des individus qui ont fait des études supérieures, qui ont un emploi, qui écoutent du rap, qui vont à la pêche ou la chasse, qui vont au cinéma et la moyenne d’âge et des heures quotidiennes passées à regarder la TV, selon les différents sous-groupes qui composent la qualification, le sexe ou l’âge. En outre, nous désirons savoir comment se ventilent les effectifs entre ces sous-groupes, ainsi que les pourcentages qu’ils représentent sur l’ensemble de l’échantillon. La choses est relativement facile à calculer avec un summarise couplée à un group_by (du package tidyverse), que nous répétons trois fois pour chacune des dimensions :

library(tidyverse)
# Je fais 3 summarise à la suite pour chacune des variables qualif, sexe et age_rec
tmp_qualif <- d %>%
  group_by(qualif) %>%  
  summarise(N = n(),
            Pourcentage = scales::percent((n()/nrow(d)), accuracy = 0.1),
            Femmes = pourcentage(sexe, "Femme"),
            "Âge" = moyenne(age, "ans"),
            "Etudes sup" = pourcentage(nivetud, "Enseignement supérieur"),
            Emploi = pourcentage(occup, "Exerce une profession"),
            "Rap / hip-hop" = pourcentage(rap.hiphop, "Ecoute du Rap"),
            "Pêche / chasse" = pourcentage(peche.chasse, "Va à la pêche/chasse"),
            "Cinéma" = pourcentage(cinema, "Va au cinéma"),
            "Heures de TV" = moyenne(heures.tv, "h"),
  )
 
tmp_sexe <- d %>%
  group_by(sexe) %>%  
  summarise(N = n(),
            Pourcentage = scales::percent((n()/nrow(d)), accuracy = 0.1),
            Femmes = pourcentage(sexe, "Femme"),
            "Âge" = moyenne(age, "ans"),
            "Etudes sup" = pourcentage(nivetud, "Enseignement supérieur"),
            Emploi = pourcentage(occup, "Exerce une profession"),
            "Rap / hip-hop" = pourcentage(rap.hiphop, "Ecoute du Rap"),
            "Pêche / chasse" = pourcentage(peche.chasse, "Va à la pêche/chasse"),
            "Cinéma" = pourcentage(cinema, "Va au cinéma"),
            "Heures de TV" = moyenne(heures.tv, "h"),
  )
 
tmp_age_rec <- d %>%
  group_by(age_rec) %>%  
  summarise(N = n(),
            Pourcentage = scales::percent((n()/nrow(d)), accuracy = 0.1),
            Femmes = pourcentage(sexe, "Femme"),
            "Âge" = moyenne(age, "ans"),
            "Etudes sup" = pourcentage(nivetud, "Enseignement supérieur"),
            Emploi = pourcentage(occup, "Exerce une profession"),
            "Rap / hip-hop" = pourcentage(rap.hiphop, "Ecoute du Rap"),
            "Pêche / chasse" = pourcentage(peche.chasse, "Va à la pêche/chasse"),
            "Cinéma" = pourcentage(cinema, "Va au cinéma"),
            "Heures de TV" = moyenne(heures.tv, "h"),
  )

Nous pouvons ensuite assembler à la suite les 3 résultats obtenus afin d’en produire une synthèse dans un seul dataframe. Pour cela, il nous faut renommer les colonnes de groupe (prenant des noms différents pour chacun des summarise : "qualif", "sexe" et "age_rec") avec un même nom (disons : "Groupe"). Nous pouvons alors joindre à la suite les lignes des 3 dataframes dans un seul avec un bind_rows :

# Je renomme la première colonne de chacun des 3 résultats
names(tmp_qualif)[1] <- "Groupe"
names(tmp_sexe)[1] <- "Groupe"
names(tmp_age_rec)[1] <- "Groupe"
 
# Je réunis les 3 résultats
synthese <- bind_rows (tmp_qualif,
                       tmp_sexe,
                       tmp_age_rec)

Voici le résultat :

La synthèse est déjà intéressante, mais il y a deux choses à améliorer :

  • Le fait d’avoir fusionné les 3 dataframes en un seul rend difficile le fait de savoir à quelle variable de groupe on se réfère (qualif, sexe ou age_rec ?). Nous pouvons le deviner par le nom du sous-groupe ("Cadre" ne renvoie évidemment pas à la dimension de l’âge, mais c’est moins évident pour les "NA") ;
  • En outre, le procédé qui a produit ces résultats n’est pas parcimonieux. Nous réalisons en effet trois fois les mêmes opérations, et si nous voulions changer l’un des indices calculés ou en ajouter un, nous devrions répéter la modification trois fois. Cet élément pourrait largement être optimisé.

3) Calcul final des indices (avec une boucle)

Pour optimiser la syntaxe, nous pouvons la réécrire avec une boucle. Pour cela, nous devons au préalable définir les dimensions qui alimenterons successivement cette boucle. Nous considérons comme précédemment la qualification, le sexe, l’âge, et nous ajoutons le niveau d’étude et l’occupation . Pour ce faire, nous stockons dans le vecteur groups le noms des différentes variables dans le jeu de données correspondant à chacune de ces dimensions :

# Je définis dans un vecteur les différentes variables pour lesquelles je veux faire tourner des summarise
groups <- c("qualif",
            "sexe",
            "age_rec",
            "nivetud",
            "occup")

Nous pouvons alors lancer une boucle pour le calcul des mêmes indices que nous avons listés précédemment. Cette boucle ne présente pas de difficulté particulière : elle tourne progressivement pour chacune des dimensions par le biais des valeurs comprise dans l’objet groups. Nous stockons les résultats dans un dataframe synthese, que nous devons créer au préalable pour que la boucle puisse l’alimenter. Une spécificité du code est qu’une nouvelle colonne Variable est créée, comprenant le nom de la variable de groupe associée à la ligne calculée. Le nom compris dans cette nouvelle colonne est ensuite intégré au début d’une ligne de séparation sep dont les cellules sont composées de ~~~. La ligne de séparation est au final ajoutée avant chaque résultat, pour obtenir une délimitation bien visible des différents groupes :

# je crée un dataframe/tibble vide
synthese <- tibble()
 
# Je lance une boucle de summarise sur chacune des variables définies dans le vecteur "groups"
for (i in seq_along(groups)) {
  tmp <- d %>%
    group_by_at(groups[[i]]) %>%  
    summarise(Variable = paste(groups[[i]]), # Le nom de la variable de groupe est affiché dans une nouvelle colonne
              N = n(),
              Pourcentage = scales::percent((n()/nrow(d)), accuracy = 0.1),
              Femmes = pourcentage(sexe, "Femme"),
              "Âge" = moyenne(age, "ans"),
              "Etudes sup" = pourcentage(nivetud, "Enseignement supérieur"),
              Emploi = pourcentage(occup, "Exerce une profession"),
              "Rap / hip-hop" = pourcentage(rap.hiphop, "Ecoute du Rap"),
              "Pêche / chasse" = pourcentage(peche.chasse, "Va à la pêche/chasse"),
              "Cinéma" = pourcentage(cinema, "Va au cinéma"),
              "Heures de TV" = moyenne(heures.tv, "h"),
    )
 
  names(tmp)[1] <- "Groupe"
  tmp$group <- as.character(tmp$group) # J'ajoute cette ligne pouvoir joindre les résultats quelle que soit la nature de la variable de groupe
  sep <- rep("~~~", ncol(tmp)) # Une ligne de séparation 'sep' est créée
  sep[1] <- str_to_upper(tmp[1,2]) # Le nom de la variable de groupe est collé en majuscule dans la première cellule de cette ligne de séparation.
  tmp <- rbind(sep, tmp) # La ligne de séparation est ajoutée avant chaque résultat, pour avoir la délimitation des différents groupes.
  tmp <- select(tmp, -Variable) # La colonne avec le nom de la variable de groupe est supprimée, ne servant plus à rien
  synthese <- bind_rows(synthese, tmp) # Les résultats sont progressivement ajoutés à un objet unique
}

La syntaxe est désormais bien plus parcimonieuse, et peut s’adapter à n’importe quelle variable de groupe. En outre, une ligne de séparation a été créée, permettant de bien séparer les cinq dimension au sein desquelles sont calculés les indicateurs : la qualification, le sexe, l’âge, le niveau d’étude et l’occupation  :

Nous pouvons maintenant exporter ces résultats dans un CSV (avec la fonction write.csv par exemple), et les ouvrir dans un tableur (Libre Office, Excel) pour les décortiquer tranquillement. La mise en forme est rudimentaire, mais elle peut être améliorée a posteriori dans le tableur afin que les résultats soient plus agréables à lire. Mais c’est une tâche qu’il est en réalité possible d’automatiser directement dans R afin d’exporter la synthèse dans un fichier Excel clé en main !

4) Mettre en forme les résultats pour Excel avec openxlsx

Il est en effet possible de créer un fichier Excel avec une mise en forme personnalisée et dépendante des données avec R ! Pour cela, nous utilisons l’excellent package openxlsx. Voici la logique à suivre : il faut d’abord créer un objet openxlsx dans R (nommé wb ci-dessous) dans lequel nous importons les données avec la fonction writeData :

library(openxlsx)
wb <- createWorkbook("WB_Object") # Je crée l'objet dans lequel on va formater toutes les infos en vue d'un export en fichier Excel
addWorksheet(wb, "Tableau synthétique") # Je lui ajoute une feuille que je nomme
writeData(wb, sheet = "Tableau synthétique", synthese, keepNA = TRUE, na.string = "NA") # J'écris les données issue de notre dataframe de synthèse dans cet objet en gardant les NA

Nous pouvons ensuite définir les styles qui seront appliqués aux différentes lignes et colonnes du tableau : celui de la ligne d’entête avec le nom des variables (hs), celui de la colonne avec le nom des sous-groupes (firstC) et celui des cellules de l’ensemble du tableau (body) :

# On crée différents styles qui seront appliqués par la suite à différentes lignes/colonnes
setColWidths(wb, "Tableau synthétique", width = 20, cols = 1:ncol(synthese)) # Largeur des colonnes
hs <- createStyle(fontColour = "#ffffff", fgFill = "#333333",  # Style de la première ligne
                  halign = "center", textDecoration = "Bold",
                  fontName = "Arial Narrow")
body <- createStyle (halign = "center") # Style des cellules du tableau
firstC <- createStyle (halign = "left", textDecoration = "Bold")  # Style de la première colonne

Il est également intéressant d’appliquer un formatage spécifique pour deux autres lignes :

  • Les lignes avec le séparateur ~~~, auxquelles je veux appliquer une couleur de fond jaune (title_group) ;
  • Les lignes avec les valeurs manquantes, dont je veux que les caractères soient en italique et en rouge (firstC_NA pour la colonne avec le nom des sous-groupes - qui sera également en gras - et OtherC_NA pour les autres cellules dans la ligne).
firstC_NA <- createStyle (halign = "left", textDecoration = c("Bold", "italic"), fontColour = "#c9211e")  # Style de la première colonne lorsque NA
OtherC_NA <- createStyle (halign = "center", fontColour = "#c9211e", textDecoration = "italic")  # Style des cellules du tableau lorsque NA
title_group <- createStyle (fgFill = "#FFC83D", textDecoration = "Bold")  # Style des lignes de séparation

Si l’entête avec le nom des variables et la colonne avec le nom des sous-groupes sont faciles à cibler (la première est toujours la première ligne, la seconde est toujours la première colonne), les lignes avec séparateurs et avec les valeurs manquantes sont plus difficiles à cibler, puisque leur position change selon les données. Nous créons ainsi deux objets row_lines_sep et row_lines_NA qui listent systématiquement ces lignes, quel que soit le jeu de donnée utilisé :

row_lines_sep = data.frame(which(synthese == "~~~", arr.ind=TRUE))
row_lines_NA = data.frame(which(is.na(synthese), arr.ind=TRUE))

Nous pouvons dès lors appliquer ces styles à l’objet wb en spécifiant les lignes et colonnes que ces styles doivent formater. C’est à ce moment-là que jouent les identifications des lignes avec séparateurs et avec valeurs manquantes, puisque certains des styles ne sont appliqués qu’aux lignes contenues dans les objets row_lines_sep ou row_lines_NA !

# On définit les styles selon les lignes/colonnes
addStyle(wb, "Tableau synthétique", hs, cols = 1:ncol(synthese), rows = 1, gridExpand = TRUE)
addStyle(wb, "Tableau synthétique", body, cols = 2:ncol(synthese), rows = 2:nrow(synthese)+1, gridExpand = TRUE)
addStyle(wb, "Tableau synthétique", firstC, cols = 1, rows = 1:nrow(synthese)+1, gridExpand = TRUE)
 
# Ici on applique les styles selon les lignes avec séparateurs et avec valeurs manquantes, contenues dans les objets row_lines_sep ou row_lines_NA !
addStyle(wb, "Tableau synthétique", title_group, cols = 1:ncol(synthese), rows = row_lines_sep[,1]+1, gridExpand = TRUE)
addStyle(wb, "Tableau synthétique", firstC_NA, cols = 1, rows = row_lines_NA[,1]+1, gridExpand = TRUE)
addStyle(wb, "Tableau synthétique", OtherC_NA, cols = 2:ncol(synthese), rows = row_lines_NA[,1]+1, gridExpand = TRUE)
 
freezePane(wb, "Tableau synthétique", firstCol = TRUE, firstRow = TRUE) # On fixe la première ligne et la première colonne

Avant d’exporter, nous supprimons les caractères ~~~ des lignes séparatrices, ces dernières étant maintenant clairement identifiable grâce à leur couleur jaune : nous réalisons cette opération avec la fonction gsub appliquée à tout le dataframe synthese grâce à un lapply, et nous réimportons les données dans le fichier wb avec writeData. Nous pouvons alors exporter le résultat final dans un fichier Excel formaté selon les styles définis avec la fonction saveWorkbook !

synthese[] <- lapply(synthese, gsub, pattern = "~~~", replacement = " ", fixed = TRUE)
writeData(wb, sheet = "Tableau synthétique", synthese, keepNA = TRUE, na.string = "NA")
saveWorkbook(wb, "synthese.xlsx", overwrite = TRUE)

Et voilà le résultat final ! Joli, non ? De mon côté, j’ai ajouté les intervalles de confiances pour chacune des proportions et des moyennes, afin d’avoir une idée des écarts significatifs (statistiquement parlant) entre les sous-groupes.