Shiny学习笔记:响应式编程-2

Reactive Programming

只有输入或者只有输出的Shiny App是十分无聊的,当一个Shiny App既有输入有有输出,就十分有魅力了:

ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
  output$greeting <- renderText({
    paste0("Hello ", input$name, "!")
  })
}

这就是Shiny魅力所在,你无需告诉输出何时更新,Shiny会自动帮你完成更新。

记住一点,Shiny App只是提供recipesShiny,而不是commands

Imperative vs declarative programming

命令式编程与声明式编程之间的关键区别就是recipescommands的区别:

  • 命令式编程:你敲入一行代码,立即运行,就像在R里面加载数据、转换数据、可视化以及保存结果等

  • 声明式编程:你只是传递更高一级的指令,依赖于别人决定如何何时行动,这就是Shiny的编程方式。

Laziness

Shiny声明式编程的一个优势允许app极度懒惰。Shiny只会做一小部分结果需要更新的工作,其它不必做的一概不做,比如下面例子:

server <- function(input, output, session) {
  output$greetnig <- renderText({
    paste0("Hello ", input$name, "!")
  })
}

就是greetning写错了,Shiny是不会报这个错的,但是你无法做任何你想做的事,因为greetning不存在的,renderText()里面的代码永远不会运行。所以多检查你的app是否存在拼写错误。

The reactive graph

我们平时运行代码的时候是从上到下依次运行,但是Shiny不是这样工作的,代码只是在需要的时候才运行。

这是一个很简单的reactive graph,如果name发生变化,greeting需要从新运行。

Reactive expressions

Shiny代码的执行顺序只由reactive graph决定,更代码在server函数中的顺序无关:

server <- function(input, output, session) {
  output$greeting <- renderText(text())
  text <- reactive(paste0("Hello ", input$name, "!"))
}

server <- function(input, output, session) {
  text <- reactive(paste0("Hello ", input$name, "!"))
  output$greeting <- renderText(text())
}

是一样的。

上面我们讲过,为什么reactive expression对于Shiny如此重要:

  • 尽可能多地提供Shiny信息,这样当输入变化时,Shiny尽量少运行计算,Shiny App更高效;

  • 通过简化reactive graphShiny App更易理解。

下面通过一个更复杂的app来理解reactive expression

假设我们需要通过图表以及假设检验来比较两个数据集,先定义两个函数:

  • histogram():在一个直方图中可视化两个数据集分布

  • t_test():比较均值及其它值

library(ggplot2)

histogram <- function(x1, x2, binwidth = 0.1, xlim = c(-3, 3)) {
  df <- data.frame(
    x = c(x1, x2),
    g = c(rep("x1", length(x1)), rep("x2", length(x2)))
  )

  ggplot(df, aes(x, fill = g)) +
    geom_histogram(binwidth = binwidth) +
    coord_cartesian(xlim = xlim)
}

t_test <- function(x1, x2) {
  test <- t.test(x1, x2)
  
  sprintf(
    "p value: %0.3f\n[%0.2f, %0.2f]",
    test$p.value, test$conf.int[1], test$conf.int[2]
  )
}

我们模拟两个数据集测试一下两个函数:

x1 <- rnorm(100, mean = 0, sd = 0.5)
x2 <- rnorm(200, mean = 0.15, sd = 0.9)

histogram(x1, x2)
cat(t_test(x1, x2))
#> p value: 0.006
#> [-0.36, -0.06]

Shiny app开发中尽快能多地从app中提取出代码,app中的代码只负责响应用户点击,app外的函数专门负责运行计算。

下面我们将上述函数封装成一个app,这样我们可以很方便地测试大量的数据集:

根据上面的histogram()函数可以知道,我们需要设计生成两个数据集的输入,每个数据集由n,mean,sd组成,还有一个控制绘图的输入,binwidthrange;两个输出:直方图输出,文本输出,根据我们前面学习的UI设计,我们可以很快就写好UI部分:

ui <- fluidPage(
  fluidRow(
    column(4, 
      "Distribution 1",
      numericInput("n1", label = "n", value = 1000, min = 1),
      numericInput("mean1", label = "µ", value = 0, step = 0.1),
      numericInput("sd1", label = "σ", value = 0.5, min = 0.1, step = 0.1)
    ),
    column(4, 
      "Distribution 2",
      numericInput("n2", label = "n", value = 1000, min = 1),
      numericInput("mean2", label = "µ", value = 0, step = 0.1),
      numericInput("sd2", label = "σ", value = 0.5, min = 0.1, step = 0.1)
    ),
    column(4,
      "Histogram",
      numericInput("binwidth", label = "Bin width", value = 0.1, step = 0.1),
      sliderInput("range", label = "range", value = c(-3, 3), min = -5, max = 5)
    )
  ),
  fluidRow(
    column(9, plotOutput("hist")),
    column(3, verbatimTextOutput("ttest"))
  )
)

server就是调用上面写好的函数,输出结果:

server <- function(input, output, session) {
  output$hist <- renderPlot({
    x1 <- rnorm(input$n1, input$mean1, input$sd1)
    x2 <- rnorm(input$n2, input$mean2, input$sd2)
    
    histogram(x1, x2, binwidth = input$binwidth, xlim = input$range)
  })

  output$ttest <- renderText({
    x1 <- rnorm(input$n1, input$mean1, input$sd1)
    x2 <- rnorm(input$n2, input$mean2, input$sd2)
    
    t_test(x1, x2)
  })
}

我们来理解一下这个appreactive graphShiny将整个输出视为一个整体,所以只要n1,mean1,sd1,n2,mean2,sd2中的任何一个发生变化,x1,x2都会自动变化,其reactive graph如下:

可以看到reactive graph之间十分紧密,几乎每一个输入都直接关联输入,这就带来两个问题:

  • app之间联系太过紧密,无法隔离开来进行分析

  • app十分低效,因为运行了太多不必要的计算。

我们可以使用reactive expression来避免这些问题,reactive()函数将结果赋值给变量x1,x2。

server <- function(input, output, session) {
  x1 <- reactive(rnorm(input$n1, input$mean1, input$sd1))
  x2 <- reactive(rnorm(input$n2, input$mean2, input$sd2))

  output$hist <- renderPlot({
    histogram(x1(), x2(), binwidth = input$binwidth, xlim = input$range)
  })

  output$ttest <- renderText({
    t_test(x1(), x2())
  })
}

这样就使得reactive graph更简化了:

可以看到,当binwidth以及range变化时,只有图是会从新绘制的,数据集是不会变化的,x1x2也只被相应的输入影响。

复制粘贴一段代码超过三次,那么你应该写个函数,但是在Shiny里只要复制粘贴一段代码超过一次,你应该将之写成reactive expression

控制执行次数

我们知道输出会自动随着输入的变化而变化,但是有的时候我们希望当输入变化时输出不要立即变化,但我们需要输出变化时再变化。我们举个例子:

ui <- fluidPage(
  fluidRow(
    column(3, 
      numericInput("lambda1", label = "lambda1", value = 3),
      numericInput("lambda2", label = "lambda2", value = 3),
      numericInput("n", label = "n", value = 1e4, min = 0)
    ),
    column(9, plotOutput("hist"))
  )
)
server <- function(input, output, session) {
  x1 <- reactive(rpois(input$n, input$lambda1))
  x2 <- reactive(rpois(input$n, input$lambda2))
  output$hist <- renderPlot({
    histogram(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  })
}

这里我们介绍一个函数reactiveTimer()来控制一定时间内自动更新的次数,下面的代码设置每分钟更新两次,所以我们不点击任何按钮,图形一直在变,是个动态图。

server <- function(input, output, session) {
  timer <- reactiveTimer(500)
  
  x1 <- reactive({
    timer()
    rpois(input$n, input$lambda1)
  })
  x2 <- reactive({
    timer()
    rpois(input$n, input$lambda2)
  })
  
  output$hist <- renderPlot({
    histogram(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  })
}

点击

想象一下,当用户不断地点击按钮,服务器端会积压大量的工作,进而导致app响应迟缓,用户体验就十分差了,如果我们设置一个运行按钮,只有输入变化且用户点击了运行按钮,app才会更新,这样就可以节省资源,提高用户体验。这个功能可以由actionButton()实现:

ui <- fluidPage(
  fluidRow(
    column(3, 
      numericInput("lambda1", label = "lambda1", value = 3),
      numericInput("lambda2", label = "lambda2", value = 3),
      numericInput("n", label = "n", value = 1e4, min = 0),
      actionButton("simulate", "Simulate!")
    ),
    column(9, plotOutput("hist"))
  )
)

要实现,只有当用户点击Simulate!按钮时,app才运行计算,我们还需要一个新的函数eventReactive()来实现,eventReactive()有两个参数:第一个参数指定哪个依赖,第二个参数指定运行哪些代码,下面的代码允许app只在simulate被点击之后,才运行x1()x2()

server <- function(input, output, session) {
  x1 <- eventReactive(input$simulate, {
    rpois(input$n, input$lambda1)
  })
  x2 <- eventReactive(input$simulate, {
    rpois(input$n, input$lambda2)
  })

  output$hist <- renderPlot({
    histogram(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  })
}

reactive graph如下:

x1,x2不在响应依赖lambda1lambda2以及n,这三个输入的变化不会启动计算运行。

Observers

目前为止我们只专注于app内部发生了什么,但有时我们需要关注app外面的变化:保存文件,发送数据到API,更新数据库,打印调试信息等,这些不会影响app的外观,你无法使用render来输出,此时就要用到observer。这里只简单介绍如何使用observerEvent()observerEvent()是一种非常重要的debug工具,observerEvent()eventReactive()十分相似,有两个参数:eventExpr以及handlerExpr,第一个参数是输入或者表达式的依赖项,第二个参数是需要运行的代码。比如下面的例子表示每次name更新的时候,都会向后台发送信息Greeting performed

server <- function(input, output, session) {
  text <- reactive(paste0("Hello ", input$name, "!"))
  
  output$greeting <- renderText(text())
  observeEvent(input$name, {
    message("Greeting performed")
  })
}

observerEvent()eventReactive()有两个重要区别:

  • 无需将observerEvent()赋值给变量
  • 因此就无法从其它用户获取

observer与输出紧密相关。

ReactiveShiny十分重要的部分,后续还需要不断在实践中加强理解。

参考:https://mastering-shiny.org/basic-reactivity.html

Researcher

I am a PhD student of Crop Genetics and Breeding at the Zhejiang University Crop Science Lab. My research interests covers a range of issues:Population Genetics Evolution and Ecotype Divergence Analysis of Oilseed Rape, Genome-wide Association Study (GWAS) of Agronomic Traits.

comments powered by Disqus