
Shiny是一個(gè)網(wǎng)頁端app,所以得同時(shí)滿足多個(gè)用戶的獨(dú)立操作。不能因?yàn)樾修改了輸入導(dǎo)致小B想要看的結(jié)果出現(xiàn)了錯(cuò)誤。所以會(huì)用到reactivity來保證流程獨(dú)立。
2.1 服務(wù)器
服務(wù)器兩個(gè)重要也是最基本的功能就是輸入和輸出。
2.1.1 Input 輸入
ui <- fluidPage(
numericInput("count", label = "Number of values", value = 100)
)
這是一個(gè)最常見的輸入模式。默認(rèn)值是100。
但是server端的話就不能給input指定數(shù)值了,因?yàn)樵趕erver端input是只讀參數(shù)。強(qiáng)行寫入數(shù)值的話會(huì)返回錯(cuò)誤。
server <- function(input, output, session) {
input$count <- 10
}
shinyApp(ui, server)
#> Error: Can't modify read-only reactive value 'count'
這個(gè)錯(cuò)誤是因?yàn)閕nput只會(huì)反饋瀏覽器的數(shù)值,當(dāng)你強(qiáng)行修改server內(nèi)部input的時(shí)候就會(huì)造成混亂。需要用到類似updateNumericInput()的函數(shù)來讓sever自動(dòng)更新。
2.1.2 Output輸出
和輸入很相似。一定也要用render函數(shù)。
ui <- fluidPage(
textOutput("greeting")
)
server <- function(input, output, session) {
output$greeting <- renderText("Hello human!")
}
- render的功能
- 自動(dòng)更新input和output
- 把R code轉(zhuǎn)換成html格式
和input一樣,如果忘了render函數(shù)或者是圖直接讀取的話會(huì)報(bào)錯(cuò)。
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.
2.2 Reactive程序
ui <- fluidPage(
textInput("name", "What's your name?"), #注意到有個(gè)逗號(hào)
textOutput("greeting")
)
server <- function(input, output, session) {
output$greeting <- renderText({
paste0("Hello ", input$name, "!")
})
}

textInput是設(shè)置UI輸入界面,并且給輸入指定變量”name”。下面一行textOutput是給輸出指定變量。這里的變量名就是”greeting”。如果沒有textInput的話那每次顯示的東西都變成了固定的,沒法進(jìn)行UI互動(dòng)了。
對于reactive,書里的定義比較復(fù)雜。理解成可以用來生成介于input和output之間的中間變量的函數(shù)。
2.3 Reactive Graph
原著中給出了reactive graph的概念,就是reactive的流程圖。比方說剛才的例子就可以變成,

我們可以說greeting 和 name 之間有reactive依存關(guān)系。
可以接下去看一個(gè)例子

server <- function(input, output, session) {
string <- reactive(paste0("Hello ", input$name, "!"))
output$greeting <- renderText(string()) ## 注意string的屬性是reactive, 所以是string()
}
這個(gè)例子在簡單的reactive里看不出什么作用,等以后編寫復(fù)雜的reactive的時(shí)候就可以大幅度減少代碼重復(fù),提高效率了。
- 練習(xí)
把下面的代碼黏貼給ui, 然后修改4個(gè)server的錯(cuò)誤
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)
}
2.3 Reactivity 表現(xiàn)
文中舉了一個(gè)比較復(fù)雜的例子。比較兩組數(shù)據(jù)的和密度圖,并進(jìn)行t-test。試著把代碼改編成Shiny App.
下面是正常R的實(shí)現(xiàn)方法。
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]
)
}
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.016
#> [-0.35, -0.04]

然后是改成shiny。
首先是ui端,可以從文面大概猜到fluidRow column 的大概用法,之后會(huì)花篇幅詳細(xì)介紹。
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 <- 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)
})
}

https://hadley.shinyapps.io/ms-case-study-1 部署在云端服務(wù)器的效果。
代碼行數(shù)有點(diǎn)多,其實(shí)把變量之間的關(guān)系稍做整理然后可視化一下就會(huì)清楚很多。

稍做觀察就不難看出變量之間的關(guān)系很密切。這就造成了兩個(gè)問題。
- 因?yàn)殛P(guān)系網(wǎng)太密,所以導(dǎo)致這個(gè)app比較難理解。沒法單獨(dú)提取app里面的變量進(jìn)行分析
- app計(jì)算效率不高。每修改一個(gè)變量都會(huì)導(dǎo)致整體計(jì)算全部重來。
所以可以對這個(gè)app進(jìn)行優(yōu)化。在前面套一個(gè)reactive函數(shù),讓x1,x2變成了可活動(dòng)的變量。這樣就不會(huì)在x1或者x2發(fā)生改變的時(shí)候重新計(jì)算整個(gè)流程,而是僅更新發(fā)生變化的地方。
Reactivity里的變量需要加(),表示是活動(dòng)的函數(shù),不是固定的value。
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())
})
}

其實(shí)這里還涉及到了組快化的概念。如下圖所示

其實(shí)x1, x2都被組塊話了,組塊話這個(gè)概念在之后的篇幅里會(huì)詳細(xì)介紹。
2.4 Timer功能
Shiny里還有定時(shí)激活功能。
在下面的程序里添加Timer也就是定時(shí)自動(dòng)激活功能。通過觀察代碼可以看出這是一段隨機(jī)數(shù)生成程序。自動(dòng)激活就等于自動(dòng)重新生成隨機(jī)數(shù)。
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)
}

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)
}
這樣就成功變成了下面的模式。每隔默認(rèn)的半秒鐘程序就會(huì)自動(dòng)運(yùn)行一次。

也可以添加Action標(biāo)簽 。只有action標(biāo)簽被點(diǎn)擊的時(shí)候程序才會(huì)運(yùn)行。
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"))
)
)
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)
}

仔細(xì)看一下,其實(shí)添加了按鈕只是多此一舉,只要改變了lambda或者n,都會(huì)自動(dòng)更新。因?yàn)閺某绦驁D里可以看出這是一個(gè)并聯(lián)的關(guān)系,并不是串聯(lián)。要把simulate串聯(lián)在里面才行。

所以需要進(jìn)行下面的修改。用eventReactive ,稍微有點(diǎn)難懂。有點(diǎn)接近If/else的邏輯關(guān)系。
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)
}

文章還提到了觀測函數(shù)observer,用來提示命令是否被執(zhí)行??梢杂脕矸答伌a執(zhí)行情況。這個(gè)函數(shù)出的結(jié)果不會(huì)被保存在任何變量里。但是可以用來Debugg。
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")
})
}
