[Etude de marché - Article 2]
Aurélien Daval
Dans le précédent article, nous avons regardé comment fonctionnait le package Shiny. Dans cet article, nous allons voir comment utiliser ce package pour programmer un questionnaire.
2. Programmation du questionnaire
Vous sortez d’un atelier de travail avec l’équipe en charge du projet. Au préalable de cette réunion, votre directeur avait envoyé un premier draft du questionnaire. Cette réunion a permis de le finaliser et d’obtenir la validation du client. Il faut maintenant programmer le questionnaire.
R n’est pas un outil de sondage comme Survey Monkey ou Askia. Cependant, avec le package Shiny (qui permet de construire des pages Web interactives sans HTML/CSS/JavaScript), il est possible de programmer une application de sondage qui permet d’afficher les différentes questions et de collecter les réponses. Sur le site d’Askia, vous pouvez répondre à leur questionnaire de démonstration, ce qui vous montre dès à présent vers quoi nous souhaitons tendre.
2.1. Les différentes type de questions.
Voilà le moment de programmer votre première question. Pour cette partie, votre code R va se situer exclusivement à l’intérieur de la partie Interface Utilisateur (UI). C’est dans la partie server que nous allons gérér l’architecture du questionnaire.
2.1.1. La question fermée à choix unique
C’est une question où le répondant doit choisir une seule réponse. Pour cela, on appelle la fonction “radioButtons”. Plusieurs arguments sont nécessaires pour cette fonction :
- InputId : l’ID unique.
- Label : l’intitulé de la question.
- Choices : les réponses à la question.
library(shiny)
shinyApp(
ui = fluidPage(
radioButtons(inputId = "Q1", label = p("Vous êtes :"),
choices = list("Un Homme" = 1, "Une femme" = 2))),
server = function(input, output, session) {})
Bravo. Votre première question s’affiche en cliquant sur “Run App” (de votre fichier app.R). Cependant, le style du radio button inclus dans ce package Shiny manque de peps visuel. Heureusement, il y a d’autres packages qui permettent d’embélir votre question.
Ainsi, nous allons faire appel à un autre package shinyWidgets. Ce dernier permet d’avoir accès d’autres styles de radio buttons. On utilise donc la fonction “radioGroupButtons” (qui se construit comme la fonction radioButtons). On ajoute plusieurs arguments :
- Selected : si cet argument est vide, il permet de ne pas présélectionner une réponse.
- Direction : “vertical”. Cela permet d’afficher les réponses en colonne.
- Justified : C’est ce qui permet d’ajuster les cases des réponses sur tout le long de la page affichée.
On ajoute également une fonction “panel” afin d’encadrer la question. Ce qui donne :
library(shiny)
library(shinyWidgets)
shinyApp(
ui = fluidPage(
panel(radioGroupButtons("Q1", label = p("Vous êtes :"),
choices = list("Un Homme" = 1, "Une femme" = 2),
selected = "", direction = "vertical", justified = TRUE))),
server = function(input, output, session) {})
2.1.2. La question numérique.
C’est une question permet de demander par exemple au répondant son âge ou son code postal. La fonction utilisée est numericInput. Plusieurs arguments sont nécessaires:
- Value : la valeur à afficher dans l’encadré. Ici, rien (value = "").
- Min : la valeur minimale acceptée.
- Max : la valeur manimale acceptée.
library(shiny)
library(shinyWidgets)
shinyApp(
ui = fluidPage(
panel(numericInput("Q2", label = p("Quel âge avez-vous ?"), value = "", min = 1, max = 110))),
server = function(input, output, session) {})
2.1.3. La question fermée à choix multiple
C’est une question où le répondant choisit une seule ou plusieurs réponses. Pour cela, on appelle la fonction “checkboxGroupButtons” du package shinyWidgets. Ce dernier se construit comme la fonction ci-dessus radioGroupButtons.
library(shiny)
library(shinyWidgets)
shinyApp(
ui = fluidPage(
panel(
checkboxGroupButtons("Q3", label = "Sur quel réseau social êtes-vous inscrit(e) ?",
choices = c("Facebook" = 1,
"Google +" = 2,
"Instagram" = 3,
"Linkedin" = 4,
"Periscope" = 5,
"Pinterest" = 6,
"Reddit" = 7,
"Snapchat" = 8,
"TikTok" = 9,
"Tumblr" = 10,
"Twitter" = 11,
"Viadeo" = 12,
"Aucun de ces réseaux" = 99),
direction = "vertical", selected = "", justified = TRUE))),
server = function(input, output, session) {})
PS : Pour l’instant, il est possible de choisir certains réseaux sociaux et la réponse “Aucun de ces réseaux”, ce qui n’est pas logique. C’est dans la partie serveur de l’application que nous règlerons ce problème.
2.1.4. La question ouverte.
La question ouverte permet aux répondants de s’exprimer librement et de sortir des questions fermées trop cadrées. C’est également pour l’entreprise la possibilité d’avoir des Verbatims. La fonction utilisée est textInput. Plusieurs arguments sont nécessaires:
- Value : Vous pouvez écrire un texte qui sera visible dans l’encadré, là où le répondant devra écrire.
- Width : C’est ce qui permet d’étendre la boite de dialogue.
library(shiny)
library(shinyWidgets)
shinyApp(
ui = fluidPage(
panel(textInput("Q4", label = ("Quels sont pour vous les inconvéniants de Facebook ?"),
value = "Ecrire ici", width = "600px"))),
server = function(input, output, session) {})
2.1.5. La question drag-and-drop.
C’est une question où le répondant fait glisser des éléments d’une liste dans un ou plusieurs Buckets. Pour programmer la question, nous avons besoin du package “sortable”. Dans cette exemple, il s’agit de demander au répondant s’il a l’intention d’acheter (ou non) certains produits.
install.packages("sortable")
La fonction utilisée est bucket_list. Plusieurs arguments sont nécessaires:
- Header : Ici la question.
- add_rank_list : c’est le nombre de buckets. Ici dans l’exemple, 4. Le bucket de la liste des produits, Oui, Peut-être ou non. On attribue à chaque Bucket un ID.
- text : C’est le texte affiché au dessus du Bucket.
- labels : c’est la liste des objets à faire glisser.
library(shiny)
library(shinyWidgets)
library(sortable)
shinyApp(
ui = fluidPage(
panel(
bucket_list(
header = c("Quels appareils prévoyez vous d'acheter lors du prochain mois?"),
add_rank_list(
input_id = "Q5",
text = "Liste des appareils",
labels = c("Une console", "Une montre", "Un drone", "Une tablette", "Un casque audio", "Un smartphone")
),
add_rank_list(
input_id = "Q5_Oui",
text = "Oui"
),
add_rank_list(
input_id = "Q5_pe",
text = "Peut-être"
),
add_rank_list(
input_id = "Q5_non",
text = "Non"
)))),
server = function(input, output, session) {})
2.2. La structure du questionnaire
Vous savez maintenant programmé(e) plusieurs types de questions. Nous allons maintenant voir ici comment gérer l’enchainement des questions. Un questionnaire affiche une question à la fois. Et lorsque le répondant répond à la question, la suivante est affichée. Regardons comment gérer cette affichage des questions dans R.
2.2.1. Mise en place d’un bandeau
Avant de s’occuper de l’enchainement des questions, nous allons tout d’abord fixer le bandeau qui va rester tous au long de l’enchainement des questions. Ce bandeau permet d’aérer le haut de la page et de fixer le logo de notre société, Sur-R-vey.
Ainsi, dans le dossier de votre application, il faut créer un nouveau dossier nommé “www”. C’est là qu’il faut placer les images nécessaires à notre application.
Pour ne pas perdre de temps, on va également programmer la page d’acceuil (la première page de votre questionnaire) et améliorer le visuel de notre questionnaire. Ainsi, il faut ajouter dans notre code :
- Un bandeau avec la fonction “titlePanel”. Et on ajoute à l’intérieur de cette fonction l’image (myImage.png) issue de notre dossier www via la ligne de code img(src=“myLogo.png”).
- Un contour via la fonction Panel : c’est ce qui permet de matérialiser la zone de notre questionnaire.
- On réduit ce contour avec la fonction style : On réduit la page à 800 px, ce qui permet de fixer les questions au centre de la page.
library(shiny)
library(shinyWidgets)
shinyApp(
ui = fluidPage(
style = "width:800px",
titlePanel(img(src="myLogo.png")),
panel(
h3("Bienvenue sur notre questionnaire en ligne. Nous vous remercions d’accepter de répondre à cette
étude en ligne menée par l’Institut d’études et de sondages Su-R-vey. Cette étude sera traitée de façon
anonyme. Nous vous garantissons la confidentialité de cette interview. Ce questionnaire vous prendra 5
minutes."))),
server = function(input, output, session) {})
2.2.2. La gestion de l’enchainement des questions.
Avant de rentrer dans la technique, je vais vous expliquer le fonctionnement de l’application. La première question (l’introduction très exactement) est programmée en clair dans l’UI. Les suivantes le sont en cachées. Et c’est dans la partie “Server” qu’on va organiser l’affichage des différentes questions lors de l’activation des différents boutons “suivant”. Le premier bouton va cacher l’introduction et afficher la première question. Le second bouton “suivant” va cacher la première question et afficher la deuxième question. Et ainsi de suite jusqu’à la fin du questionnaire.
Dans cette section, je vais diviser le code en plusieurs partie. La totalité du code est disponible à la fin de la section.
Rendre invisible les questions.
Lorsqu’on parle ci-dessus d’une question, on parle très exactement de deux élements :
- De la question en elle-même.
- Du bouton suivant via la fonction “actionButton”. Deux arguments : l’ID et le texte inscrit sur le bouton.
C’est un tout qu’il faut agréger. Chaque question a en effet son bouton associé. C’est ce qui va permettre (via l’ID de chaque bouton), de gérer l’enchainement des questions (avec la gestion des filtres notamment, explication à suivre ci-dessous). Pour cela, on utilise un élément HTML “div” qui va permettre d’agréger ces deux éléments. Ainsi, dans la partie UI, on a :
div(id = "Intro",
h3("Bienvenue sur notre questionnaire en ligne. Nous vous remercions d’accepter
de répondre à cette étude en ligne menée par l’Institut d’études et de sondages
Su-R-vey. Cette étude sera traitée de façon anonyme. Nous vous garantissons la
confidentialité de cette interview. Ce questionnaire vous prendra une vingtaine
de minutes."),
actionButton("button0", "Suivant"))
Pour programmer la seconde question, il faut la rendre non visible. Pour cela, on utilise la fonction hidden du package “shinyjs”.
install.packages("shinyjs")
Pour faire fonctionner cette fonction, il faut :
- Dire à notre script qu’on utilise cette fonction du package shinyjs : library(shinyjs).
- Activer les fonctions hidden dans notre page (fluidPage) via la ligne de code : shinyjs::useShinyjs().
Cette fonction est un attribut booléen qui, lorqu’il est présent, spécifie qu’un élément n’est pas visible, jusqu’à qu’une condition soit remplie pour pouvoir l’afficher (les conditions sont dans la partir server). Cet attribut est associé à l’ensemble de la balise div. Ainsi, on a (toujours dans la partie UI) :
library(shinyjs)
ui = fluidPage(
shinyjs::useShinyjs(),
# Question Q1.
hidden(div(id = "Q1",
radioGroupButtons("Q1", label = p("Vous êtes :"),
choices = list("Un Homme" = 1, "Une femme" = 2),
selected = "", direction = "vertical", justified = TRUE),
actionButton("button1", "Suivant"))))
Faire fonctionner le bouton suivant.
Pour l’instant, notre application affiche l’introduction et un premier bouton suivant (alors même que toutes les questions sont programmées). Cependant, lorsque l’on appuie sur ce premier bouton suivant, rien ne se passe. Pour pallier à cela, il faut programmer la partie server.
Pour cela, on va utiliser la fonction observeEvent qui va effectuer une action en réponse à un événement (ici, cliquer sur le bouton suivant). 2 arguments sont nécessaires pour faire fonctionner cette fonction :
- L’évenement qui va déclencher l’action : ici, lorsqu’on clique sur le premier bouton (button0).
- La fonction qui doit être appelée chaque fois que l’événement se produit. Ici, on cache l’introduction via l’argument hide (id = “Intro”) et on montre la première question via l’argument show (id = “Q1”). On utilise pour cela du Javascript via shinyjs.
Dans la partie Server, on a donc:
observeEvent(input$button0, {
shinyjs::hide(id = "Intro")
shinyjs::show(id = "S1")
})
observeEvent(input$button1, {
shinyjs::hide(id = "Q1")
shinyjs::show(id = "Q2")
})
2.2.3. Question filtrée
La question 4 est filtrée : en effet, si le répondant n’est pas inscrit au réseau social Facebook, on ne va pas lui demander les inconvéniants de ce dernier.
Pour cela, il faut créer deux événements :
- Le premier événement : si la réponse Facebook (=1 –> en langage R: == 1) est sélectionnée à la question 3, je passe à la suivante.
- Le second événement : si la réponse n’est pas sélectionnée (≠1 –> en langage R: != 1), je passe à la question 5.
On a ainsi :
observe(if(input$button3 && input$Q3 == 1) {
shinyjs::hide("Q3")
shinyjs::show("Q4")
})
observe(if(input$button3 && input$Q3 != 1) {
shinyjs::hide("Q3")
shinyjs::show("Q5")
})
2.3. L’obligation de donner une réponse avant de passer à la question suivante
L’enchainement des questions est maintenant bien géré. Cependant, lorsque le répondant clique sur le bouton suivant, ce dernier passe à la question suivante malgré l’absence de réponse. Or, l’objectif, c’est d’avoir la réponse du répondant.
2.3.1. Obligation de répondre aux questions à choix unique/multiple.
Ainsi, pour obliger le répondant à se positionner, il faut ajouter à l’événement “le répondant a cliqué sur le bouton Suivant” un autre événement “il y a une réponse”. Pour ajouter cet événement, il faut jouer sur plusieurs éléments :
- Un opérateur conditionnel : If. Si la condition est vrai, alors l’action contenue entre crochets est déclenchée.
- Un opérateur logique “Et” : &&. Ce dernier évalue d’abord la condition de gauche et si celle-ci est vraie, alors il passe à celle de droite.
- L’opérateur logique “Différent” : !.
- La fonction “Absence de valeurs” : is.null.
Ainsi, sur la première question (2ième page –> le genre), on a :
observe(if(input$button1 && !is.null(input$Q1)) {
shinyjs::hide("Q1")
shinyjs::show("Q2")
})
2.3.2. Obligation de répondre avec un chiffre/nombre à une question numérique.
Retour sur la question 2 (sur l’âge du répondant) : pour l’instant, si le répondant ne renseigne pas un chiffre/nombre ou ne respecte pas les valeurs minimum/maximum, ce dernier peut tout de même passer à la question suivante.
Pour obliger le répondant à répondre un chiffre/nombre, il faut mettre dans les conditions pour passer à la question suivante que ce dernier doit être élément numérique. Pour cela, il faut utiliser la condition “is.numeric”. Comme ce dernier impose un résultat numérique, la condition “!is.null(input$Q2)” n’est plus nécessaire.
De plus, pour éviter les réponses “farfelues”, nous allons mettre en place une borne haute (110 ans) avant de passer à la question suivante. On a donc :
observe(if(input$button2 && is.numeric(input$Q2) && input$Q2 <= 110) {
shinyjs::hide("Q2")
shinyjs::show("Q3")
})
Enfin, si ce dernier ne respecte pas la borne, il est important de signifier au répondant pourquoi il ne peut pas passer à la question suivante. Pour cela, on va mettre en place un message d’erreur - en mode caché - dans l’UI, en dessous de l’input numérique Q2 –> Q2_out.
hidden(div(id = "Q2",
numericInput("Q2", label = p("Quel âge avez-vous ?"), min = 1, max = 110, value = ""),
hidden(div(id = "Q2_out", p("La réponse doit être comprise entre 1 et 110"))),
actionButton("button2", "Suivant"))),
Et c’est dans la partie Server que nous allons voir comment afficher ce message. Ainsi, s’il y a un nombre renseigné –> !is.na(input$Q2) et si ce chiffre est supérieur à 110 –> (input$Q2 > 110), il faut montrer ce message. Ou (–> en langage R:|) si ce dernier est inférieur à 1. On a ainsi :
observeEvent(input$Q2, {
if ((!is.na(input$Q2) && input$Q2 > 110) | (!is.na(input$Q2) && input$Q2 < 1 )) {
shinyjs::show("Q2_out")
} else {
shinyjs::hide("Q2_out")
}}, ignoreInit = TRUE)
2.3.3. Le cas de deux réponses antinomiques (au sein d’une même question).
Retour sur la question 3 (sur les réseaux sociaux) : il n’est pas possible de passer à la question suivante sans avoir répondu à cette question. Cependant, il est possible de choisir certains réseaux sociaux et la réponse “Aucun de ces réseaux”. Ce qui n’a pas de sens.
Ainsi, pour palier à ce problème, il faut lorsque le répondant clique sur “Aucun de ces réseaux” que les autres réponses s’effacent.
Pour cela, il faut dans la partie server utiliser la fonction updateCheckboxGroupButtons qui permet de changer la valeur d’entrée en fonction de certaines règles. Ainsi, si la réponse 99 fait partie (%in%) des réponses à la question Q3, il ne faut retenir que la réponse 99 (selected = “99”). Sinon, retenir les réponses à la Q3 (selected = input$Q3)
observeEvent(input$Q3, {
if (99 %in% input$Q3) {
updateCheckboxGroupButtons(session, "Q3", selected = "99")
} else {
updateCheckboxGroupButtons(session, "Q3", selected = input$Q3)
}
}, ignoreInit = TRUE)
2.4. Conclusion
Ci-dessous le code complet de notre application.
library(shiny)
library(shinyWidgets)
library(sortable)
library(shinyjs)
shinyApp(
# Questionnaire
ui = fluidPage(
style = "width:800px",
shinyjs::useShinyjs(),
titlePanel(img(src="myLogo.png")),
panel(
##### Questionnaire #####
# Introduction
div(id = "Intro",
h3("Bienvenue sur notre questionnaire en ligne. Nous vous remercions d’accepter de répondre à cette
étude en ligne menée par l’Institut d’études et de sondages Su-R-vey. Cette étude sera traitée de façon
anonyme. Nous vous garantissons la confidentialité de cette interview. Ce questionnaire vous prendra 5
minutes."),
actionButton("button0", "Suivant")),
# Question Q1.
hidden(div(id = "Q1",
radioGroupButtons("Q1", label = p("Vous êtes :"),
choices = list("Un Homme" = 1, "Une femme" = 2),
selected = "", direction = "vertical", justified = TRUE),
actionButton("button1", "Suivant"))),
# Question Q2.
hidden(div(id = "Q2",
numericInput("Q2", label = p("Quel âge avez-vous ?"), min = 1, max = 110, value = ""),
hidden(div(id = "Q2_out", p("La réponse doit être comprise entre 1 et 110"))),
actionButton("button2", "Suivant"))),
# Question Q3.
hidden(div(id = "Q3",
checkboxGroupButtons("Q3", label = "Sur quel réseau social êtes-vous inscrit(e) ?",
choices = c("Facebook" = 1,
"Google +" = 2,
"Instagram" = 3,
"Linkedin" = 4,
"Periscope" = 5,
"Pinterest" = 6,
"Reddit" = 7,
"Snapchat" = 8,
"TikTok" = 9,
"Tumblr" = 10,
"Twitter" = 11,
"Viadeo" = 12,
"Aucun de ces réseaux" = 99),
direction = "vertical", selected = "", justified = TRUE),
actionButton("button3", "Suivant"))),
# Question Q4.
hidden(div(id = "Q4",
textInput("Q4", label = ("Quels sont pour vous les inconvéniants de Facebook ?"),
value = "Ecrire ici", width = "600px"),
actionButton("button4", "Suivant"))),
# Question Q5.
hidden(div(id = "Q5",
bucket_list(
header = c("Quels appareils prévoyez vous d'acheter lors du prochain mois?"),
add_rank_list(
input_id = "Q5_Liste",
text = "Liste des appareils",
labels = c("Une console", "Une montre", "Un drone", "Une tablette",
"Un casque audio", "Un smartphone")
),
add_rank_list(
input_id = "Q5_Oui",
text = "Oui"
),
add_rank_list(
input_id = "Q5_pe",
text = "Peut-Etre"
),
add_rank_list(
input_id = "Q5_non",
text = "Non"
)),
actionButton("button5", "Suivant"))),
# Fin du questionnaire
hidden(div(id = "Fin",
p("Ce questionnaire est à présent terminé. Je vous remercie vivement
pour votre participation et je vous souhaite une bonne fin de
journée / soirée"),
actionButton("submit", "Terminé"))))),
server = function(input, output, session) {
##### L'enchainement des questions#####
observeEvent(input$button0, {
shinyjs::hide(id = "Intro")
shinyjs::show(id = "Q1")
})
observe(if(input$button1 && !is.null(input$Q1)) {
shinyjs::hide("Q1")
shinyjs::show("Q2")
})
# Obliger une réponse numérique.
observe(if(input$button2 && is.numeric(input$Q2) && input$Q2 <= 110) {
shinyjs::hide("Q2")
shinyjs::show("Q3")
})
# Mise en place d'un message si le répondant ne respecte pas la borne.
observeEvent(input$Q2, {
if ((!is.na(input$Q2) && input$Q2 > 110) | (!is.na(input$Q2) && input$Q2 < 1 )) {
shinyjs::show("Q2_out")
} else {
shinyjs::hide("Q2_out")
}}, ignoreInit = TRUE)
# Programmation du choix 99 de la question 3 (Si Q3 = 99 --> déselectionner tous les autres)
observeEvent(input$Q3, {
if (99 %in% input$Q3) {
updateCheckboxGroupButtons(session, "Q3", selected = "99")
} else {
updateCheckboxGroupButtons(session, "Q3", selected = input$Q3)
}
}, ignoreInit = TRUE)
# Question 4 est filtrée :
observe(if(input$button3 && input$Q3 == 1) {
shinyjs::hide("Q3")
shinyjs::show("Q4")
})
observe(if(input$button3 && input$Q3 != 1) {
shinyjs::hide("Q3")
shinyjs::show("Q5")
})
# Reprise Q5
observe(if(input$button4 && !is.null(input$Q4)) {
shinyjs::hide("Q4")
shinyjs::show("Q5")
})
observeEvent(input$button5, {
shinyjs::hide("Q5")
shinyjs::show("Fin")
})})
Bravo. Vous venez de programmer votre premier questionnaire sur R. Pour conclure, regardons ce que même questionnaire donnerait sur Survey Monkey avec ce lien. Notons qu’il n’y a pas de branchement conditionnel (option payante). Le questionnaire maintenant programmé, il faut s’occuper d’enregistrer les réponses et de le publier en ligne. Nous verrons ça dans le prochain article.