import matplotlib.pyplot as pltR 动态交互可视化进阶与 shiny 入门
绘图工具对比
- Matlab,Origin,Excel…
- Python:常用
matplotlib库
语法习惯:对象.方法(其他参数)
- R:大多依赖于
ggplot2包
library(ggplot2)语法习惯:方法(对象,其他参数)
Python 绘图常用包较多:matplotlib.pyplot, pylab, seaborn,R 绘图语法更成体系,基于 ggplot2 的语法不改大的框架。以下只讨论 R 中基于 ggplot2 的可视化框架及相关的拓展包,以呈现更美观,更富信息量的动态交互式图表。
R 动态/交互式图表
ggplot23(Hadley Wickham, 2019 COPSS 奖) 入门快,语法简单,满足大多数情况的简单需求,基础语法如下:
library(ggplot2)
ggplot(data = <DATA>,
mapping = aes(<MAPPINGS>)) +
<GEOM_FUNCTION>(
mapping = aes(<MAPPINGS>),
stat = <STAT>,
position = <POSITION>) +
<SCALE_FUNCTION> +
<COORDINATE_FUNCTION> +
<FACET_FUNCTION> +
<THEME_FUNCTION>一般情况下,设定数据,坐标与绘图类型已经可以满足需求。
ggplot2 包除了提供了一系列常用图的绘制函数,更重要的是构建起了一种绘图框架,即使用 + 进行图层的叠加,可以叠加多个 <GEOM_FUNCTION>,也可以在原本的图像上叠加新的功能,如动画与交互。
gganimate
gganimate extends the grammar of graphics as implemented by ggplot2 to include the description of animation. It does this by providing a range of new grammar classes that can be added to the plot object in order to customise how it should change with time.
transition_*()defines how the data should be spread out and how it relates to itself across time.view_*()defines how the positional scales should change along the animation.shadow_*()defines how data from other points in time should be presented in the given point in time.enter_*()/exit_*()defines how new data should appear and how old data should disappear during the course of the animation. ease_aes() defines how different aesthetics should be eased during transitions.
示例1-mtcars 汽车数据箱线图
mtcars 为 R 内置数据集,包含 32 量汽车相关信息,详细字段解释见文档。
library(ggplot2)
ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
theme_minimal() +
labs(x = "Number of cylinders", y = "Miles/(US) gallon")library(gganimate)
p <- ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
theme_minimal() +
labs(x = "Number of cylinders", y = "Miles/(US) gallon") +
transition_states(
gear,
transition_length = 1,
state_length = 3
) +
enter_fade() +
exit_shrink() +
labs(title = 'Now showing the boxplot of gear state: {closest_state}',
subtitle = 'Frame {frame} of {nframes}')
p <- animate(p, nframes = 50, rewind = FALSE, width = 600, height = 400)transition_states关键的动画函数,指定用于切分数据框的分类变量,不求细节的动画只设置这一个函数就可以。transition_length不同状态之间变化的时间(相对),值越大变化的越慢;state_length在一个状态上停留的时间(相对),值越大停留的越久;
ease_aes可以设置渐变的模式(图像转换的速度),默认为线性,简单观察变化时候没有单独设置的必要。enter_fade和exit_shrink控制数据的渐入和渐出,由transition_states中指定的分类变量切分数据框后某个类别没有数据时,该设置效果才会显现。animate设置其他相关参数:nframes确定动画有多少个 frame(frame \(\neq\) state),值越大动画生成的速度越慢;rewind是否需要收尾相连,默认为 FALSE;fps一秒钟显示几个 frame;duration整个动画几秒钟,duration + fps 的设置可以覆盖掉 nframe。
可以输出的参数
frame当前 frame 的索引nframesframe 总量progress当前 frame 播放进程 = frame/nframesdata当前 frame 使用的数据- 自行设置
示例2-gapminder 散点图
回顾经典的可视化案例 Hans Rosling’s 200 Countries, 200 Years, 4 Minutes - The Joy of Stats - BBC Four
现在用 gapminder 包中的 gapminder 数据集5模仿这个经典案例。gapminder 数据集包含五个洲 142 个国家 12年(1952-2007 每隔五年记录一次)的 GDP,人均寿命,人口总数的信息。
library(gapminder)
p <- ggplot(gapminder, aes(x = gdpPercap, y = lifeExp, color = continent, size = pop)) +
geom_point(alpha = 0.5) +
scale_x_log10() +
ylim(30, 85) +
theme_minimal() +
theme(plot.title = element_text(size = 16, hjust = 0.5)) +
transition_states(year, 2, 1) +
scale_size(range = c(.1, 24), guide="none") +
labs(title = "Year: {frame_time}", x = 'GDP per capita', y = 'life expectancy') +
transition_time(year) +
enter_fade() +
exit_shrink()
p <- animate(p, nframe = 100, duration = 15, width = 600, height = 400)题外话:介绍一个网站 gapminder.org 下载数据,直接可视化。“该网站致力于用数据事实来消除常见的全球误解,他们从世界各地的官方机构发布的信息中,收集了上百种关于世界经济、人口、环境、健康等公共指标的历史数据,用非常简单而生动的方式展示给大家。”
gganimate 的重心在于“动态”而非交互。当数据为时序数据,或者想观察其他变量随其中一个有序变量的变化情况时方便使用。如果原本静态图已经有足够的展示数据的能力,我们需要的只是按照自己的需要查看不同数据点的详细信息,即交互性。plotly 包和 ggigraph 都可以实现交互性,下面逐一介绍。
plotly
plotly 应该是将简单的 ggplot 对象转化为交互图表最简单的方法了!导入 plotly 包后只要在对象外面嵌套一层 ggplotly 函数让静态图表拥有了“回应问题”的能力,依旧以上面 mtcars 箱线图为例
library(plotly)
p <- ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
theme_minimal() +
labs(x = "Number of cylinders", y = "Miles/(US) gallon")
ggplotly(p)同理,我们截取 gapminder 中 1997 年的数据绘制交互性散点图。
p <- ggplot(gapminder %>% filter(year == 1997),
aes(x = gdpPercap, y = lifeExp, color = continent, size = pop)) +
geom_point(alpha = 0.5) +
scale_x_log10() +
theme_minimal()
ggplotly(p)用 plotly 包可以快速赋予已经绘制好的图交互性,鼠标悬浮即可显示详细信息,而不需要修改原本图表生成的代码,非常简单实用。plotly 也可以基于自己的一套绘图函数绘制交互性图表,依旧以 1997 年 gapminder 数据为例来观察区别。
plot_ly(gapminder %>% filter(year == 1997),
x = ~log(gdpPercap), y = ~lifeExp,
size = ~pop, color = ~continent,
marker = list(opacity = 0.4, sizemode = 'diameter')) %>%
add_trace(type = "scatter", mode = "markers")可以发现,对比直接将 ggplot 对象转化为交互性图表,用 plotly 的函数(1)更复杂;(2)数据悬浮需要指定并单独设置(3)但功能更完善,具体请查阅 plotly 官方网站6,如果对交互性要求更高,需要将交互性图表嵌入网页中,可以详细学习,对于一般的需求直接嵌套就足够了。
是否可以将
gganimate和plotly嵌套使用?不可以,但这样的效果plotly可以实现。
ggigraph
ggigraph 和 plotly 实现的功能类似,都将图转化为 html widget 实现交互性。ggigraph 无法像前文提到那样直接转化 ggplot 对象,而是需要更换绘图函数,但优势在于其语法与 ggplot2 非常类似(从命名上就就能知道),对于大多数函数,只需在函数名上加入后缀 _interactive,最后嵌套 girafe 即可。
ggplot2 绘图中常使用的函数加上 _interactive 后,可以直接获取以下三个变量以实现交互性:
tooltip: 鼠标悬浮停留时显示的数据;data_id: 选择一个分类变量,会高亮与鼠标当前悬浮停留的数据点相同类别的数据;onclick: 点击鼠标时要执行的 javescript 命令。
依然以上述 gapminder 1997 年的数据为例
library(ggiraph)
p <- ggplot(gapminder %>% filter(year == 1997),
aes(x = gdpPercap, y = lifeExp, color = log(pop))) +
geom_point_interactive(aes(tooltip = country,
data_id = continent), size = 2) +
scale_x_log10() +
theme_minimal()
girafe(code = print(p), width_svg = 5, height_svg = 3,
options = list(
opts_hover_inv(css = "opacity:0.3;"),
opts_hover(css = "fill:red;")
))对比 ggigraph 和 plotly 的交互性,有如下结论:
ggigraph的优点:- 语法更贴近
ggplot2更容易学习; onclick可以执行自定义的动作,也方便应用在 shiny app 中;- 交互功能更全面。
- 语法更贴近
ggigraph的缺点:- 不能直接转换
ggplot对象; - 需要单独设置 Zoom。
- 不能直接转换
ggridges
ggridges 用于生成山峦图,并不属于动态或交互式图表,但对于观察时间序列中某个变量分布随时间的变化,或在迭代算法中观察参数分布中应用广泛,一张图包含大量感兴趣的信息。深度学习中常用的可视化工具 tensorboard 就是用山峦图来描述迭代过程中 tensor 的分布变化情况。
绘制 gapminder 数据中国家人均寿命的分布随年份的变化情况:
library(ggridges)
ggplot(gapminder, aes(x = lifeExp, y = factor(year), fill = year)) +
geom_density_ridges(alpha = 0.5) +
theme_ridges() +
theme(legend.position = "none") +
labs(title = 'Life Expactancy Change', subtitle = "Among 142 Countries", y = "year")Picking joint bandwidth of 3.88
绘制 gapminder 数据中 GDP(log 变换后)的分布随年份的变化情况:
ggplot(gapminder, aes(x = log(gdpPercap), y = factor(year), fill = year)) +
geom_density_ridges(alpha = 0.5) +
theme_ridges() +
theme(legend.position = "none") +
labs(title = 'Life Expactancy Change', subtitle = "Among 142 Countries", y = "year")Picking joint bandwidth of 0.402
ggridges 为完全遵循 ggplot2 语法的拓展包,故语法不需要进一步介绍。
More Info
更多基于 ggplot2 语法的拓展包可参见 https://exts.ggplot2.tidyverse.org/gallery/.
Shiny 入门介绍
shiny 是用于简单方便构建网页应用程序的 R 包,install.packages("shiny") 后就可以开始构建网页应用的奇妙之旅了。
假设大家对 RStudio 界面有基本了解,知道如何新建 shiny project。下面的介绍主要基于 shiny 官方提供的入门教程。
初始 Shiny
shiny 包中提供多个简单的样例 App 方便快速了解 shiny 的应用与功能
library(shiny)
runExample("01_hello") # a histogram
runExample("02_text") # tables and data frames
runExample("03_reactivity") # a reactive expression
runExample("04_mpg") # global variables
runExample("05_sliders") # slider bars
runExample("06_tabsets") # tabbed panels
runExample("07_widgets") # help text and submit buttons
runExample("08_html") # Shiny app built from HTML
runExample("09_upload") # file upload wizard
runExample("10_download") # file download wizard
runExample("11_timer") # an automated timer复杂一点的例子可以查阅 shiny 官方展示的样例 https://shiny.rstudio.com/gallery/.
Shiny App 基本构成
新建一个 shiny project 后,目录下会生成文件 app.R,默认初始化内容如下:
#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
# http://shiny.rstudio.com/
#
library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("Old Faithful Geyser Data"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30)
),
# Show a plot of the generated distribution
mainPanel(
plotOutput("distPlot")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
output$distPlot <- renderPlot({
# generate bins based on input$bins from ui.R
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
# draw the histogram with the specified number of bins
hist(x, breaks = bins, col = 'darkgray', border = 'white',
xlab = 'Waiting time to next eruption (in mins)',
main = 'Histogram of waiting times')
})
}
# Run the application
shinyApp(ui = ui, server = server)基本组件可以总结为三部分:
library(shiny)
# Define UI ----
ui <- fluidPage()
# Define server logic ----
server <- function(input, output) {}
# Run the app ----
shinyApp(ui = ui, server = server)官方文档:The user interface (ui) object controls the layout and appearance of your app. The server function contains the instructions that your computer needs to build your app. Finally the shinyApp function creates Shiny app objects from an explicit UI/server pair.
用户界面(UI)
shiny app 在这部分确定网页布局,可以在 fluidPage 添加网页构成元素,常见的构成组件有
titlePanel:App 的标题sidebarLayout:sidebarPanel侧边栏的内容mainPanel主栏目的内容
确定好基本框架以后,就可以将 html tag 嵌入到各个 Panel 中。html 的 tag 名称与 shiny 的调用函数有一一对应关系,如:
library(shiny)
as.character(h1("My title"))[1] "<h1>My title</h1>"
as.character(span("the text within the span"))[1] "<span>the text within the span</span>"
span("the text within the span")详细对应关系如下表所示:
Panel 中的 html tag 堆叠样例
sidebarPanel(
h1("Installation"),
p("Shiny is available on CRAN, so you can install it in the usual way from you R console:"),
code("install.packages(\"shiny\")"),
br(),
br(),
img(src="rstudio.png", height = 100),
p("Shiny is a product of", span("RStudio", style = "color:blue")),
)至此就可以在 Panel 中像搭积木一样确定网页的大体样式了,网页非交互性的部分基本已确认,在 server 内容为空的情况下已经可以生成一个网页试一试了!(见 app-simplest)
特殊文件夹:www,存放共享文件。
除了基本 html 命令,还可以直接嵌入组件,添加组件还可以方便得接受输入数据,以实现图表或统计数据的交互性。和 html 的 tag 添加方式类似,可以轻松得在 UI 里添加单选框、多选框、日期选择、文件上传入口等功能(见 app-widget)。获取的输入信息进行一定的转换后可以在 server 的函数中调用。
不同 widget 需要获取不同参数,但所有 widget 都需要(1)name;(2)label. 名称(name)对用户来说是不可见的,但在获取 widget 相关输入信息是需要根据名称来获取;标签(label)会显示在 widget 上,设置为空字符串 "" 时也可以取消 widget 上的文字。
常用显示文字提示与获取输入 widget 如下:
常用显示输出的 widget 如下,这类 widget 可以从 server 中获取输出数据:
(widget 功能示例 app 见 app-widget)
服务器
有了网页的框架,现在需要在 server 中填充为了成功填装内容需要的命令,server 是实现交互性的关键函数。UI 负责通过 widget 获取用户的输入数据,server 接收数据,进行必要的计算或绘图操作后,再把结果返回到 UI 中,这样就实现了交互性功能。注意不同的输出类型需要需要与输出 UI 相对应,如生成的 ggplot 对象需要用 plotOutput 输出,一个简单的输出样例如下:
library(shiny)
ui <- fluidPage(
titlePanel("Simple Output"),
sidebarLayout(
sidebarPanel(),
mainPanel(
plotOutput("distPlot")
)
)
)
server <- function(input, output) {
output$distPlot <- renderPlot({
hist(rnorm(1000))
})
}
shinyApp(ui = ui, server = server)(显示效果见 app-simple-output)
注意到 server 中一个特殊的变量 output,output 是一个列表,其中储存待输出的量,UI 的输出 widget 可以直接识别列表 output 中显示的变量。output 中的变量需要用 render* 函数生成,server 中的 render* 与 UI 中的 *Output 有一一对应关系。注意 render* 中需要用一对花括号 {} 括起内部 R 语句。
与此类似,输入信息储存在名为 input 的列表中,input$<name in widget> 即可获取对应数据。
效率问题:初次生成网页时会运行整个
app.R脚本,每次有一个新用户访问时只会重新生成 server,而 server 中的render*函数在每次更改输入 widget 选项时都会重新运行。
构建应用
UI 和 server 都准备就位,只需要运行以下函数就可以看到自己生成的网站了!
shinyApp(ui = ui, server = server)简单交互实现
示例1-gapminder 散点图
了解了 shiny app 的基本构成及输入输出的流动形式,现在就可以构建一个简单交互式 app 尝试一下了。依旧以 gapminder 数据为例,尝试构建一个网页显示某年份各个国家的人均寿命和 GDP 的关系,将年份设置为 Input,对应散点图为 Output。
library(shiny)
library(ggplot2)
library(plotly)
library(gapminder)
library(dplyr)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("Gapminder Explorer"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
sliderInput("year",
"Input the Year:",
min = 1952,
max = 2007,
value = 1952,
step = 5)
),
# Show a plot of the generated distribution
mainPanel(
# plotOutput("plot"),
plotlyOutput("plotly")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
output$plotly <- renderPlotly({
p <- ggplot(gapminder %>% filter(year == input$year),
aes(x = gdpPercap, y = lifeExp, color = continent, size = pop)) +
geom_point(alpha = 0.5) +
scale_x_log10() +
theme_minimal()
ggplotly(p)
})
# output$plot <- renderPlot({
# p <- ggplot(gapminder %>% filter(year == input$year),
# aes(x = gdpPercap, y = lifeExp, color = continent, size = pop)) +
# geom_point(alpha = 0.5) +
# scale_x_log10() +
# theme_minimal()
# p
# })
}
# Run the application
shinyApp(ui = ui, server = server)(生成效果见 app-demo-scatter)
plotly 支持 shiny 输出,提供 renderPlotly 和 plotlyOutput,普通的 ggplot 对象直接使用 shiny 提供的 renderPlot 和 plotOutput 即可。
示例2-mtcars 箱线图
library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("mtcars Data Explorer"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
selectInput("x", "X Axis:", choices = c("Number of cylinders",
"Number of forward gears",
"Number of carburetors"),
selected = "Number of cylinders"),
selectInput("y", "Y Axis:", choice = c("Miles/(US) gallon",
"Displacement (cu.in.)",
"Gross horsepower",
"1/4 mile time"),
selected = "Miles/(US) gallon")
),
# Show a plot of the generated distribution
mainPanel(
plotlyOutput("boxplot")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
output$boxplot <- renderPlotly({
data_x <- switch(input$x,
"Number of cylinders" = mtcars$cyl,
"Number of forward gears" = mtcars$gear,
"Number of carburetors" = mtcars$carb)
data_y <- switch(input$y,
"Miles/(US) gallon" = mtcars$mpg,
"Displacement (cu.in.)" = mtcars$disp,
"Gross horsepower" = mtcars$hp,
"1/4 mile time" = mtcars$qsec)
color = switch(input$x,
"Number of cylinders" = "steelblue",
"Number of forward gears" = "red",
"Number of carburetors" = "purple")
data = data.frame(x = data_x, y = data_y)
p <- ggplot(data, aes(x = factor(x), y = y, alpha = 0.7)) +
geom_boxplot(fill = color) +
theme_minimal()
ggplotly(p)
})
}
# Run the application
shinyApp(ui = ui, server = server)(生成效果见 app-demo-switch)
switch函数将 widget 数据转换为 R 变量方便计算操作
继续美化
bslib 包提供多个 shiny 主题,只需要在 UI 中用一行的空间设定 theme 即可,如
ui <- fluidPage(
theme = bs_theme(version = 4, bootswatch = "minty")
...
)不确定想用什么主题,也可以在网页上交互式选择。
library(shiny)
library(bslib)
ui <- fluidPage(
theme = bs_theme(),
...
)
server <- function(input, output) {
bs_themer()
...
}
shinyApp(ui, server)依旧使用 mtcars 的示例 app,增加主题选择后效果如 app-theme。
bslib 包还支持自定义主题,但现有 theme 已经足够满足大多数情况的需求。
More Info
- 更多 shiny demo 与进阶使用方法请查阅 shiny 官网示例 https://shiny.rstudio.com;
- shiny 参考书:Mastering Shiny (Hadley Wickham).
- Shiny for Python12 is currently in Alpha!
Footnotes
https://www.zhihu.com/question/28707877↩︎
https://www.zhihu.com/question/28707877↩︎
https://ggplot2.tidyverse.org/↩︎
https://github.com/rstudio/cheatsheets/blob/main/data-visualization-2.1.pdf↩︎
https://www.rdocumentation.org/packages/gapminder/versions/0.3.0↩︎
https://plotly.com/↩︎
https://zhuanlan.zhihu.com/p/39720917↩︎
https://shiny.rstudio.com/tutorial/written-tutorial/lesson2/↩︎
https://shiny.rstudio.com/tutorial/written-tutorial/lesson3/↩︎
https://shiny.rstudio.com/tutorial/written-tutorial/lesson4/↩︎
https://shiny.rstudio.com/tutorial/written-tutorial/lesson4/↩︎
https://shiny.rstudio.com/py/↩︎