Peaufiner ses tutoriels R avec learnr et gradethis (2/2)

Pour construire des tutoriels R sur-mesure, les packages learnr et gradethis fournissent tous les outils nécessaires. RStudio, qui développe ces packages, met en ligne de soigneuses réalisations avec ses fameux Primers pour apprendre R.

Learnr et gradethis sont plutôt bien documentés (en anglais). Qui entend maitriser ces librairies doit en passer par la lecture de ces guides. Après un 1er exposé de présentation[1], j’aspire dans ce second article à vous faire gagner du temps, par une série d’astuces pratiques. Vous pourrez ainsi fignoler vos tutoriels à votre guise, avant de les proposer à vos élèves.

Dessiné par Yanina Bellini Saibene

Sommaire

Installer toutes les librairies utiles,
dans leurs dernières versions

Learnr et gradethis connaissent de belles évolutions ces derniers mois, avec l’arrivée de nouveaux développeurs en renfort. Je vous suggère donc d’installer les versions les plus récentes (dites de développement). Parsons et FlashCard (ici un exemple) complètent la panoplie des widgets de learnr. gradethis permet de contrôler finement les résolutions d’exercices. 

Voici en résumé les instructions d’installation :

devtools::install_github("rstudio/learnr", build_vignettes = TRUE)
remotes::install_github("rstudio-education/gradethis")
devtools::install_github("rstudio/packrat")
remotes::install_github("rstudio/parsons")
install.packages("flashCard")

En français s'il vous plaît !

Grâce à @_ColinFay, learnr peut désormais s’afficher en français, finis les boutons obligatoires “start over” ou “Submit answer”. Il suffit de préciser ceci dans l’en-tête de votre tutoriel :

output: 
  learnr::tutorial:
    language: fr

Pour connaître toutes les possibilités de personnalisation multilingue, entrez cette ligne dans R :

vignette("multilang", package = "learnr")

Voici en particulier comment j’ai personnalisé la traduction française en cours d’extension, pour ma convenance personnelle, dans cet en-tête dont je livre maintenant une présentation plus complète :

---
title: "Je code en R !"
output: 
  learnr::tutorial:
    progressive: true
    language: 
      fr:
        button:
          runcode: Tester le code
          startover: Réinitialiser
          hints: Indice(s) ?
          nexttopic: "Sujet suivant »"
          previoustopic: "« Sujet précédent"
        text:
          startover: Réinitialiser ce tutoriel
          areyousure: "Êtes-vous certain de vouloir recommencer ? Vos réponses actuelles seront perdues."
runtime: shiny_prerendered
---

Le support multilingue dans learnr est tout récent, veillez donc à bien installer la dernière version. Et il est encore partiel. Les obsessionnels (dont je suis) pourront traiter, temporairement en JavaScript, les traductions manquantes avec un peu de maîtrise des évènements Shiny (cf. le code du tutoriel exemple).

Affiner le style de votre tutoriel avec un peu de CSS

On peut trouver reposant le style délicatement bleuté proposé par défaut par learnr (ce sont les couleurs de RStudio). Mais voilà, ma directrice graphique m’invite à harmoniser l’apparence des contenus de ce blog, place donc à CSS ! Un document learnr est au format RMarkdown, il comprend des “chunks” de code, qui peuvent être indifféremment du R, du CSS, du JavaScript… 

Voici donc comment ajuster un espace après-titre et insérer un logo :

```{css}
  h3 {
    margin-bottom: 0.4em;
  }

  #tutorial-topic:before { 
    content: url(img/logo-icem7-100.png) ;
  }
```

Si je vais vers une feuille de style substantielle et réutilisable, l’externaliser s’avèrera bien plus commode. J’indique ici le chemin vers un fichier CSS dans l’en-tête du document :

output: 
  learnr::tutorial:
    css: css/custom_css.css

Voici donc le résultat de l’application de mes styles, avec mes polices favorites, un joli logo, des boutons subtilement retravaillés :

Accédez au tutoriel
Styles personnalisés avec logo, polices choisies et couleurs ajustées

Tout comme le soin apporté à la traduction ou à l’orthographe, le travail sur le style témoigne de votre implication auprès de vos utilisateurs, et même d’une forme de respect. C’est un métalangage qu’ils perçoivent très bien ! Précisez qui vous êtes, et de quand date votre travail. Attention toutefois à ne point trop en faire, en multipliant images, icones et arrière-plans source de “bruit cognitif” inutile.

Voici comme point de comparaison la présentation par défaut, sans traduction :

Style par défaut d'un tutoriel learnr, non traduit

Afficher des extraits de tutoriel
dans une page web quelconque

Les aperçus ci-dessus sont de simples captures d’écran, mais si je voulais insérer, dans un iframe, un extrait de mon tutoriel, comment m’y prendre ? Je voudrais par exemple :

  • montrer une rubrique spécifique de mon tutoriel, et seulement celle-là, sans boutons “Suivant” ou “Précédent” ;
  • masquer le panneau latéral gauche et le titre général du tutoriel ;
  • faire en sorte que la résolution de l’exercice, qui va augmenter la hauteur du contenu à restituer (messages informatifs, affichage du résultat), décale de façon transparente la suite de la page, sans apparition d’ascenseur.

Je peux déjà facilement afficher une section spécifique de mon tutoriel si chaque section est conçue pour se révéler progressivement, ce que j’active ainsi : 

## Reconstitution d'une phrase {data-progressive = TRUE}

Je vais désormais indiquer comme source de l’iframe une URL complétée par le ciblage d’une ancre (ici https://icem7.shinyapps.io/learnr-concepts-intro/#section-reconstitution-dune-phrase-v2)

Pour le programmeur, la difficulté majeure consiste à reconnaitre quand le morceau de tutoriel est diffusé dans une iframe, afin que des instructions CSS spécifiques le recomposent différemment. 

Manque de chance, par défaut, l’interface RStdudio compile et diffuse un tutoriel learnr dans une iframe. Je ne peux donc utiliser “suis-je dans une iframe” comme un test discriminant. Je dois rajouter un test sur le nom de domaine hébergeant le tutoriel : 

```{js}
if (window.top != window.self && window.location.hostname.indexOf('shinyapps') > 0) {
  document.body.classList.add("framed");
}
```

Une fois la classe .framed ajoutée, je masque ici le menu de gauche et le titre général :

```{css}
  .framed .topicsContainer, .framed h2.tutorialTitle {
    display:none;
  }
```

Pour aller un peu plus loin et faire en sorte que la hauteur de mon iframe s’ajuste automatiquement à la hauteur de l’exercice, y compris quand je fais l’exercice, je peux suivre la démarche expliquée par Desirée de Leon avec un “iframe-resizer”. Ainsi, l’intégration devient totalement transparente pour le lecteur, comme vous pouvez le constater avec cet extrait, qui met en scène un exercice learnr au sein même d’une page WordPress :

Appréhender la diversité des widgets mobilisables

Learnr déploie une jolie gamme de widgets par défaut :

  • questions avec champ de saisie,
  • questions à choix unique / multiple (QCU / QCM),
  • exercices de codage avec correction,

que l’on peut compléter avec des widgets proposés par des librairies complémentaires :

  • problèmes de Parson,
  • flash (ou flip) Cards.
Plus généralement, un tutoriel learnr comme tout document RMarkdown peut valoriser des images, des vidéos, du code R auto-exécutable…

Comprendre les mécanismes de vérification de gradethis

La librairie gradethis offre au concepteur la possibilité d’examiner le code R que les utilisateurs “soumettent”, et de leur adresser des feedbacks ajustés. Cette librairie vient d’être sérieusement rénovée, il importe d’utiliser la nouvelle syntaxe, qui mobilise les fonctions grade_this() et grade_this_code() (oubliez-donc grade_result() et grade_conditions() qui vont sombrer dans l’oubli). 

Malheureusement, à la date d’écriture de cet article, la doc n’est pas encore totalement à jour et cohérente (la démo gradethis_demo() non plus !) Je vous invite à suivre les ressources proposées en fin d’article.

Quand l’utilisateur “soumet” un bloc de code, la fonction grade_this() l’ausculte par le biais de tests définis par le concepteur. Je considère par exemple ici l’éventualité d’une faute d’orthographe dans le résultat :

  if (str_detect(tolower(.result), 'programe') == TRUE) {
    fail("Faut-il un m ou deux m à ce progr... ?")
  }

Différentes variables caractérisent la “soumission” de l’utilisateur. Parmi les plus importantes :

  • .result est le dernier résultat produit par le code (y compris s’il a été affecté à une variable) ;
  • .envir_result se réfère à l’environnement de l’exercice, à savoir son contexte d’exécution ;
  • .user_code comprend tout le bloc de code ;
  • .solution décrit la solution, qui figure dans un “chunk” (bloc de code nommé) dédié.

Ainsi puis-je compléter mon schéma de vérification par cette succession de tests, typiquement une série de fails possibles, et à la fin un ou plusieurs pass (car il arrive que plusieurs réponses puissent être considérées comme bonnes) :

if ( !exists("mots", envir = .envir_result) ) {
  fail("Un indice : la variable 'mots' est absente de votre réponse")
}

if (.result == .solution) {
  pass("Bravo ! Si ce sont vos débuts avec R, c'est fantastique !")
}

Pour plus de détails sur la fonction centrale grade_this(), je vous invite à consulter la rubrique “Sélection de ressources utiles” plus bas, et le code du tutoriel exemple.

Comprendre la notion d’indépendance
des environnements d’exercice dans learnr

Dans chaque bloc d’exercice, l’apprenant est en mesure de créer de nouveaux objets en mémoire : variables simples, vecteurs, pourquoi pas tables à partir de fichiers CSV téléchargés : tout R ou presque lui est ouvert

Mais s’il passe d’exercice en exercice, il aura – peut-être – la surprise de constater qu’il ne peut poursuivre son travail précédent. Chaque exercice déploie son propre environnement, indépendant donc des autres.

Vous pouvez le vérifier en direct dans les deux blocs learnr suivants :

Voilà qui semble contraindre la progression dans le tutoriel. Le concepteur peut toutefois préparer des ressources dans un “chunk” spécial, de “setup“, dont chaque exercice pourra disposer. Par exemple ci-dessus, une table dénommée “rsa_df” est bien disponible pour chaque exercice. 

Voici un setup-type, qui précharge toutes les librairies utiles, par exemple ici tidyverse dont les apprenants pourront bénéficier par défaut pour chacun de leurs exercices :

```{r setup, include = FALSE}
library(tidyverse)
library(learnr)
library(gradethis)
url = "https://static.data.gouv.fr/resources/suivi-mensuel-des-prestations-de-solidarite/20201224-095107/tbl-rsa.csv"
rsa_df = read_csv(url)
```

Il est même possible de définir une collection de blocs setup, se complétant progressivement les uns les autres, et pour chaque exercice de préciser de quel setup il a besoin. Ainsi, les élèves n’ont pas à répéter l’intégralité de leur travail à chaque exercice, ils repartent en fait d’une situation idéale, un peu comme s’ils avaient réussi les exercices précédents.

En pratique, il est ainsi possible d’aborder les exercices dans le désordre, ou de reprendre un tutoriel en cours de route. C’est du reste l’argument des concepteurs de learnr pour justifier ce cloisonnement des environnements d’exercices.

Si le formateur souhaite préparer des ressources comme des fichiers csv ou xlsx, accessibles à ses élèves à partir du tutoriel, il pourra les télécharger, dans le chunk “setup” (en vérifiant par exemple s’ils sont déjà présents), vers le répertoire défini par la variable tempdir suivante. 

Ce répertoire temporaire propre au serveur d’hébergement peut-être rendu accessible en lecture dans tous les exercices. Par exemple, un fichier disponible en opendata peut être ainsi mis en cache sur le serveur une fois pour toutes, pour éviter des téléchargements répétés.

tempdir = str_c(rappdirs::user_data_dir(), "/temp")

Évaluer correctement la création d'une table
ou d'un graphique

S’il est aisé de tester un résultat numérique ou caractère, c’est un peu plus complexe quand il s’agit d’un objet élaboré comme une table, un graphique, un modèle, qu’un exercice demanderait de produire. 

Une première idée consiste à tester le code de l’élève en le confrontant au code de la “solution”. La fonction grade_this_code() réalise cela de façon assez intelligente. Elle ne se contente pas de comparer l’écriture mot à mot, ou à la virgule près, elle compare en réalité les versions compilées, sous forme d’un arbre logique, de chaque bloc de code. Elle est donc relativement flexible. Elle présente pourtant quelques défauts, à mes yeux rédhibitoires :

  • si l’élève, par excès de zèle est allé plus loin que la consigne, par exemple en affinant la présentation d’un graphique (axes, légendes, titres…), il sera sanctionné sans trop comprendre pourquoi ;
  • les messages d’erreur s’affichent en anglais (traduction non encore possible).

Je lui préfère donc une série de tests exogènes, validant les caractéristiques essentielles du résultat attendu. Ils reconnaissent donc la valeur du travail accompli par l’apprenant, quel que soit l’itinéraire qui lui a paru le plus pertinent.

Voici à nouveau deux cadres de tests concrets avec learnr et grade_this_code() / grade_this() :

Un graphique ggplot est un objet dont les propriétés sont accessibles. Voici par exemple le test sur la bonne définition des variables visuelles, qui s’accommode de différentes variantes, toutes justes, que les élèves ont pu imaginer :

# repérage du graphique ggplot
g = .result

# liste de toutes les variables visuelles et leur correspondance dans le dataframe
# qu'elles soient définies dans ggplot() ou dans geom_line()
l = c(map( g$mapping, all.vars), map(g$layers[[1]]$mapping, all.vars) )

if (!isTRUE( all.equal( sort(unlist(l)), c( colour = "dep", x = "mois", y = "rsa") ) )) {
fail("Vos variables visuelles ne sont pas encore celles attendues !")
}

Mettre à disposition un tutoriel learnr
sous forme d’un package ou d’une appli web

Construire et tester son tutoriel dans RStudio, c’est bien, le distribuer auprès de ses élèves, c’est mieux ! Et si possible sans que ceux-ci ne galèrent trop à le lancer sur leur ordinateur. Oubliez tout de suite le procédé consistant à leur transmettre le document source .rmd : les solutions y sont exposées, et le risque est grand que la bonne installation de toutes les librairies requises pour que votre tutoriel fonctionne prenne la moitié de votre cours…

Il reste deux cheminements viables :

  • faire de votre tutoriel un package, déposé sur Github, et donc téléchargeable ;
  • l’installer sur un hébergement Shiny, propre à votre organisme, ou sur shinyapps.io.

L’option “package” vous permet de ne pas dépendre d’un hébergement extérieur. Si tout se passe bien, tout l’environnement d’exécution se mettra en place à l’installation du package, avec les bonnes versions de librairies requises.

Pour autant, l’accès au tutoriel avec un simple navigateur est bien pratique, et de nature à toucher un public plus vaste, libre du matériel qu’il a sous la main pour se connecter. Il faut toutefois que le serveur hébergeant votre tutoriel tienne la charge, notamment si vous gérez 10 ou 20 élèves au même moment.
 

Il est enfin possible, avec la librairie learnhash, de récupérer la “copie” de chacun de vos élèves sous forme d’une chaine que vous pourrez décoder.

Pour découvrir comment procéder, veuillez consulter les ressources qui suivent.

Sélection de ressources utiles

2 commentaires sur “Peaufiner ses tutoriels R avec learnr et gradethis (2/2)”

  1. Bonjour Eric,

    Merci mille fois pour ce superbe article!!

    Je me débattais un peu avec gradethis et les exemples que j’avais trouvé jusqu’à maintenant ne suffisaient pas à me montrer la voie… les exemples que vous avez développés, en étant un peu plus complexes, m’ont permis de me débloquer!

    Lise

    1. Bonjour Lise,

      merci beaucoup pour cette réaction, venant de vous dont j’apprécie beaucoup l’expertise et le talent pédagogique, cela me touche particulièrement !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.