Génération et envoi automatique de supports à destination des clients, points de blocages et solutions pour avancer

Article de la présentation qui aurait dû se faire pour l'International Women's Day à Nantes le 19 mars et qui a été annulée suite à l'épidémie de COVID-19

Marie Vaugoyeau

13 minutes

Objectif

Le but est de réaliser un support de communication à destination des clients pour voir l’évolution de leurs résultats par rapport aux données des autres clients de même catégorie (Benchmark).
Ce support devait être généré et envoyé automatiquement aux clients.

Afin que les personnes qui le souhaite puissent réutiliser ces lignes de codes, le template utilisé est ici, les données sont celles des Pingouins du package FlexParamCurve de Stephen OSWALD et de LifeCycleSavings du package datasets. Les exemples de supports éditées à la fin de l’article sont disponibles ici et .

Déroulé du travail :

_ Réalisation d’un template sous Power Point
_ Création des graphiques à insérer
_ Création du support Power Point en utilisant le package officer de David Gohel
_ Envoi du support avec sendmailR d’Olaf Mersmann

Création de graphiques sur les données

Lorsque j’ai réalisé ce travail c’était sur des données clients donc confidentielles. Pour cet article de blog je vais utiliser les données de masse des pingouins en fonction de leurs âges et les comparer aux données moyennes des autres pingouins nés dans le même ordre et la même année pour montrer comment réaliser un graphique avec un benchmarck. Je vais aussi réaliser un autre graphique sur les données internationnales du revenu réel disponible par habitant.

Comparaison d’un pingouin par rapport aux autres

Comparer un pingouin par rapport aux autres de la même année et du même ordre de naissance est facile grâce à ggplot.

library(tidyverse)
library(FlexParamCurve) # pour avoir les données sur les pingouins

creation_du_graphique_pour_comparer_un_pingouin_aux_autres <- function(num_identifiant_pingouin){
  
  benchmarck <- penguin.data %>% 
    filter(
      bandid != num_identifiant_pingouin,
      year == (penguin.data %>% filter(bandid == num_identifiant_pingouin) %>% slice(1))$year,
      ck == (penguin.data %>% filter(bandid == num_identifiant_pingouin) %>% slice(1))$ck
    ) # le benchmarck se fait aves les données des autres pingouins nées la même année et même ordre de ponte
  
  penguin.data %>% 
    filter(
      bandid == num_identifiant_pingouin
    ) %>% 
    ggplot() +
    aes(
      x = ckage,
      y = weight
    ) +
    geom_smooth(data = benchmarck, color = "red", fill = "orange", alpha = 0.4, formula = y ~ x^2) + 
    geom_point(color = "black") + 
    geom_smooth(se = FALSE, formula = y ~ x^2, color = "black") +
    theme_classic() +
    ggtitle(glue::glue("Pingouin '{num_identifiant_pingouin}' comparer à ses semblables (en rouge)"))
  
 }

"-0.993421052631561 422" %>% 
  creation_du_graphique_pour_comparer_un_pingouin_aux_autres()

"-0.993421052631561 308" %>% 
  creation_du_graphique_pour_comparer_un_pingouin_aux_autres()

Création d’une fonction pour voir les données sous format “français”

De base, R présente les données numérique sans espace ni arrondi mais de temps en temps dans des graphiques, il peut-être intéressant de pouvoir afficher les chiffres sous le format “français” c’est-à-dire 123 456 789 comme Excel peut le faire nativement.

Pour cet exemple, je vais utiliser les données de LifeCycleSavings.

# library(tidyverse) # pas utile si déjà appelé précédement
library(scales) # pour utiliser la fonction label_number de mise en forme des nombres
library(ggrepel) # pour ajouter des valeurs non chevauchantes sur le graphique

mise_en_forme_fr <- label_number(accurancy = 1)
# je demande à ce qu'il n'y ai pas de décimale

# ça fonctionne sans soucis
1332859475.645878 %>% mise_en_forme_fr()
## [1] "1 332 859 476"
LifeCycleSavings %>% 
  rownames_to_column("country") %>% 
  mutate(label = dpi %>% mise_en_forme_fr()) %>% 
  ggplot() +
  aes(x = country, y = dpi) +
  geom_segment(aes(xend = country, y = 0, yend = dpi), size = 6) +
  geom_label_repel(aes(label = label), segment.color = NA, direction="y", size = 3) +
  theme_classic() +
  theme(
    axis.text.x = element_text(angle = 90)
  )

# aïe l'arrondi ne se fait pas

Vous pouvez voir que malgré mon test préliminaire, cela ne fonctionne pas bien…
En effet, si la mise en forme globale est bonne l’arrondi ne se fait pas… Mais pourquoi ???

Je suis restée bloquée un petit moment à me demander ce qui n’allait pas et pourquoi et j’ai fini par demander de l’aide sur la slack francophone des utilisateurs de R, GRRR.
Petit point info : Quelque soit le support sur lequel vous voulez demander de l’aide, pensez à réaliser un repex, un exemple reproductible !
Oui les gens sur les forums ou slacks sont vos ami-e-s et oui, la sphère R est bienveillante mais n’oubliez pas que personne n’est dans votre tête (à part vous, enfin normalement), donc le premier pas vers la résolution d’un problème est de le sortir de son contexte pour pouvoir l’expliquer à tous en utilisant des données accessibles !
Cela m’est déjà arrivé plus d’une fois qu’en réalisant le repex pour demander de l’aide, j’ai vu et corrigé mon erreur.

Cette fois_ci cela n’a pas fonctionné, donc voici les lignes de codes que j’ai partagée sur la slack.

# library(scales)
# library(tidyverse)

mise_en_forme_fr <- label_number(accurancy = 1)

c(13556.4646, 546946.65465) %>% map(mise_en_forme_fr)
## [[1]]
## [1] "13 556"
## 
## [[2]]
## [1] "546 947"
tibble(
  valeur = 
    runif(n = 50, min = 100, max = 6000)
  ) %>% 
  mutate(mise_en_forme = valeur %>% mise_en_forme_fr)
## # A tibble: 50 x 2
##    valeur mise_en_forme
##     <dbl> <chr>        
##  1  1880. 1 880.0      
##  2  2015. 2 014.5      
##  3  3134. 3 134.3      
##  4  1015. 1 015.0      
##  5  2796. 2 796.2      
##  6  5718. 5 718.1      
##  7  4910. 4 910.0      
##  8  5250. 5 250.0      
##  9  1740. 1 739.8      
## 10  2101. 2 101.0      
## # ... with 40 more rows

Cette fois-là c’est Julien Barnier qui m’a dépanné en me faisant remarquer que j’avais fait une faute à accuracy, il n’y a pas de ‘n’…
Bizarrement ça a tout de suite mieux fonctionné.

mise_en_forme_fr <- label_number(accuracy = 1)

tibble(
  valeur = 
    runif(n = 50, min = 100, max = 6000)
  ) %>% 
  mutate(mise_en_forme = valeur %>% mise_en_forme_fr)
## # A tibble: 50 x 2
##    valeur mise_en_forme
##     <dbl> <chr>        
##  1  4658. 4 658        
##  2   994. 994          
##  3  5676. 5 676        
##  4  4622. 4 622        
##  5  1136. 1 136        
##  6  2786. 2 786        
##  7  5686. 5 686        
##  8  4436. 4 436        
##  9   851. 851          
## 10  5139. 5 139        
## # ... with 40 more rows
graphique_LCS <- LifeCycleSavings %>% 
  rownames_to_column("country") %>% 
  mutate(label = dpi %>% mise_en_forme_fr()) %>% 
  ggplot() +
  aes(x = country, y = dpi) +
  geom_segment(aes(xend = country, y = 0, yend = dpi), size = 6) +
  geom_label_repel(aes(label = label), segment.color = NA, direction="y", size = 3) +
  theme_classic() +
  theme(
    axis.text.x = element_text(angle = 90)
  )

graphique_LCS

Création du support PowerPoint avec officer

De base je ne suis pas trop pour la création de support pptx avec R, je préfère les rapports html mais cela reste un support très attendu pour la communication à destination des clients.

Grâce au package officer, je vais créer un support simple mais dynamique avec des liens entre les diapos.

library(officer)
library(glue)

emplacement_du_support <- "template_rstudio_article_off.pptx"

# pour voir les types de diapos qui existent et les éléments qui les constituent
read_pptx(emplacement_du_support) %>%
  layout_properties() %>% 
  select(master_name, name, type, id, ph_label)
##    master_name                    name     type id                    ph_label
## 1     Intégral    Diapositive de titre     body 10                 Rectangle 9
## 2     Intégral    Diapositive de titre     body  8        Straight Connector 7
## 3     Intégral    Diapositive de titre     body 12                ZoneTexte 11
## 4     Intégral    Diapositive de titre     body 11                      Oval 5
## 5     Intégral    Contenu avec légende     body  3       Content Placeholder 2
## 6     Intégral    Contenu avec légende     body  4          Text Placeholder 3
## 7     Intégral                 Contact     body 26                ZoneTexte 25
## 8     Intégral                 Contact     body 22                    Image 21
## 9     Intégral      Image avec légende     body  4          Text Placeholder 3
## 10    Intégral      Image avec légende     body  8        Straight Connector 7
## 11    Intégral                  Resume     body 10   Espace réservé du texte 9
## 12    Intégral Titre et texte vertical     body  3 Vertical Text Placeholder 2
## 13    Intégral                  Resume     body 14  Espace réservé du texte 13
## 14    Intégral Titre vertical et texte     body  3 Vertical Text Placeholder 2
## 15    Intégral Titre vertical et texte     body  7        Straight Connector 6
## 16    Intégral                 Contact     body 14                ZoneTexte 13
## 17    Intégral                  Resume     body 22  Espace réservé du texte 21
## 18    Intégral                 Contact     body 23                Rectangle 22
## 19    Intégral                 Contact     body 24                Rectangle 23
## 20    Intégral                  Resume     body  6   Espace réservé du texte 5
## 21    Intégral                 Contact     body  3                 ZoneTexte 2
## 22    Intégral                 Contact     body  8                 Graphique 7
## 23    Intégral                 Contact     body  9 Espace réservé du contenu 5
## 24    Intégral Titre, Contenu et Image     body  6 Espace réservé du contenu 5
## 25    Intégral                  Resume     body 12  Espace réservé du texte 11
## 26    Intégral Titre, Contenu et Image     body  9 Espace réservé du contenu 8
## 27    Intégral                  Resume     body 16  Espace réservé du texte 15
## 28    Intégral                  Resume     body 18  Espace réservé du texte 17
## 29    Intégral                  Resume     body 20  Espace réservé du texte 19
## 30    Intégral        Titre et contenu     body  5 Espace réservé du contenu 5
## 31    Intégral                  Resume     body 24  Espace réservé du texte 23
## 32    Intégral                  Resume     body  4                 Rectangle 3
## 33    Intégral             Comparaison     body  5          Text Placeholder 4
## 34    Intégral Titre, Contenu et Image     body  5                 Graphique 4
## 35    Intégral        Titre de section     body  8        Straight Connector 7
## 36    Intégral Titre, Contenu et Image     body  7 Espace réservé du contenu 6
## 37    Intégral       PrésentationMarie     body  7                 ZoneTexte 6
## 38    Intégral Titre, Contenu et Image     body  4 Espace réservé du contenu 3
## 39    Intégral       PrésentationMarie     body  5 Espace réservé du contenu 5
## 40    Intégral        Titre de section     body  9                 Rectangle 8
## 41    Intégral        Titre et contenu     body  3       Content Placeholder 2
## 42    Intégral        Titre et contenu     body  4                 Graphique 3
## 43    Intégral        Titre de section     body  3          Text Placeholder 2
## 44    Intégral             Comparaison     body  6       Content Placeholder 5
## 45    Intégral        Titre de section     body 11                      Oval 5
## 46    Intégral           Deux contenus     body  3       Content Placeholder 2
## 47    Intégral       PrésentationMarie     body  4                 Graphique 3
## 48    Intégral       PrésentationMarie     body  3                 ZoneTexte 2
## 49    Intégral             Comparaison     body  4       Content Placeholder 3
## 50    Intégral           Deux contenus     body  4       Content Placeholder 3
## 51    Intégral             Comparaison     body  3          Text Placeholder 2
## 52    Intégral        Titre et contenu    title  2                     Title 1
## 53    Intégral Titre, Contenu et Image    title  2                     Title 1
## 54    Intégral      Image avec légende    title  2                     Title 1
## 55    Intégral    Contenu avec légende    title  8                     Title 7
## 56    Intégral Titre vertical et texte    title  2            Vertical Title 1
## 57    Intégral           Deux contenus    title  2                     Title 1
## 58    Intégral Titre et texte vertical    title  2                     Title 1
## 59    Intégral        Titre de section    title  2                     Title 1
## 60    Intégral             Comparaison    title 10                     Title 9
## 61    Intégral              Titre seul    title  2                     Title 1
## 62    Intégral    Diapositive de titre ctrTitle  2                     Title 1
## 63    Intégral    Diapositive de titre      tbl  5 Espace réservé du tableau 4
## 64    Intégral      Image avec légende      pic  3       Picture Placeholder 2
creation_du_support <- function(num_identifiant_pingouin){
  read_pptx(emplacement_du_support) %>% 
    # 1. Création de la diapo de titre
    add_slide(
      layout = "Diapositive de titre", 
      master = "Intégral"
    ) %>% 
    ph_with(
      value = num_identifiant_pingouin, 
      location = ph_location_label("Title 1")
    ) %>% 
    ph_with(
      value = penguin.data %>% filter(bandid == num_identifiant_pingouin) %>% slice(1) %>% as_tibble() %>% select(year, ck),
      location = ph_location_label("Espace réservé du tableau 4")
    ) %>% 
    
    # 2. Ajout du sommaire cliquable
    add_slide(
      layout = "Resume",
      master = "Intégral"
    ) %>% 
    ph_with(
      value = glue("Numéro d'identification du pingouin : {num_identifiant_pingouin}"),
      location = ph_location_label("Espace réservé du texte 5")
    ) %>% 
    ph_with(
      value = "Informations sur Marie Vaugoyeau",
      location = ph_location_label("Espace réservé du texte 9")
    ) %>% 
    ph_with(
      value = "Graphique Life Cycle Saving",
      location = ph_location_label("Espace réservé du texte 11")
    ) %>% 
    ph_with(
      value = glue("Graphique associé au pingouin {num_identifiant_pingouin}"),
      location = ph_location_label("Espace réservé du texte 13")
    ) %>% 
    ph_with(
      value = "Informations pour me contacter",
      location = ph_location_label("Espace réservé du texte 15")
    ) %>% 
    
    # 3. Ajout de la diapo d'information sur moi (petite page de pub)
    add_slide(
      layout = "PrésentationMarie",
      master = "Intégral"
    ) %>% 
    ph_with(
      value = "Clic retour menu", 
      location = ph_location_label("Espace réservé du contenu 5")
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du contenu 5", 
      slide_index = 2
    ) %>% 
    
    # 4. Ajout du graphique LCS
    add_slide(
      layout = "Titre et contenu",
      master = "Intégral"
    ) %>% 
    ph_with(
      value = "Graphique LCS",
      ph_location_label("Title 1")
    ) %>% 
    ph_with(
      value = graphique_LCS,
      ph_location_label("Content Placeholder 2")
    ) %>% 
    ph_with(
      value = "Clic retour menu", 
      location = ph_location_label("Espace réservé du contenu 5")
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du contenu 5", 
      slide_index = 2
    ) %>%
    
    # 5. Ajout du graphique personnalisé du pingouin
    add_slide(
      layout = "Titre, Contenu et Image",
      master = "Intégral"
    ) %>% 
    ph_with(
      value = "Graphique du pingouin",
      ph_location_label("Title 1")
    ) %>% 
    ph_with(
      value = external_img("pingouin.png"), # image extraite de l'article à l'origine des données
      location = ph_location_label("Espace réservé du contenu 3")
    ) %>% 
    ph_with(
      value = glue("{(penguin.data %>% filter(bandid == num_identifiant_pingouin) %>% slice(1))$year} {(penguin.data %>% filter(bandid == num_identifiant_pingouin) %>% slice(1))$ck}"),
      ph_location_label("Espace réservé du contenu 8")
    ) %>% 
    ph_with(
      value = creation_du_graphique_pour_comparer_un_pingouin_aux_autres(num_identifiant_pingouin),
      ph_location_label("Espace réservé du contenu 6")
    ) %>% 
    ph_with(
      value = "Clic retour menu", 
      location = ph_location_label(ph_label = "Espace réservé du contenu 5")
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du contenu 5", 
      slide_index = 2
    ) %>%
    
    # 6. Ajout de la diapo de contact
    add_slide(
      layout = "Contact",
      master = "Intégral"
    ) %>% 
    ph_with(
      value = "Clic retour menu", 
      location = ph_location_label(ph_label = "Espace réservé du contenu 5")
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du contenu 5", 
      slide_index = 2
    ) %>%
    
    # Création des liens sur la page du sommaire
    on_slide(index = 2) %>%
    ph_slidelink(
      ph_label = "Espace réservé du texte 9", 
      slide_index = 3
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du texte 11", 
      slide_index = 4
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du texte 13", 
      slide_index = 5
    ) %>%
    ph_slidelink(
      ph_label = "Espace réservé du texte 15", 
      slide_index = 6
    )
}

Envoi du support avec le package sendmailR

Une fois que le squelette du support est prêt, il ne reste plus qu’à envoyer à chaque pingouin (ou chaque client) son support personnalisé.

library(sendmailR)

num_identifiant_pingouin <- c("-0.993421052631561 422", "-0.993421052631561 308")

message_support <- map(
  .x = num_identifiant_pingouin,
  .f = ~ mime_part(
    creation_du_support(.x), 
    "support.pptx"
  )
)

message_support[[1]]$`Content-Type` <- "application/vnd.openxmlformats-officedocument.presentationml.presentation"

message_support[[2]]$`Content-Type` <- "application/vnd.openxmlformats-officedocument.presentationml.presentation"

adresse_d_envoi <- c("experimentatateur1@truc.com", "experimentateur2@truc.com") # adresse de la personne qui a suivi le pingouin/client

adresse_du_pingouin <- c("pingouin1@pole_nord.fr", "pingouin2@pole_nord.fr") # adresse mail du pingouin/client

# Envoi des mails
map2(
  .x = adresse_d_envoi, # on peut même associé une adresse d'envoi différente par pingouin/client
  .y = adresse_du_pingouin,
  .f = ~ sendmail(
    from = glue("<{.x}>"),
    to = glue("<{.y}>"),
    subject = glue("synthèse de la prise de poids du pingouin {num_identifiant_pingouin}"),
    msg = list(
      glue("Bonjour,\nVeuillez trouver ci-joint la synthèse pour le pingouin {num_identifiant_pingouin}.\nBonne journée, \nMarie Vaugoyeau"),
      message_support
    ),
    control = list(smtpServer = "ASPMX.L.GOOGLE.COM")
  )
)

Ouppsss, soucis cela ne fonctionne pas….
Mais pourquoi ???
En fait, un petit coup de fil rapide à David Gohel, 1 minute de conversation et hop la solution apparaît comme une évidence, ce que je traite comme un pptx est en fait un rpptx…
Il faut donc éditer le support pptx avant de l’envoyer et là ça passe sans soucis !

Deux solutions, soit on enregistre tous les supports pptx que l’on envoie pour archive (solution prise ici et ), soit on créé un support temporaire qui sera écrasé.

enregistrement_du_support_pptx <- function(num_identifiant_pingouin){
  num_identifiant_pingouin %>% 
    creation_du_support() %>% 
    print(glue('presentation_{num_identifiant_pingouin %>% str_remove_all("[:punct:]")}.pptx'))
}

num_identifiant_pingouin <- c("-0.993421052631561 422", "-0.993421052631561 308")

num_identifiant_pingouin %>% 
  map(enregistrement_du_support_pptx)

Et voilà !

J’espère que cet article vous permettra de mieux chercher de l’aide et de réaliser plus facilement vos travaux avec R.