如何用 R 绘制动态统计图?

如果一幅图胜过千言万语,那么一幅会动的图呢?

需求

绘制统计图形,是为了给谁看?

显然不是给电脑看。

因为它看不懂,也没必要看。给它数据就好了。它理解起来,更准确。

绘制统计图形,是给人看的。

可以给别人看。例如合作者、读者、审稿人,或者演讲时的观众。

但更多的情况,图也是给自己看的。

为什么要画图?

因为密密麻麻的数字或符号,远不如一幅图像,看得清楚和舒服。

人类中的大多数,目前还没有进化出对海量原始数据,条件反射一般的理解能力。

漫长的演化史上,人类的感官只要能有效发现食物(包含猎物),快速捕获危险信号(例如捕食者逼近),和同类高效交流(使用声音、表情或肢体语言)就大概率可以在残酷的自然淘汰赛里幸存下来。

不得不从财务报表这样的密集数据里,发现机会和风险,是最近几百年才有的事儿。

巴菲特和芒格这样的投资大家,也许有这种超能力。

但这种能力,显然不是所有人的标配。

对普通人来说,理解大量的数据,统计图形很必要。因此人们常说,“一幅图胜过千言万语”。

在《如何用Python从海量文本抽取主题?》一文里,我给你展示过如何绘制主题挖掘图形。

而《如何用Python和R对故事情节做情绪分析?》一文中,我给你介绍了如何绘制故事情绪时间序列。

如你所见,这些图很有用。

但是它们只是静态的。

那么,如果图是动态的呢?

那至少,它能够给我们提供更多一个维度的信息。

这种功能,真的有用吗?

我这里给你看一个例子。

这幅动态统计图,描绘了世界不同区域,人均 GDP 和预期寿命之间的关联。随着左上角年份的不断变化,你会看到几十年来,这个世界的发展变化。

Hans Rosling 曾经用类似的数据和动画效果,做了非常精彩的 TED 演讲。我上课的时候,不止一次拿来作为演示样例,让学生揣摩学习。

如果你感兴趣的话,可以点击这个链接查看视频。

你知道吗?只需要短短10行语句,你也能自己绘制出这个图形。

不过我们学东西,不宜贪多求快。

要绘制上图,你需要了解相关的基础知识。一下子摄入很多新知,可能造成认知负荷,对你的学习兴趣没有益处。

本文中,我用一个更简单的例子,给你展现如何用 R 绘制动态统计图。

有了它作为基础,结合我给你推荐的相关学习资源,你也能很快做出更为实用,甚至是令人惊艳的动图。

环境

你不需要安装任何软件。只需要点击这个链接http://t.cn/ReaP9Mk),就可以使用 R 编程环境了。

等准备工作完毕,你会看到,浏览器里面开启了一个 RStudio 界面。

你如果时间紧迫,不想输入任何代码,却又想马上看到运行结果,可以点击左上角的 File -> Open File,并且从出现的文件列表中,选择 code.Rmd

你就能看见下图这样打开该文件后的结果。

Rmd 文件后缀,代表 R Markdown,是 RStudio 这个 IDE 上可以使用的一种特殊的 Markdown 文件。说它特殊,是因为其中的代码段落,可以直接运行出结果。

界面左上方这里,有一个毛线球形状的按钮,名称叫做 Knit ,点击一下,它会把这个 code.Rmd 文件,转换成 HTML ,并且其中全部的代码,都显示出运行结果来。

如果你没有那么急,就请按照我下面的说明来操作。根据教程,一步步手动输入语句。这样更有助于你的理解,收获会更大。

点击左上角的 File -> New File ,选择菜单里面的第一项 R Script

此时,你会看到左侧分栏一个空白编辑区域开启,可以输入语句了。

输入之前,我们先给文件起个名字。点击 File -> Save 按钮。

在新出现的对话框里面,输入 demo ,回车。

好了,下面就可以输入并运行代码了。

代码

首先,我们需要读入几个必要的软件包:

library("tidyverse")
library("lubridate")
library("gganimate")

如果你看过我的《如何用R和API免费获取Web数据?》一文,对于 tidyverse 应该并不陌生。它是大神 Hadley 等人共同开发的一系列 R 工具包合集。对我来说,它改变了之前 R 语言"难以学习"、"语法古怪"、"不好使用"等刻板印象。

lubridate 是用来处理时间数据的 R 软件包。如果没有这东西,你每次操作时间数据,都会麻烦许多。

gganimate 顾名思义,后面我们绘制动态图形,需要用到。

下面看看我们这次使用的数据。

数据保存的格式是 .RData ,需要使用 load() 函数读入。

load('carriers_jan.RData')

读入以后,保存在其中的一个数据框变量 carriers_jan 就复活了。下面我们看看其内容:

carriers_jan
## # A tibble: 93 x 3

##    mydate     carrier     n

##    <date>     <chr>   <int>

##  1 2013-01-01 AA         94

##  2 2013-01-01 DL        112

##  3 2013-01-01 UA        165

##  4 2013-01-02 AA         94

##  5 2013-01-02 DL        152

##  6 2013-01-02 UA        170

##  7 2013-01-03 AA         95

##  8 2013-01-03 DL        128

##  9 2013-01-03 UA        159

## 10 2013-01-04 AA         95

## # ... with 83 more rows

我们解释一下该数据的内容。

这个数据实际上是从《如何用4行 R 语句,快速探索你的数据集?》一文中的 nycflights13 数据集,通过转换得来的。

转换后的数据,统计了不同航空公司在2013年1月,每一天从纽约三大机场起飞航班次数。

为了简便,我们在这个数据集里,只保留了3家航空公司,即:

  • 美国航空(American Airlines,AA)
  • 达美航空(Delta Air Lines, DL)
  • 美联航(United Airlines, UA)

下面我们挑出1月1日的数据看看:

carriers_jan %>%
  filter(mydate == ymd('20130101'))
## # A tibble: 3 x 3

##   mydate     carrier     n

##   <date>     <chr>   <int>

## 1 2013-01-01 AA         94

## 2 2013-01-01 DL        112

## 3 2013-01-01 UA        165

可见,这一天里,美国航空起飞航班 94 架次,达美 112 ,美联航为 165 。

根据上表,我们绘制一张柱状图(bar chart)。

横坐标是航空公司名称,是分类数据;纵坐标是航班次数,是量化数据。

carriers_jan %>%
  filter(mydate == ymd('20130101')) %>%
  ggplot(aes(x=carrier, y=n, fill=carrier)) +
  geom_bar(stat='identity', position='identity')

如上图所示,三家航空公司从纽约机场起飞次数,分别采用了不同颜色柱状图进行了可视化。

红色是美国航空,绿色是达美航空,蓝色是美联航。

简单解释一下其中的 ggplot 语句。

ggplot2 也是 Hadley Wickham 的作品,属于 tidyverse 软件包的一部分。

它将 Leland Wilkinson 提出的"绘图语法"(Grammar of Graphics)在 R 语言上实现。

在《如何用 Python 和 API 收集与分析网络数据?》一文中,我们已经介绍过 ggplot2 的 Python 克隆(plotnine),所以这里就不赘述背景了。

你只要记住,它绘制图形的时候,采用的是"分层"机制就好。

ggplot(aes(x=carrier, y=n, fill=carrier))
这一句讲述映射(mapping)关系,指定了把 carrier 信息投射到 x 轴, n(航班次数)投射到 y 轴,用不同 carrier 类别填充不同的色彩。

但是单单这一句,实际上是绘制不出东西来的,不信你可以尝试执行一下:

carriers_jan %>%
  filter(mydate == ymd('20130101')) %>%
  ggplot(aes(x=carrier, y=n, fill=carrier))

请注意这个图里, x 轴和 y 轴的设置,都与我们的预期一致。但是任何实质性内容,都没有绘制出来。因为咱们还没有告诉 ggplot ,打算画一个什么类别的统计图形。

这就是下一句 geom_bar(stat='identity', position='identity') 的用处。

这句话告诉 ggplot ,请绘制柱状图,柱的高度按照 y 值设置,对应 x 上每一个取值(航空公司名称),分别绘制一根柱。

这张静态图,只能告诉我们2013年1月1日这一天,纽约机场这3个航空公司起飞航班数量信息。

假如我们想多了解一个维度,也就是把时间加进去,怎么办?

这里办法并不唯一。

最简单的常规方法,是把三维信息压缩到二维平面里面去。

因为我们看二维图像,除了能观察到位置区别之外,还可以辨识色彩。

利用下列语句,你可以把这张图轻松做出来。

carriers_jan %>%
  ggplot(aes(x=mydate, y=n, color=carrier)) +
  geom_point() + geom_line()

注意,这里因为我们不再把时间限定在1月1日了,因此你得把 filter(mydate == ymd('20130101')) 这一句去掉,使用全部1个月的时间。否则使用时间轴就没有意义了。

这里的 ggplot(aes(x=mydate, y=n, color=carrier)) ,你应该能观察到跟之前的图形间,映射关系的差别。

不同于上一幅图,我们把 mydate ,而不是 carrier 映射到了 x 轴。 y 轴的映射关系没有变化。

我们此次不打算绘制柱状图了,而是描绘随时间变化趋势,所以选用的是散点图(geom_point())+折线图(geom_line())。

这就意味着,再考虑柱状图里面的填充,就不恰当了,所以我们把 carrier 的信息,映射到颜色上去(color=carrier)。

从这张图里,你可以发现非常显著的规律性。

假如你不想这样压缩信息,而希望用图形随时间的动态变化,来体现附加的时间维度,该怎么办?

这时,你就需要使用 gganimate 这个动画包的功能了。

gganimate 目前的开发维护者,是 Thomas Lin Pedersen 。这是他的 github 页面地址。

他把原先的 gganimate 包接管了过来,仿照 ggplot 的风格,对语法进行了修改和补充,使其能够无缝融入到 ggplot 语句里,很方便地调用。

因为可以用动态体现时间维度,所以我们这次依然绘制柱状图。语句如下:

carriers_jan %>%
  ggplot(aes(x=carrier, y=n, fill=carrier)) +
  geom_bar(stat='identity', position='identity') +
  transition_time(mydate)

图动起来了,是吧?

解释一下语句。

与之前静态柱状图的区别,也是去掉了时间的限定那一句 filter(mydate == ymd('20130101')) ,以便描绘整个儿一月份的情况。

另一个显著差别,是加入了最后一行语句, transition_time(mydate) ,这也是图像能够动起来的关键。

根据 gganimate 官方的说明,图形转换可以有多个不同类型语句来控制。因为我们恰好有 mydate 这个时间数据列,所以可以使用最自然而简单的 transition_time() 方法。

transition_time(mydate) 根据时间信息对数据框进行切片,然后分别加以展示。图像因而动了起来。

不过,这里有个很严重的问题------你根本就看不清,当前的动态结果对应哪个时间。对不对?

咱们需要改进一下。

改进的方法很简单:加入图片标题,显示时间,并且让标题对应着一起变化。

修改后的代码如下:

carriers_jan %>%
  ggplot(aes(x=carrier, y=n, fill=carrier)) +
  geom_bar(stat='identity', position='identity') +
  transition_time(mydate) +
  labs(title='{frame_time}')

这下,你一眼就可以从标题中,看到当前动图对应的时间了。

这里我们用到了 ggplotlabs() 函数,这个函数负责图片的标记设定,除了标题以外,你还可以设置横纵轴说明等内容。

我们用 title 参数设置标题内容。标题需要变化,所以我们得传入一个可以变化的量title 参数。

我们传入的是 {frame_time} ,这就是我们刚才提到的, gganimate 自动切片所用的时间数据。 传入参数时,不要忘了需要将其包裹在双引号里,作为字符串类型传入。

小结

本文给你展示了 R 环境绘制动态统计图的方法,具体包含以下知识点:

  • 如何读入 .RData 格式的数据文件;
  • 如何利用 ggplot 命令映射变量,选择统计图类型(包括柱状图、散点图和折线图等);
  • 如何使用 gganimatetransition_time() 方法绘制基于时间数据的动态图;
  • 如何通过 labs 设置,动态显示时间,以便于和图像的变化对应。

为了展示样例的最小化,本文的动态统计图非常简单,技术含量并不高。

抛砖引玉。希望你举一反三,绘制出更有价值、内容也更加丰富的动态统计图来。

如果你对 ggplot2 绘图包感兴趣,想详细了解其语法,可以读作者 Hadley Wickham 自己写的书《ggplot2:数据分析与图形艺术》。

如果你想了解 gganimate 包的更多用法,可以阅读官方文档,或者看这段作者的演讲视频

希望这些资源,能对你今后可视化沟通、展示自己的数据分析结果,有所帮助。

给你留个思考题:

本文中的数据,是从《如何用4行 R 语句,快速探索你的数据集?》一文中的 nycflights13 数据集,通过转换(data manipulation)得来的。

你能不能自己利用 R 或者 Python 语句,完成这一转化过程呢?

欢迎留言,把你的思考和解决过程分享给大家。

小提示:

喜欢请点赞。还可以微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)

如果你对数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,846评论 25 707
  • 工欲善其事,必先利其器。总结一下,方便多了。R语言还是很牛逼的,可以干很多事情。有一把顺手的刀还是很重要的。 0....
    Liam_ml阅读 4,612评论 1 60
  • 情人节这些表白文案你都知道吗? 如果不需要表白,说点小甜话也是极浪漫极好的。 总之,除了“我爱你”,“我想你”, ...
    文案与美术阅读 678评论 1 10
  • 四十不惑,这是论语中对孔子或者说君子不同年龄阶段应该达到境界的一种论断。而每个阶段都是下一个阶段的基础,十有五而志...
    279aa0fea404阅读 271评论 1 1
  • 午六、若行若住廣說乃至若解勞睡正知而住(分三科)未一、徵 【若行若住,廣說乃至若解勞睡正知住者:云何為行?云何為住...
    德虔阅读 382评论 0 1