用R markdown 生成交互式深度分析报告

Jean

<h5>R markdown的交互式动态报告由Shiny支持,在Rstudio中通过菜单File->New File->R Markdown->Shiny即可创建,与其它R markdown文档的区别是它提供了反应式输入输出组件,根据输入实时更新文档中相应的输出。先看一个简单的例子。<br>1、内嵌反应式组件的Shiny文档<br><i>---<br>title: "Shiny Doc 测试"<br>output: html_document<br>runtime: shiny<br>---<br><br>## R markdown Shiny文档测试<br>### 运行一些R代码<br>```{r}<br>date<-Sys.Date()<br>```<br>日期,inline形式的代码只显示结果,不会显示代码:`r date`<br><br>### 定义反应式输入,相当于ui.R<br>```{r setup, include=FALSE}<br>knitr::opts_chunk$set(echo = TRUE)<br>```<br>```{r}<br>numeric("rows", "头部显示行数", 5)<br>```<br><br>### 定义反应式输出,相当于server.R<br>```{r}<br>rows<-reactive({input$rows})<br>renderTable({ head(cars, rows())})<br>```<br><br>### 在后面可以继续引用反应式输入,包括反应式变量,render()输出等反应式组件。<br>行数:`r renderText({rows()})`。<br><br>### 定义另一个反应式输入<br>```{r}<br> slider("tail","尾部显示行数:", min = 2, max = 6, value = 4)<br>```<br><br>### 在后面引用反应式输出<br>```{r}<br>renderTable({tail(cars, input$tail)})<br>```<br></i><br>在头部的YAML定义中,通过runtime: shiny来定义它是个Shiny文档,它的输出只能是html_document,在线交互式,不能是pdf_document等其它格式。<br>然后在文档中直接嵌入Shiny反应式组件。与Shiny App的显著区别是, ui与server函数中的反应式组件,以及普通R代码可以按书写文档的需要自由的交织在一起,Shiny Server上部署运行时会自动分开抽取出来。<br>渲染效果如下图,改变行数输入值,输出就会相应调整。<br></h5> 这时Rstudio文档编辑工具栏上的运行按钮会自动由Knit变为Run Document,Rstuido识别它是个Shiny文档。它适用于从头开始创建报告。 <h5>2、内嵌独立Shiny App的文档<br>有3种嵌入的方式,虽然与Shiny文档的其它部分显示在同一个网页中,但它们操作不了嵌入Shiny APP内部的数据,没有这样的API。<br>1) shinyApp()函数嵌入,嵌入在文档中定义的Shiny APP。<br><i>---</i><br><i>title: "Shiny Doc"</i><br><i>output: html_document</i><br><i>runtime: shiny</i><br><i>---</i><br><br><i>```{r, echo=FALSE}</i><br><i>shinyApp(</i><br> <br><i> ui = fluidPage(</i><br><i> select("region", "Region:", </i><br><i> choices = colnames(WorldPhones)),</i><br><i> plotOutput("phonePlot", height=270)</i><br><i> ),</i><br> <br><i> server = function(input, output) {</i><br><i> output$phonePlot <- renderPlot({</i><br><i> barplot(WorldPhones[,input$region]*1000, </i><br><i> ylab = "Number of Telephones", xlab = "Year")</i><br><i> })</i><br><i> },</i><br> <br><i> options = list(height = 345)</i><br><i>)</i><br><i>```</i><br><br>2) shinyAppDir()嵌入,嵌入部署运行在其它目录的Shiny APP。<br></h5><h5><i>---<br></i></h5><h5><i>title: "Shiny Doc"<br>output: html_document<br>runtime: shiny<br>---<br>```{r, echo=FALSE}<br># 如果有global.R,要在嵌入之前手工调用,它不会自动执行。<br># https://github.com/rstudio/rmarkdown/issues/211<br># source("../kmeansExample/global.R")<br>shinyAppDir(<br> "../kmeansExample",<br> options = list(<br> width = "100%"<br> )<br>)<br>```</i></h5><br>这两种方式嵌入独立的Shiny App,都不支持global.R全局设置脚本,需要在文档中嵌入Shiny App之前自己手工调用执行。上面的Rmd执行效果如下:<br> <h5>3) knitr::include_app()嵌入,网页IFRAME嵌入。<br><i>---<br>title: "墨尔本房价分析报告"<br>author: "Jean"<br>date: "`r Sys.Date()`"<br>output: html_document<br>runtime: shiny<br>---<br><br>```{r setup, include=FALSE}<br>knitr::opts_chunk$set(echo = TRUE)<br>```<br><br>## 运行一些R代码,以便在报告里引用它们的结果。<br><br>显示当前的工作目录,显示代码及其执行结果是为了说明这是一个R markdown报告。<br><br>```{r somecode, echo=TRUE}<br>getwd()<br>```<br><br>## 嵌入完整Shiny App<br><br>通过在R Markdown 报告中使用 `shinyAppDir` 函数嵌入其它目录下运行的完整Shiny App,本例嵌入了Shiny App “墨尔本房价回归分析”,它调用底层的Python脚本在预处理数据的基础上完成各个回归模型的计算(当时是用Python研究的GBDT回归模型,当然也可以完全用R来完成),Shiny App主要是管理各个回归模型数据的交互式可视化展示:<br><br>```{r shinyapp, =FALSE, echo=TRUE, out.width="100%"}<br># R markdown嵌入Shiny App时,目前不支持加载global.R, 具体见下面的issue<br># https://github.com/rstudio/rmarkdown/issues/211<br># 对于使用global.R的Shiny App,要自己手动加载global.R<br># 不过在Rstudio开发环境中虽然测试通过了,但在Shiny Server部署运行时有问题。<br># global.R应该是正常加载了,但UI中不能显示所有render()函数渲染的组件,目前还没有找到原因。<br>source("../melbourne/global.R")<br>shinyAppDir(<br> "../melbourne",<br> options = list(<br> width = "100%", height=1000<br> ))<br># <br># 这一行输出正常,证明在Shiny Server中手动加载global.R执行正常。<br># perf<br>```<br><br>改用include_app来嵌入App,因为它是通过IFRAME嵌入的,没有运行环境的影响。<br>```{r, =TRUE, echo=TRUE, out.width="100%"}<br># knitr::include_app()在Rstudio开发环境与Shiny Server部署环境运行都正常。<br>knitr::include_app("https://jeanye.cn:4443/shiny/users/jean/melbourne/", height="1200px")<br><br>```</i><br></h5> <h5>这是通过在当前的Shiny文档网页中插入一个IFRAME来嵌入,完全不受Shiny文档运行环境的影响。比如墨尔本房价分析App,如果用前一种方式嵌入,由于未知的原因,所有render()函数渲染输出的Widget都显示为空白,而用这种方式嵌入就可以正常显示。<br><br>3、通过shinytest包后台运行Shiny App嵌入<br>一般引用已经部署应用的Shiny App来撰写深度分析报告,往往需要比较不同输入参数下的分析结果,这就要求Shiny文档与引用的Shiny App之间有交互的能力,由Shiny文档中设定引用Shiny App的输入参数,引用它的输出结果,然后在文档中加以比较分析。幸运的是,可以通过shinytest包后台运行被引用的Shiny App来完成,虽然它没有可视的UI界面,但可以在Shiny文档中通过程序去模拟UI的操作,然后提取输出的结果展示或比较分析。下面以墨尔本房价回归分析APP为例具体看看。<br><br><i>---</i><br><i>title: "墨尔本房价分析报告"</i><br><i>author: "Jean"</i><br><i>date: "`r Sys.Date()`"</i><br><i>output: html_document</i><br><i>runtime: shiny</i><br><i>---</i><br><br><i>## 后台加载墨尔本房价回归分析Shiny APP</i><br><i>需要用shinytest包,以便设置反应式输入,以及截图和提取反应式输出。</i><br><i>```{r}</i><br><i>library(shiny) </i><br><i>library(shinytest)</i><br><i># Set loadTimeout to a long time</i><br><i># Can not use shinytest for larger application</i><br><i># Error in sd_startShiny(self, private, path, seed) : </i><br><i># Cannot find shiny port number. Error:</i><br><i># https://community.rstudio.com/t/can-not-use-shinytest-for-larger-application/6514</i><br><i>app <- shinytest::ShinyDriver$new("../melbourne", loadTimeout=1000000)</i><br><i># get background browser window size.</i><br><i>app$getWindowSize()</i><br><i>```</i><br><br><i>### 默认算法与异常值阀值:</i><br><br><i>算法:`r app$getValue("algo")` 异常值阀值:`r app$getValue("threshold")`% 屏幕截图。</i><br><br><i>```{r, out.width = "100%", fig.width=10, fig.height=10}</i><br><i>app$takeScreenshot()</i><br><i>```</i><br><br><br><i>### 设置新的算法与阀值参数</i><br><br><i>设置算法</i><br><i>```{r}</i><br><i>app$setInputs(algo="LigthGBM")</i><br><i>app$getValue("algo")</i><br><i>```</i><br><br><i>设置异常值阀值</i><br><i>```{r}</i><br><i>app$setInputs(threshold=35)</i><br><i>app$getValue("threshold")</i><br><i>```</i><br><br><br><i>算法:`r app$getValue("algo")` 异常值阀值:`r app$getValue("threshold")`% 屏幕截图。</i><br><i>```{r, out.width = "100%", fig.width=10, fig.height=10}</i><br><i>app$takeScreenshot()</i><br><i>```</i><br><br><br><br><i>## 直接显示其中一个Widget</i><br><i>### 提取Shiny APP中返回的数据</i><br><i>用shinytest包的findWidget()函数找到Widget,然后用Widget的getHtml()函数得到Widget的HTML源码(包括数据),</i><br><i>然后把HTML源码插入到当前页面中。因为shinytest后台运行的Shiny APP运行在另一个R进程中,Widget的源码和数据不在本进程中,所以要得到Widget的HTML源码,并插入到当前的页面。反应式输入的值可以通过getValue()函数直接得到,所以需要这样提取的一般是反应式输出的值,特别是render()函数渲染输出的HTML Widget。</i><br><br><i>参阅:</i><br><br><i>https://cran.r-project.org/web/packages/shinytest/shinytest.pdf</i><br><br><i>https://rstudio.github.io/shinytest/reference/Widget.html</i><br><br><i>显示所有Widget</i><br><br><i>```{r}</i><br><i>app$listWidgets()</i><br><i>```</i><br><br><br><i>### 性能数据</i><br><i>```{r}</i><br><i>perf<-app$findWidget("performance")</i><br><i>perfHtml<-perf$getHtml()</i><br><i>HTML(perfHtml)</i><br><i>```</i><br><br><i>### 训练集拟合效果 </i><br><i>```{r}</i><br><i>trainPlot<-app$findWidget("trainPlot")</i><br><i>trainHtml<-trainPlot$getHtml()</i><br><i>HTML(trainHtml)</i><br><i>```</i><br><br><i>### 验证集拟合效果 </i><br><i>```{r}</i><br><i>validPlot<-app$findWidget("validPlot")</i><br><i>validHtml<-validPlot$getHtml()</i><br><i>HTML(validHtml)</i><br><i>```</i><br><br><i>### 异常数据</i><br><i>DataTable Widget只返回第一页,因为getHtml()只返回了一页。</i><br><i>```{r}</i><br><i>outliers<-app$findWidget("outliers")</i><br><i>outliersHtml<-outliers$getHtml()</i><br><i>HTML(outliersHtml)</i><br><br><i>```</i><br><br><i>可以通过shinytest包的execute()函数执行JavaScript找到“下页”按钮,模拟点击翻页,用程序逐页把数据抓过来。</i><br><i>```{r}</i><br><i>app$execute("</i><br><i>var nextPage = document.getElementsByClassName('next');</i><br><i>if (nextPage !=null){nextPage[0].click();}"</i><br><i>)</i><br><i>```</i><br><br><i>```{r}</i><br><i>outliers<-app$findWidget("outliers")</i><br><i>outliersHtml<-outliers$getHtml()</i><br><i>HTML(outliersHtml)</i><br><i>```</i><br><br>执行结果如下,这样写深度分析报告就比较充分和自由了。<br></h5>