Shiny从入门到入定——3-在特别大的花园里面挖呀挖呀挖
发表于:2024-01-20 | 分类: IT
字数统计: 9k | 阅读时长: 35分钟 | 阅读量:

3 基础反应性

3.1 介绍

在 Shiny 中,您使用反应式编程来表达服务器逻辑。反应式编程是一种优雅而强大的编程范式,但起初可能会让人感到迷失,因为它与编写脚本的范式非常不同。反应式编程的关键思想是定义一个依赖图,这样当输入变化时,所有相关的输出都会自动更新。这使得应用程序的流程大大简化,但需要一段时间才能理解如何将所有内容整合在一起。

本章将为您提供对反应式编程的简单介绍,教授您 Shiny 应用中最常见的反应式结构的基本知识。我们将从服务器函数概览开始,更详细地讨论输入和输出参数的工作方式。接下来,我们将回顾最简单的反应形式(将输入直接连接到输出),然后讨论反应式表达式如何帮助您消除重复的工作。最后,我们将回顾一些新用户常见的障碍。

3.2 server 函数

正如您所见,每个 Shiny 应用程序的内部如下所示:

library(shiny)

ui <- fluidPage(
  # front end interface
)

server <- function(input, output, session) {
  # back end logic
}

shinyApp(ui, server)

上一章介绍了前端的基础知识,ui 对象包含向应用程序的每个用户呈现的 HTML。ui 很简单,因为每个用户都获得相同的 HTML。server 更复杂,因为每个用户都需要获得应用程序的独立版本;当用户 A 移动滑块时,用户 B 不应该看到他们的输出变化。

为了实现这种独立性,Shiny 在每次启动新会话时都会调用你的 server () 函数。就像其他任何 R 函数一样,当 server 函数被调用时,它会创建一个新的本地环境,该环境独立于该函数的每次其他调用。这使得每个会话都有一个唯一的状态,并隔离函数内创建的变量。这就是为什么你在 Shiny 中进行的几乎所有反应式编程都将在 server 函数内进行的原因。

server 函数有三个参数: inputoutputsession 。因为你永远不会自己调用服务器函数,所以你永远不会自己创建这些对象。相反,它们是由 Shiny 在会话开始时创建的,会话会返回到特定的会话。目前,我们将重点放在 input 和 output 参数上,而将 session 留到后面的章节。

3.2.1 输入

输入参数是一个类似列表的对象,其中包含从浏览器发送的所有输入数据,并根据输入 ID 命名。例如,如果您的 UI 包含一个输入 ID 为 count 的数字输入控件,如下所示:

ui <- fluidPage(
  numericInput("count", label = "Number of values", value = 100)
)

然后,您可以使用 input$count 访问该输入的值。它最初将包含值 100,并随着用户在浏览器中更改值而自动更新。

与典型的列表不同,输入对象是只读的。如果您尝试在 server 函数内修改输入,将会出现错误:

server <- function(input, output, session) {
  input$count <- 10  
}

shinyApp(ui, server)
#> Error: Can't modify read-only reactive value 'count'

发生此错误是因为输入反映了浏览器中正在发生的事情,而浏览器是 Shiny 的 “单一事实来源”。如果你能修改 R 中的值,你可能会引入不一致,其中输入滑块在浏览器中说了一件事,而 input$count 在 R 中说了不同的事情。这将使编程变得具有挑战性!稍后,在第 8 章中,您将学习如何使用 updateNumericInput() 之类的函数来修改浏览器中的值,然后 input$count 将相应地更新。

关于输入还有一件更重要的事情:它可以选择允许谁来读取它。要从输入中读取,你必须处于由 renderText()reactive() 等函数创建的响应式环境中。我们很快就会回到这个想法,但这是一个重要的约束,它允许输出在输入更改时自动更新。这段代码说明了如果你犯了这个错误,你会看到的错误:

server <- function(input, output, session) {
  message("The value of input$count is ", input$count)
}

shinyApp(ui, server)
#> Error: Can't access reactive value 'count' outside of reactive consumer.
#> ℹ Do you need to wrap inside reactive() or observer()?

3.2.2 输出

输出与输入非常相似:它也是一个根据输出 ID 命名的类似列表的对象。主要区别在于,您使用它来发送输出而不是接收输入。您总是将输出对象与渲染函数一起使用,如下面的简单示例:

ui <- fluidPage(
  textOutput("greeting")
)

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

(请注意,ID 在用户界面中被引用,但在服务器中没有引用 —— 不加引号。)

渲染函数做了两件事:

  • 它建立了一个特殊的反应式上下文,该上下文会自动跟踪输出所使用的输入。

  • 它将您的 R 代码的输出转换为适合在网页上显示的 HTML。

与输入一样,输出在使用上也是非常挑剔的。将会出现错误如果:

  • 您忘记使用渲染函数

    server <- function(input, output, session) {
        output$greeting <- "Hello human"
    }
    shinyApp(ui, server)
    #> Error: Unexpected character object for output$greeting
    #> ℹ Did you forget to use a render function?
  • 试图从输出中读取

    server <- function(input, output, session) {
        message("The greeting is ", output$greeting)
    }
    shinyApp(ui, server)
    #> Error: Reading from shinyoutput object is not allowed.

3.3 响应式编程

如果应用程序只有输入或只有输出,那么它将非常无聊。Shiny 的真正魔力在于当您拥有同时具有两者的应用程序时。让我们看一个简单的例子:

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

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

很难在书中展示这是如何工作的,但我在图 3.1 中尽力了。如果您运行应用程序并输入名称框,您会看到问候语会自动更新,就像您输入一样7

图3.1:响应性意味着输出会随着输入的变化而自动更新,就像我在这个应用程序中输入“J”,“o”,“e”一样。请访问https://hadley.shinyapps.io/ms-connection查看实时更新。

这是 Shiny 的一个核心概念:您不需要告诉输出何时更新,因为 Shiny 会自动为您解决。它是如何工作的呢?函数体中到底发生了什么?让我们更准确地思考服务器函数内部的代码:

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

很容易将此理解为 “将 hello 和用户名拼接在一起,然后将其发送到 output$greeting ”。但是这种思维模式在细微但重要的方面是错误的。仔细考虑一下:根据这种模型,您只发布一次指令。但是 Shiny 会在我们每次更新 input$name 时执行该操作,因此一定有更多的事情发生。

应用程序之所以能够工作,是因为代码并没有告诉 Shiny 创建字符串并将其发送到浏览器,而是告诉 Shiny 如果需要的话如何创建字符串。至于代码何时(甚至是否!)运行,这取决于 Shiny。它可能早在应用程序启动时就开始运行,也可能稍后运行;可能多次运行,也可能永远不运行!这并不是说 Shiny 反复无常,只是说决定何时执行代码是 Shiny 的责任,而不是您的责任。将您的应用程序视为提供食谱给 Shiny,而不是给它下命令。

3.3.1 命令式编程与声明式编程的区别

命令式编程和声明式编程是两种重要的编程风格,它们之间的区别在于:

  • 在命令式编程中,您发出特定的命令并立即执行。这是您在分析脚本中习惯的编程方式:您命令 R 加载数据、转换数据、可视化数据并将结果保存到磁盘。

  • 在声明式编程中,您表达了较高的目标或描述了重要的约束,并依靠其他人来决定如何以及 / 或何时将其转化为行动。这是您在 Shiny 中使用的编程风格。

使用命令式代码,您可以说 “给我做个三明治”8。使用声明式代码,您可以表达 “无论何时我打开冰箱,都要确保里面有一个三明治”。命令式代码是断言的;声明式代码是被动的。

大部分时间,声明式编程是非常自由的:您只需要描述您的整体目标,软件就会想出如何实现它们而无需进一步的干预。不利的一面是,偶尔您会确切地知道您想要什么,但您无法弄清楚如何以声明式系统理解的方式对其进行构架9。本书的目标是帮助您发展对底层理论的理解,以尽可能减少这种情况的发生。

3.3.2 懒惰

Shiny 中声明式编程的优势之一是应用程序可以非常懒惰。Shiny 应用程序只会做最少量工作来更新您当前可见的输出控件10。然而,这种懒惰有一个重要的缺点,您应该注意。你能找出下面 server 函数的问题吗?

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

如果您仔细观察,您可能会注意到我写的是 greting 而不是 greeting 。在 Shiny 中,这不会产生错误,但不会实现您的期望。greting 输出不存在,因此 renderText () 内部的代码永远不会被执行。

如果您正在开发 Shiny 应用程序,并且您就是无法理解为什么您的代码从未运行过,请仔细检查您的 UI 和服务器函数是否使用相同的标识符。

3.3.3 响应式图

Shiny 的懒惰性具有另一个重要属性。在大多数 R 代码中,您可以通过从上到下阅读代码来理解执行顺序。这在 Shiny 中不起作用,因为代码只在需要时才运行。要了解执行顺序,您需要查看响应式图,它描述了输入和输出之间的连接方式。上述应用程序的响应式图非常简单,如图 3.2 所示。

图3.2 响应式图显示了输入和输出之间的连接方式

响应式图包含每个输入和输出的一个符号,并且我们将输入连接到输出,每当输出访问输入时。这个图告诉您 greeting 将需要重新计算,每当 name 被更改时。我们通常将这种关系描述为 greeting 对 name 具有反应依赖性。

注意我们为输入和输出使用的图形约定:name 输入自然适合 greeting 输出。我们可以将它们紧密地排列在一起,如图 3.3 所示,以强调它们如何组合在一起;通常我们不会这样做,因为这只适用于最简单的应用程序。

响应式图中组件的形状表达了它们连接的方式。

图3.3 响应式图中组件的形状表达了它们连接的方式

响应式图是一个强大的工具,用于理解您的应用程序如何工作。随着应用程序变得越来越复杂,快速高层次概述响应式图通常很有用,以提醒您所有组件如何组合在一起。在本书中,我们将通过显示响应式图来帮助您理解示例的工作原理,稍后在第 14 章中,您将学习如何使用 reactlog,它将为您绘制图形。

3.3.4 反应式表达式

您将在响应式图中看到的另一个重要组件是反应式表达式。现在,您可以将其视为减少响应式代码重复的工具,通过在响应式图中引入额外的节点。

在我们的简单应用程序中,我们不需要反应式表达式,但我还是会添加一个,以便您可以了解它如何影响响应式图,如图 3.4 所示。

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

图3.4:由于反应式表达式连接输入和输出,因此在其两侧绘制角度。

反应式表达式接受输入并产生输出,因此它们具有结合输入和输出特征的形状。希望这些形状可以帮助您记住组件如何组合在一起。

3.3.5 执行顺序

了解代码的执行顺序是很重要的,因为执行顺序完全由响应式图决定。这与大多数 R 代码不同,在大多数 R 代码中,执行顺序由代码行的顺序决定。例如,我们可以翻转简单 server 函数中的两行代码的顺序:

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

您可能会认为这会产生错误,因为 output$greeting 引用了尚未创建的响应式表达式 string。但是请记住,Shiny 是惰性的,因此只有在会话开始后,在 string 被创建后,才会运行该代码。

相反,此代码产生与上述相同的响应式图,因此代码运行的顺序完全相同。像这样组织代码对于人类来说很混乱,最好避免。相反,请确保响应式表达式和输出仅引用上面定义的内容,而不是下面。这将使您的代码更易于理解。

这个概念非常重要,与大多数其他 R 代码不同,因此我要再说一遍:响应式代码运行的顺序仅由响应图确定,而不是由其在服务器函数中的布局决定。

3.3.6 练习题

3.3.6.1 给定如下 UI:

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

修复下面三个服务器函数中的简单错误。首先尝试通过阅读代码发现问题;然后运行代码以确保你已修复它。

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

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

server3 <- function(input, output, server) {
  output$greting <- paste0("Hello", input$name)
}

3.3.6.2 绘制以下 server 函数的反应图:

server1 <- function(input, output, session) {
  c <- reactive(input$a + input$b)
  e <- reactive(c() + input$d)
  output$f <- renderText(e())
}
server2 <- function(input, output, session) {
  x <- reactive(input$x1 + input$x2 + input$x3)
  y <- reactive(input$y1 + input$y2)
  output$z <- renderText(x() / y())
}
server3 <- function(input, output, session) {
  d <- reactive(c() ^ input$d)
  a <- reactive(input$a * 10)
  c <- reactive(b() / input$c) 
  b <- reactive(a() + input$b)
}

3.3.6.3 为何下述代码会失败?

var <- reactive(df[[input$var]])
range <- reactive(range(var(), na.rm = TRUE))

为什么 range () 和 var () 不是好的反应式命名?

3.4 反应表达式

我们已经在几个地方快速浏览了反应表达式,所以希望您已经了解了它们的作用。现在,我们将深入了解更多的细节,并说明为什么在构建真实应用程序时它们如此重要。

反应表达式之所以重要,是因为它们为 Shiny 提供了更多信息,以便在输入发生变化时减少重新计算,使应用程序更高效,并通过简化反应图来让人类更容易理解应用程序。反应表达式具有输入和输出的味道:

与输入类似,您可以在输出中使用反应表达式的计算结果。

与输出类似,反应表达式依赖于输入,并自动知道何时需要更新。

这种双重性意味着我们需要一些新的词汇:我将使用 生产者 来指代反应输入和表达式,而 消费者 则指代反应表达式和输出。图 3.5 用维恩图展示了这种关系。

图3.5 输入和表达式是反应性的生产者;表达式和输出是反应性的消费者

我们需要一个更复杂的应用程序来查看使用响应式表达式的优点。首先,我们将通过定义一些常规的 R 函数来为我们的应用程序提供支持,从而为我们的应用程序搭建舞台。

3.4.1 动机

假设我想用绘图和假设检验来比较两个模拟数据集。我做了一些实验,并提出了以下函数: freqpoly() 用频率多边形12可视化两个分布, t_test() 使用 t 检验来比较均值,并用字符串总结结果:

library(ggplot2)

freqpoly <- 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, colour = g)) +
    geom_freqpoly(binwidth = binwidth, size = 1) +
    coord_cartesian(xlim = xlim)
}

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

译者注:

这段代码中定义了两个函数,freqpoly 和 t_test。以下是它们的简单解释:

  1. freqpoly 函数:
    这个函数是为了对比两个数据集 x1 和 x2 的分布。它首先将这两个数据集合并到一个数据框中,并为每个数据集分配一个组别 "x1" 和 "x2"。然后,它使用 ggplot2 包的 geom_freqpoly 函数绘制频数多边形图,这个图能够展示数据的分布情况。 binwidth 参数决定了分箱的宽度,而 xlim 参数则设定了 x 轴的显示范围。

  2. t_test 函数:
    这个函数执行 t 检验,这是一种用于比较两组平均值是否有显著差异的统计检验方法。函数接受两个向量 x1 和 x2 作为输入,然后使用 R 的内置 t.test 函数执行 t 检验。然后,它使用 sprintf 函数以一种格式化的方式返回结果,包括 p 值以及 95% 置信区间。

    注意:在这段代码中, sprintf 函数用于格式化输出字符串。"%0.3f" 表示浮点数,保留 3 位小数;"[%0.2f, %0.2f]" 表示浮点数,保留 2 位小数,用于显示置信区间。

如果我有一些模拟数据,我可以使用这些函数来比较两个变量:

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

freqpoly(x1, x2)
cat(t_test(x1, x2))
#> p value: 0.005
#> [-0.39, -0.07]

在真正的分析中,你可能在完成这些函数之前已经做了一堆探索。我在这里跳过了这个探索,这样我们就可以尽快地进入应用程序。但是将命令式代码提取到常规函数中是所有 Shiny 应用程序的一项重要技术:你从应用程序中提取的代码越多,就越容易理解。这是很好的软件工程,因为它有助于隔离问题:应用程序外的函数专注于计算,这样应用程序内的代码就可以专注于响应用户操作。我们将在第 18 章再次回到这个想法。

3.4.2 应用程序

我想使用这两个工具快速探索一系列模拟。Shiny 应用程序是一种很好的方法,因为它可以让你避免繁琐地修改和重新运行 R 代码。下面我将这些部分打包成一个 Shiny 应用程序,在这里我可以交互式地调整输入。

让我们从 UI 开始。我们将在第 6.2.3 节中详细介绍 fluidRow()column() 的作用;但是你可以从它们的名字中猜出它们的目的。第一行有三个用于输入控件的列(distribution 1、distribution 2 和绘图控件)。第二行有一个宽列用于绘图,一个窄列用于假设检验。

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,
      "Frequency polygon",
      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 函数从指定的分布中抽取后,结合了 freqpoly () 和 t_test () 函数的调用:

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

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

图3.6:一个Shiny应用程序,可让您使用t检验和频数多边形比较两个模拟分布。请访问https://hadley.shinyapps.io/ms-case-study-1以查看实时效果。

这段定义服务器和用户界面的内容产生了图 3.6。你可以在 https://hadley.shinyapps.io/ms-case-study-1 找到它的活跃版。我推荐你打开应用程序并进行快速操作,以确保在继续阅读之前理解其基本操作。

3.4.3 反应式图表

让我们开始绘制这个应用程序的反应式图表。Shiny 足够智能,仅在引用的输入发生更改时更新输出;它不够智能,不能仅选择性地运行输出内部的代码片段。换句话说,输出是原子的:它们要么整体执行,要么不执行。

例如,以下是从 server 中获取的代码片段:

x1 <- rnorm(input$n1, input$mean1, input$sd1)
x2 <- rnorm(input$n2, input$mean2, input$sd2)
t_test(x1, x2)

作为阅读此代码的人,你可以看出我们只需要在 n1、mean1 或 sd1 发生变化时更新 x1,在 n2、mean2 或 sd2 发生变化时更新 x2。然而,Shiny 只将输出视为一个整体,因此每当 n1、mean1、sd1、n2、mean2 或 sd2 中的一个发生变化时,它都会更新 x1 和 x2。这导致了图 3.7 所示的反应图。

图3.7 反应图显示,每一个输出都取决于每一个输入

你会注意到,图中非常密集:几乎每个输入都直接连接到每个输出。这造成了两个问题:

  • 应用程序难以理解,因为有太多的连接。您无法将应用程序的任何部分单独拉出来进行分析。

  • 应用程序效率低下,因为它做了很多不必要的工作。例如,如果您更改了图的断点,将重新计算数据;如果您更改了 n1 的值,将更新 x2(在两个地方!)。

应用程序还有一个其他主要缺陷:频率多边形和 t 检验使用单独的随机抽样。这可能会误导,因为您期望它们在同一基础数据上工作。

幸运的是,我们可以通过使用反应式表达式来提取重复的计算,从而解决所有这些问题。

3.4.4 简化图表

在 server 函数中,我们将现有代码重构为两个新的反应式表达式 x1 和 x2,它们模拟来自两个分布的数据。要创建反应式表达式,我们调用 reactive() 并将结果分配给一个变量。要稍后使用该表达式,我们将变量称为函数进行调用。

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({
    freqpoly(x1(), x2(), binwidth = input$binwidth, xlim = input$range)
  }, res = 96)

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

这种转换产生了图 3.8 所示的实质上更简单的图表。这个更简单的图表使得应用程序更容易理解,因为您可以将连接组件分开理解;分布参数的值只会通过 x1 和 x2 影响输出。这种重写还使应用程序更高效,因为它减少了大量的计算。现在,当您更改 binwidth 或 range 时,只有图表发生变化,而不是基础数据。

图3.8 使用反应式表达式大大简化了图表,使其更容易理解

为了强调这种模块化,图 3.9 在独立组件周围画了框。我们将在第 19 章讨论模块时回到这个想法。模块允许您提取重复的代码以进行重用,同时保证其与应用程序中的其他所有内容隔离。对于更复杂的 app,模块是一种非常有用且强大的技术。

图3.9 模块强制隔离应用程序的各个部分

您可能熟悉编程中的 “三次规则”:无论何时您复制和粘贴某些内容三次,您应该想办法减少重复(通常通过编写函数)。这是很重要的,因为它减少了代码中的重复量,使代码更容易理解,并且随着您的需求变化而更容易更新。

然而,我认为在 Shiny 中,您应该考虑 “一次规则”:无论何时您复制和粘贴某些内容一次,您应该考虑将重复的代码提取到反应式表达式中。对于 Shiny 来说,这个规则更加严格,因为反应式表达式不仅使人类更容易理解代码,而且也提高了 Shiny 有效地重新运行代码的能力。

3.4.5 我们为什么需要反应式表达式?

当您刚开始使用反应式代码时,您可能会想知道为什么我们需要反应式表达式。您为什么不使用现有的工具来减少代码中的重复:创建新变量和编写函数?不幸的是,这两种技术在反应式环境中都不起作用。

如果您尝试使用变量来减少重复,您可能会编写类似这样的代码:

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

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

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

如果您运行此代码,则会收到错误,因为您尝试在反应式上下文之外访问输入值。即使您没有收到该错误,您仍然会遇到问题:x1 和 x2 只会在会话开始时计算一次,而不是每次输入之一更新时都会计算。

如果您尝试使用函数,则应用程序将工作:

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

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

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

但它与原始代码有同样的问题:任何输入都会导致所有输出被重新计算,并且 t 检验和频率多边形将在单独的样本上运行。响应式表达式会自动缓存其结果,并且仅在输入更改时才进行更新13

变量只计算一次值(粥太凉了),函数在每次调用时计算值(粥太烫了),而响应式表达式只在值可能发生变化时计算值(粥刚刚好!)。

3.5 控制评估时间

现在你已经熟悉了反应性的基本概念,我们将讨论两种更高级的技术,它们可以让你增加或减少反应性表达式的执行频率。在这里,我将展示如何使用基本技术;在第 15 章中,我们将回到它们的底层实现。

为了探索基本思想,我将简化我的模拟应用程序。我将使用只有一个参数的分布,并强制两个样本共享相同的 n。我还将删除绘图控件。这将产生更小的 UI 对象和 server 功能:

ui <- fluidPage(
  fluidRow(
    column(3, 
      numericInput("lambda1", label = "lambda1", value = 3),
      numericInput("lambda2", label = "lambda2", value = 5),
      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({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}

这生成了如图 3.10 所示的应用程序和如图 3.11 所示的反应图。

图3.10 一个更简单的应用程序,显示从两个泊松分布中抽取的随机数的频率多边形。参见 live 在线在https://hadley.shinyapps.io/ms-simulation-2。

图3.11 反应图

3.5.1 定时失效

想象一下,如果你想通过不断重新模拟数据来强调这是模拟数据的事实,以便你看到动画而不是静态图14。我们可以使用新的函数: reactiveTimer() 来增加更新的频率。

reactiveTimer() 是一个反应式表达式,它依赖于一个隐藏的输入:当前时间。当您想要一个反应式表达式比其他方式更频繁地使自己无效时,您可以使用 reactiveTimer() 。例如,以下代码使用 500 毫秒的间隔,这样图将每两秒钟更新一次。这足够快,以提醒你正在查看模拟,而不会因快速变化而使你眼花缭乱。这一变化产生了如图 3.12 所示的反应图。

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({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}

图3.12 reactiveTimer(500)引入了一个新的反应式输入,该输入每半秒自动使自己无效一次

注意我们在计算 x1 () 和 x2 () 的反应式表达式中使用 timer () 的情况:我们调用它,但不使用它的值。这使得 x1 和 x2 可以对 timer 产生反应式依赖,而不必担心它返回的确切值。

3.5.2 点击事件

在上述场景中,想象一下如果模拟代码需要 1 秒钟才能运行完会发生什么情况。我们每 0.5 秒执行一次模拟,所以 Shiny 会有越来越多的工作要做,而且永远无法追赶上来。同样的问题也可能发生在如果有人在您的应用程序中快速点击按钮,而您正在进行的计算相对昂贵的情况下。这可能会给 Shiny 创建大量的后台工作,当 Shiny 忙于处理这些后台工作时,它无法响应任何新的事件,从而导致用户体验不佳。

如果这种情况发生在您的应用程序中,您可能希望要求用户选择执行昂贵的计算,要求他们点击一个按钮。这是 actionButton() 的一个很好的用例:

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

要使用操作按钮,我们需要学习一种新工具。为了理解原因,让我们首先使用与上述相同的方法来解决这个问题。与上面一样,我们提到 simulate 但不使用它的值来对其产生反应式依赖。

server <- function(input, output, session) {
  x1 <- reactive({
    input$simulate
    rpois(input$n, input$lambda1)
  })
  x2 <- reactive({
    input$simulate
    rpois(input$n, input$lambda2)
  })
  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}

图3.13 具有操作按钮的应用程序。参见 live 在线在https://hadley.shinyapps.io/ms-action-button。

图3.14 这个反应图没有实现我们的目标;我们添加了一个依赖项,而不是取代了现有的依赖项。

这产生了图 3.13 的应用程序和图 3.14 的反应图。这没有达到我们的目标,因为它只是引入了一个新的依赖项:当我们点击 simulate 按钮时, x1()x2() 将更新,但它们将继续在 lambda1lambda2n 改变时更新。我们希望取代现有的依赖项,而不是增加新的依赖项。

为了解决这个问题,我们需要一个新的工具:一种使用输入值而不产生反应式依赖的方法。我们需要 eventReactive() ,它有两个参数:第一个参数指定要对其产生依赖的参数,第二个参数指定要计算的内容。这让应用程序只有在 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({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}

图 3.15 显示了新的反应图。请注意,如所期望的那样, x1x2 不再对 lambda1lambda2n 产生反应式依赖:改变它们的值不会触发计算。我保留了浅灰色的箭头,只是为了提醒您 x1 和 x2 继续使用这些值,但不再对其产生反应式依赖。

图3.15 eventReactive()使分离依赖项(黑色箭头)和使用来计算结果的值(浅灰色箭头)成为可能。

3.6 观察者

到目前为止,我们一直关注应用程序内部发生的事情。但有时,您需要将应用程序外部的某些操作与应用程序内部的操作关联起来,以在其他地方产生副作用。这可能是将文件保存到共享网络驱动器、向 Web API 发送数据、更新数据库,或者(最常见)是在控制台上打印调试消息。这些操作不会影响应用程序的外观,因此您不应该使用输出和渲染函数。相反,您需要使用观察者(observer)。

有多种创建观察者的方法,我们将在第 15.3 节中讨论它们。现在,我想向您展示如何使用 observeEvent() ,因为它在您学习 Shiny 时为您提供了一个重要的调试工具。

observeEvent()eventReactive() 非常相似。它有两个重要的参数: eventExprhandlerExpr 。第一个参数是输入或表达式,对其产生依赖;第二个参数是执行的代码。例如,对 server() 进行以下修改意味着每当 name 被更新时,都会向控制台发送一条消息:

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

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

observeEvent()eventReactive() 之间有两个重要的区别:

  • 您不需要将 observeEvent() 的结果分配给一个变量,因此

  • 您不能从其他反应式消费者中引用它。

观察者和输出密切相关。您可以将输出视为具有特殊副作用:更新用户浏览器中的 HTML。为了强调这种紧密关系,我们将在反应图上以相同的方式绘制它们。这产生了如图 3.16 所示的反应图。

图3.16 在反应图中,观察者看起来与输出相同

3.7 总结

本章应有助于您更好地了解 Shiny 应用程序的后端,即响应用户操作的 server() 代码。您还初步掌握了支撑 Shiny 的响应式编程范式。在这里您所学到的东西将会对您大有帮助;我们将在第 13 章中回到底层理论。响应式编程非常强大,但它与您最习惯的 R 编程命令式风格截然不同。如果需要一段时间才能理解所有后果,请不要感到惊讶。

本章结束了我们对 Shiny 基础的概述。下一章将通过创建一个更大的 Shiny 应用程序来帮助您练习迄今为止所学的材料,该应用程序旨在支持数据分析。

关注

关注公众号 “生信之巅”。

生信之巅微信公众号生信之巅小程序码

敬告:使用文中脚本请引用本文网址,请尊重本人的劳动成果,谢谢!Notice: When you use the scripts in this article, please cite the link of this webpage. Thank you!

上一篇:
Shiny从入门到入定——4-挖坑完毕之案例研究
下一篇:
Shiny从入门到入定——2-在大大的花园里面挖呀挖呀挖
本文目录
本文目录