structuration code shiny + report

Postez ici vos questions, réponses, commentaires ou suggestions - Les sujets seront ultérieurement répartis dans les archives par les modérateurs

Modérateur : Groupe des modérateurs

Gabriel Terraz
Messages : 591
Enregistré le : 26 Sep 2011, 15:11

structuration code shiny + report

Messagepar Gabriel Terraz » 11 Déc 2020, 15:02

Bonjour à tous,

Je me suis lancé dans la création d'une appli shiny (c'est ma première...).
L'idée est que l'utilisateur :
- Choisisse un jeu de paramètres avec différents widgets
- Clique sur un bouton pour lancer les calculs (longs, ce sont des simus)
- Admire le tableau de résultat
- Télécharge un rapport (le tableau de résultat + autres graphes) sous format html

En piochant à droite à gauche j'ai pu me débrouiller, mais pour la génération du rapport je bloque.

Voici mon code (j'ai simplifié au maximum, mais cela reste long...) :

Code : Tout sélectionner

###############################################
###   Fonctions nécessaires                 ###
###############################################
source(file.path(path, "rnorm.R")) ## Je charge les fonctions nécessaires, disons qu'ici on charge la fonction rnorm()

###############################################
###   Application shiny                     ###
###############################################
ui <- fluidPage(
  # App title
  titlePanel("Stabilité : calcul de puissance"),
  # Sidebar layout
  sidebarLayout(
    # Sidebar
    sidebarPanel(
      # Input:
      numericInput("MOY", "Moyenne", value = 1200, min = 0, step = 0.01),
      sliderInput("SD", "Ecart-type", min = 6, max = 15, value = c(8, 10), step = 1),
      , width = 3),
     # Main panel for displaying outputs
    mainPanel(
      actionButton(inputId = "SubmitButton",label = " 1. Lancer le calcul", icon("paper-plane"),
                   style="color: #fff; width: 200px; background-color: #337ab7; border-color: #2e6da4"),
      downloadButton("report", " 2. Générer le rapport",),
      HTML("<br><br>"), ## Manually add vertical space
      # Output: Table summarizing the values entered and the results
      tableOutput("values")
      , width = 6)
  )
)

# Define server logic for slider examples
server <- function(input, output) {
  observeEvent(input$SubmitButton, {
    # Reactive expression to create data frame of all input values
    probaValues <- reactive({
      # Create a Progress object
      progress <- shiny::Progress$new()
      # Make sure it closes when we exit this reactive, even if there's an error
      on.exit(progress$close())
      progress$set(message = "Calcul en cours :", value = 0, detail = "Jeu de paramètres 1")
      ## "Traitement" des inputs (Une quizaine en réalité)
      MOY <- as.numeric(input$MOY)
      SD <- min(as.integer(input$SD)):max(as.integer(input$SD))
      ## Construction d'un dataframe avec toutes les combinaisons de paramètres
      param <- expand.grid(mean = MOY,
                           sd = SD,
                           n = 1,
                           stringsAsFactors = FALSE)
      n <- nrow(param)
      probas <- numeric(n)
      for(i in 1:n){
        PARAM <- lapply(param, "[[", i)
        probas[i] <- do.call(rnorm, PARAM)
        progress$inc(1/n, detail = paste("Jeu de paramètres", i + 1))
        Sys.sleep(2) ## Pour admirer ma barre de progression, mais en vrai c'est long
      }
      data.frame(moyenne = param$mean, ecart = param$sd, proba = probas, check.names=FALSE)
    })
   
    # Show the values in an HTML table ----
    output$values <- renderTable({
      isolate({probaValues()})
    }, digits = 3)
   
  })## End of observe event
}

# Create Shiny app ----
shinyApp(ui, server)


Voici ce que j'ai trouvé pour générer le rapport. L'idée est d'afficher output$values ainsi que de créer des graphes avec les résultats de deux-ci

Code : Tout sélectionner

output$report <- downloadHandler(
  # For PDF output, change this to "report.pdf"
  filename = "report.html",
  content = function(file) {
    # Copy the report file to a temporary directory before processing it, in
    # case we don't have write permissions to the current working dir (which
    # can happen when deployed).
    tempReport <- file.path(tempdir(), "report.Rmd")
    file.copy("report.Rmd", tempReport, overwrite = TRUE)

    # Set up parameters to pass to Rmd document
    params <- output$values

    # Knit the document, passing in the `params` list, and eval it in a
    # child of the global environment (this isolates the code in the document
    # from the code in this app).
    rmarkdown::render(tempReport, output_file = file,
                      params = params,
                      envir = new.env(parent = globalenv()))
  }
)


Avec un fichier Rmd qui va bien en template.

Pour le moment je n'arrive pas à faire fonctionner la création du rapport car je ne sais pas vraiment où le placer dans monde code.
Est-ce que vous auriez des pistes ?

Merci

Sébastien Rochette
Messages : 54
Enregistré le : 03 Juil 2020, 12:43
Contact :

Re: structuration code shiny + report

Messagepar Sébastien Rochette » 11 Déc 2020, 15:15

Bonjour,

Vous ne pouvez pas utiliser les "output" pour mettre dans le rapport. Ce qui est stocké dedans n'est pas un graph ou un tableau, mais les informations pour les afficher dans l'interface utilisateur.
Ce que vous pouvez faire, c'est stocker le contenu du code des "output" dans une "reactiveValues()" au moment de la création des objets. Ainsi vous avez des objets R classiques à manipuler et donc à insérer dans votre Rmd.

Ceci étant dit, en terme de méthodologie, il est plus prudent et organisé de commencer par réaliser votre code dans un Rmd, le transformer en fonction et utiliser ces fonctions dans la partie serveur de votre application Shiny. Sinon, au fur et à mesure que votre application va grossir, car ce sera forcément le cas, elle ne sera plus maintenable.
Par ailleurs, avec cette méthode, vous aurez déjà un code fonctionnel pour produire votre Rmd qui pourra être construit directement en ligne de commande si besoin.
Enfin, si les calculs que vous faites faire sont trop longs, vous atteindrez peut-être le timeout de Shiny et les résultats seront perdus. Il conviendrait alors d'opérer les calculs en deux temps et de laisser R travailler en arrière plan sans risque de perdre les travaux.

Je vous recommande la lecture de ce livre numérique (en anglais) pour une méthode recommandée de développement d'une application Shiny : https://engineering-shiny.org/
Et si jamais l'anglais est un problème, nous avons toute une série d'articles de blog sur le sujet : https://rtask.thinkr.fr/fr/shiny-fr/
Sébastien
Dev, Consult, Formateur
ThinkR

Gabriel Terraz
Messages : 591
Enregistré le : 26 Sep 2011, 15:11

Re: structuration code shiny + report

Messagepar Gabriel Terraz » 11 Déc 2020, 15:45

Merci d'avoir pris le temps de répondre.
Disons que j'ai pas vraiment le temps de me lancer dans des choses qui me paraissent compliquées et que ce n'est pas bien grave si l'appli n'est pas réalisée dans les règles de l'art.

Lorsque vous dites
Ce que vous pouvez faire, c'est stocker le contenu du code des "output" dans une "reactiveValues()" au moment de la création des objets. Ainsi vous avez des objets R classiques à manipuler et donc à insérer dans votre Rmd.


Comment cela se traduit concrètement à partir de mon magnifique code ? Je ne suis pas vraiment sûr de comprendre...

Fred Santos
Messages : 233
Enregistré le : 11 Avr 2009, 10:00
Contact :

Re: structuration code shiny + report

Messagepar Fred Santos » 11 Déc 2020, 17:11

(Je laisserai Sébastien compléter car je suis loin d'être un expert shiny moi-même)

Le problème se situe précisément quand tu fais :

Code : Tout sélectionner

 # Set up parameters to pass to Rmd document
params <- output$values

Comme le dit Sébastien, output$values ne contient pas un résultat en lui-même, c'est juste une déclaration formelle pour dire que dans ton UI, "values" doit afficher telle ou telle chose. À cet endroit de ton code, tu aurais le droit d'utiliser un input$quelquechose (qui, lui, est bien une valeur !), mais pas un output$quelquechose.
Il faudrait en fait que la valeur que tu souhaites passer ici (c'est-à-dire, basiquement, ton truc calculé dans probaValues()) soit préalablement stocké/retravaillé en tant que reactiveValue, c'est-à-dire en que valeur réellement utilisable à cet endroit du code.

Par ailleurs, je crois (Sébastien, tu confirmes ?) que c'est une mauvaise idée de wrapper tout ton server dans un gros observeEvent.

Un code qui semble fonctionner, chez moi :

Code : Tout sélectionner

###############################################
###   Application shiny                     ###
###############################################
ui <- fluidPage(
    # App title
    titlePanel("Stabilité : calcul de puissance"),
    # Sidebar layout
    sidebarLayout(
        # Sidebar
        sidebarPanel(
            # Input:
            numericInput("MOY", "Moyenne", value = 1200, min = 0, step = 0.01),
            sliderInput("SD", "Ecart-type", min = 6, max = 15, value = c(8, 10), step = 1),
            , width = 3),
        # Main panel for displaying outputs
        mainPanel(
            actionButton(inputId = "SubmitButton",label = " 1. Lancer le calcul", icon("paper-plane"),
                         style="color: #fff; width: 200px; background-color: #337ab7; border-color: #2e6da4"),
            downloadButton("report", " 2. Générer le rapport",),
            HTML("<br><br>"), ## Manually add vertical space
            # Output: Table summarizing the values entered and the results
            tableOutput("values")
            , width = 6)
    )
)

# Define server logic for slider examples
server <- function(input, output) {
    
    result 
<- reactiveValues(
        dtf = NULL
    
)
    
    observeEvent
(input$SubmitButton, {
        # Create a Progress object
        progress <- shiny::Progress$new()
        # Make sure it closes when we exit this reactive, even if there's an error
        on.exit(progress$close())
        progress$set(message = "Calcul en cours :", value = 0, detail = "Jeu de paramètres 1")
        ## "Traitement" des inputs (Une quizaine en réalité)
        MOY <- as.numeric(input$MOY)
        SD <- min(as.integer(input$SD)):max(as.integer(input$SD))
        ## Construction d'un dataframe avec toutes les combinaisons de paramètres
        param <- expand.grid(mean = MOY,
                             sd = SD,
                             n = 1,
                             stringsAsFactors = FALSE)
        n <- nrow(param)
        probas <- numeric(n)
        for(i in 1:n){
            PARAM <- lapply(param, "[[", i)
            probas[i] <- do.call(rnorm, PARAM)
            progress$inc(1/n, detail = paste("Jeu de paramètres", i + 1))
            Sys.sleep(2) ## Pour admirer ma barre de progression, mais en vrai c'est long
        }
        result$dtf <- data.frame(moyenne = param$mean,
                                 ecart = param$sd,
                                 proba = probas,
                                 check.names=FALSE)
    })
    
    
# Show the values in an HTML table ----
    output$values <- renderTable({
        result$dtf
    
}, digits = 3)
    
    output$report 
<- downloadHandler(
        # For PDF output, change this to "report.pdf"
        filename = "report.html",
        content = function(file) {
            # Copy the report file to a temporary directory before processing it, in
            # case we don't have write permissions to the current working dir (which
            # can happen when deployed).
            tempReport <- file.path(tempdir(), "report.Rmd")
            file.copy("report.Rmd", tempReport, overwrite = TRUE)
            
            
# Set up parameters to pass to Rmd document
            params <- list(dtf = result$dtf)
            
            
# Knit the document, passing in the `params` list, and eval it in a
            # child of the global environment (this isolates the code in the document
            # from the code in this app).
            rmarkdown::render(tempReport, output_file = file,
                              params = params,
                              envir = new.env(parent = globalenv()))
        }
    )
}

# Create Shiny app ----
shinyApp(ui, server)

Et pour ton Rmd :

Code : Tout sélectionner

---
title: "Dynamic report"
output: html_document
params:
  dtf: NULL
---

```{r}
# The `params` object is available in the document.
params$dtf
```

La table de résultats :

```{r}
params$dtf
```

CHez moi en tout cas ça roulotte tranquillement, j'espère que chez toi aussi ;-)

Sébastien Rochette
Messages : 54
Enregistré le : 03 Juil 2020, 12:43
Contact :

Re: structuration code shiny + report

Messagepar Sébastien Rochette » 11 Déc 2020, 18:15

La proposition de Fred illustre effectivement l'utilisation de la "reactiveValues()" dont je parlais et qui devrait vous sortir de l'impasse.

Je comprend que vous n'ayez pas le temps de faire les choses "dans les règles de l'art", mais le risque pour vous c'est d'accumuler de la dette technique sur votre application.
Du coup, vous allez continuer d'ajouter des choses dans votre application, qui vont marcher en apparence, mais qui vont être lourdes de conséquences pour votre futur vous. Shiny a une gestion de la réactivité qui pose rapidement problème : les mêmes opérations sont calculées plusieurs fois sans que vous vous en rendiez compte, au détour d'un clic ici ou là.
Par exemple, effectivement, ce n'est pas une bonne idée d'imbriquer des objets réactifs à l'intérieur d'objets réactifs. Vous vous en êtes rendus compte car vous avez été obligés de mettre un "isolate()" dans un "rendertable()" pour empêcher la réactivité.

J'espère que vous ne vous sentez pas agressé dans mes propos. J'essaie de vous indiquer que si vous vous lancer dans Shiny avec une mauvaise méthodologie, ça va se retourner contre vous à un moment ou un autre. Et avec Shiny, ce moment arrive généralement assez vite. Donc soyez très prudents.

Il y a pas mal de lecture dans ce que je vous ai donné, mais je peux le résumer ainsi:

1. Construire le code nécessaire dans un Rmd directement. Cela vous permet de tester le fonctionnement sans avoir à passer par tous les clics nécessaires.
2. Factoriser ce code en fonctions R pour en mettre le moins possible dans l'app Shiny elle-même
3. Créer votre partie UI sans serveur (ou pas trop), juste pour voir ce que donne l'apparence générale
4. Inclure vos fonctions aux endroits adéquats dans l'app
5. Renforcer le code. Exemples reproductibles, tests unitaires, documentation, versionnement du code, ... (Cette étape est mieux quand elle est faite en parallèle du code)

En plus, pour simplifier toutes les étapes de structuration de l'app, on a créé un package R nommé {golem} et qui est aussi libre et open-source : https://thinkr-open.github.io/golem/
Sébastien
Dev, Consult, Formateur
ThinkR

Gabriel Terraz
Messages : 591
Enregistré le : 26 Sep 2011, 15:11

Re: structuration code shiny + report

Messagepar Gabriel Terraz » 02 Fév 2021, 09:57

Bonjour à vous deux,

Désolé pour le long silence, je me remets seulement maintenant dans ce projet.
Merci Fred pour ton code, j'ai pu créer un html j'en ai les larmes aux yeux ;). Je vais maintenant reverser tout mon code dans le tiens, plutôt que d'adapter le mien (échec assuré).
Pour le moment la génération du rapport est extrêmement longue (plusieurs minutes). Peut être dû au VPN (...), à voir en local bien que je vois pas trop où le réseau serait sollicité...
Etape suivante : intégrer des graphiques.

Merci Sébastien pour toutes tes infos, je m'en servirais pour le prochain projet qui sera réalisé dans les règles de l'art du dieu shiny.


Retourner vers « Questions en cours »

Qui est en ligne

Utilisateurs parcourant ce forum : Aucun utilisateur enregistré et 1 invité