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只是提供
recipes
给Shiny,而不是commands
Imperative vs declarative programming
命令式编程与声明式编程之间的关键区别就是recipes
与commands
的区别:
-
命令式编程:你敲入一行代码,立即运行,就像在
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 graph
,Shiny 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
组成,还有一个控制绘图的输入,binwidth
,range
;两个输出:直方图输出,文本输出,根据我们前面学习的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)
})
}
我们来理解一下这个app
的reactive graph
,Shiny将整个输出视为一个整体,所以只要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
变化时,只有图是会从新绘制的,数据集是不会变化的,x1
,x2
也只被相应的输入影响。
复制粘贴一段代码超过三次,那么你应该写个函数,但是在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
不在响应依赖lambda1
,lambda2
以及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
与输出紧密相关。
Reactive
是Shiny十分重要的部分,后续还需要不断在实践中加强理解。
参考:https://mastering-shiny.org/basic-reactivity.html