
如果一幅圖勝過千言萬語,那么一幅會動的圖呢?
需求
繪制統(tǒng)計圖形,是為了給誰看?
顯然不是給電腦看。
因為它看不懂,也沒必要看。給它數(shù)據(jù)就好了。它理解起來,更準確。
繪制統(tǒng)計圖形,是給人看的。
可以給別人看。例如合作者、讀者、審稿人,或者演講時的觀眾。
但更多的情況,圖也是給自己看的。
為什么要畫圖?
因為密密麻麻的數(shù)字或符號,遠不如一幅圖像,看得清楚和舒服。
人類中的大多數(shù),目前還沒有進化出對海量原始數(shù)據(jù),條件反射一般的理解能力。
漫長的演化史上,人類的感官只要能有效發(fā)現(xiàn)食物(包含獵物),快速捕獲危險信號(例如捕食者逼近),和同類高效交流(使用聲音、表情或肢體語言)就大概率可以在殘酷的自然淘汰賽里幸存下來。

不得不從財務報表這樣的密集數(shù)據(jù)里,發(fā)現(xiàn)機會和風險,是最近幾百年才有的事兒。

巴菲特和芒格這樣的投資大家,也許有這種超能力。
但這種能力,顯然不是所有人的標配。
對普通人來說,理解大量的數(shù)據(jù),統(tǒng)計圖形很必要。因此人們常說,“一幅圖勝過千言萬語”。
在《如何用Python從海量文本抽取主題?》一文里,我給你展示過如何繪制主題挖掘圖形。

而《如何用Python和R對故事情節(jié)做情緒分析?》一文中,我給你介紹了如何繪制故事情緒時間序列。

如你所見,這些圖很有用。
但是它們只是靜態(tài)的。
那么,如果圖是動態(tài)的呢?
那至少,它能夠給我們提供更多一個維度的信息。
這種功能,真的有用嗎?
我這里給你看一個例子。

這幅動態(tài)統(tǒng)計圖,描繪了世界不同區(qū)域,人均 GDP 和預期壽命之間的關聯(lián)。隨著左上角年份的不斷變化,你會看到幾十年來,這個世界的發(fā)展變化。
Hans Rosling 曾經(jīng)用類似的數(shù)據(jù)和動畫效果,做了非常精彩的 TED 演講。我上課的時候,不止一次拿來作為演示樣例,讓學生揣摩學習。

如果你感興趣的話,可以點擊這個鏈接查看視頻。
你知道嗎?只需要短短10行語句,你也能自己繪制出這個圖形。
不過我們學東西,不宜貪多求快。
要繪制上圖,你需要了解相關的基礎知識。一下子攝入很多新知,可能造成認知負荷,對你的學習興趣沒有益處。
本文中,我用一個更簡單的例子,給你展現(xiàn)如何用 R 繪制動態(tài)統(tǒng)計圖。
有了它作為基礎,結(jié)合我給你推薦的相關學習資源,你也能很快做出更為實用,甚至是令人驚艷的動圖。
環(huán)境
你不需要安裝任何軟件。只需要點擊這個鏈接(http://t.cn/ReaP9Mk),就可以使用 R 編程環(huán)境了。

等準備工作完畢,你會看到,瀏覽器里面開啟了一個 RStudio 界面。

你如果時間緊迫,不想輸入任何代碼,卻又想馬上看到運行結(jié)果,可以點擊左上角的 File -> Open File,并且從出現(xiàn)的文件列表中,選擇 code.Rmd 。

你就能看見下圖這樣打開該文件后的結(jié)果。

Rmd 文件后綴,代表 R Markdown,是 RStudio 這個 IDE 上可以使用的一種特殊的 Markdown 文件。說它特殊,是因為其中的代碼段落,可以直接運行出結(jié)果。

界面左上方這里,有一個毛線球形狀的按鈕,名稱叫做 Knit ,點擊一下,它會把這個 code.Rmd 文件,轉(zhuǎn)換成 HTML ,并且其中全部的代碼,都顯示出運行結(jié)果來。

如果你沒有那么急,就請按照我下面的說明來操作。根據(jù)教程,一步步手動輸入語句。這樣更有助于你的理解,收獲會更大。
點擊左上角的 File -> New File ,選擇菜單里面的第一項 R Script 。

此時,你會看到左側(cè)分欄一個空白編輯區(qū)域開啟,可以輸入語句了。

輸入之前,我們先給文件起個名字。點擊 File -> Save 按鈕。

在新出現(xiàn)的對話框里面,輸入 demo ,回車。

好了,下面就可以輸入并運行代碼了。
代碼
首先,我們需要讀入幾個必要的軟件包:
library("tidyverse")
library("lubridate")
library("gganimate")
如果你看過我的《如何用R和API免費獲取Web數(shù)據(jù)?》一文,對于 tidyverse 應該并不陌生。它是大神 Hadley 等人共同開發(fā)的一系列 R 工具包合集。對我來說,它改變了之前 R 語言"難以學習"、"語法古怪"、"不好使用"等刻板印象。
lubridate 是用來處理時間數(shù)據(jù)的 R 軟件包。如果沒有這東西,你每次操作時間數(shù)據(jù),都會麻煩許多。
gganimate 顧名思義,后面我們繪制動態(tài)圖形,需要用到。
下面看看我們這次使用的數(shù)據(jù)。
數(shù)據(jù)保存的格式是 .RData ,需要使用 load() 函數(shù)讀入。
load('carriers_jan.RData')
讀入以后,保存在其中的一個數(shù)據(jù)框變量 carriers_jan 就復活了。下面我們看看其內(nèi)容:
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
我們解釋一下該數(shù)據(jù)的內(nèi)容。
這個數(shù)據(jù)實際上是從《如何用4行 R 語句,快速探索你的數(shù)據(jù)集?》一文中的 nycflights13 數(shù)據(jù)集,通過轉(zhuǎn)換得來的。
轉(zhuǎn)換后的數(shù)據(jù),統(tǒng)計了不同航空公司在2013年1月,每一天從紐約三大機場起飛航班次數(shù)。
為了簡便,我們在這個數(shù)據(jù)集里,只保留了3家航空公司,即:
- 美國航空(American Airlines,AA)
- 達美航空(Delta Air Lines, DL)
- 美聯(lián)航(United Airlines, UA)
下面我們挑出1月1日的數(shù)據(jù)看看:
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 ,美聯(lián)航為 165 。
根據(jù)上表,我們繪制一張柱狀圖(bar chart)。
橫坐標是航空公司名稱,是分類數(shù)據(jù);縱坐標是航班次數(shù),是量化數(shù)據(jù)。
carriers_jan %>%
filter(mydate == ymd('20130101')) %>%
ggplot(aes(x=carrier, y=n, fill=carrier)) +
geom_bar(stat='identity', position='identity')

如上圖所示,三家航空公司從紐約機場起飛次數(shù),分別采用了不同顏色柱狀圖進行了可視化。
紅色是美國航空,綠色是達美航空,藍色是美聯(lián)航。
簡單解釋一下其中的 ggplot 語句。
ggplot2 也是 Hadley Wickham 的作品,屬于 tidyverse 軟件包的一部分。
它將 Leland Wilkinson 提出的"繪圖語法"(Grammar of Graphics)在 R 語言上實現(xiàn)。
在《如何用 Python 和 API 收集與分析網(wǎng)絡數(shù)據(jù)?》一文中,我們已經(jīng)介紹過 ggplot2 的 Python 克?。╬lotnine),所以這里就不贅述背景了。
你只要記住,它繪制圖形的時候,采用的是"分層"機制就好。

ggplot(aes(x=carrier, y=n, fill=carrier))
這一句講述映射(mapping)關系,指定了把 carrier 信息投射到 x 軸, n(航班次數(shù))投射到 y 軸,用不同 carrier 類別填充不同的色彩。
但是單單這一句,實際上是繪制不出東西來的,不信你可以嘗試執(zhí)行一下:
carriers_jan %>%
filter(mydate == ymd('20130101')) %>%
ggplot(aes(x=carrier, y=n, fill=carrier))

請注意這個圖里, x 軸和 y 軸的設置,都與我們的預期一致。但是任何實質(zhì)性內(nèi)容,都沒有繪制出來。因為咱們還沒有告訴 ggplot ,打算畫一個什么類別的統(tǒng)計圖形。
這就是下一句 geom_bar(stat='identity', position='identity') 的用處。
這句話告訴 ggplot ,請繪制柱狀圖,柱的高度按照 y 值設置,對應 x 上每一個取值(航空公司名稱),分別繪制一根柱。
這張靜態(tài)圖,只能告訴我們2013年1月1日這一天,紐約機場這3個航空公司起飛航班數(shù)量信息。
假如我們想多了解一個維度,也就是把時間加進去,怎么辦?
這里辦法并不唯一。
最簡單的常規(guī)方法,是把三維信息壓縮到二維平面里面去。
因為我們看二維圖像,除了能觀察到位置區(qū)別之外,還可以辨識色彩。
利用下列語句,你可以把這張圖輕松做出來。
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)。
從這張圖里,你可以發(fā)現(xiàn)非常顯著的規(guī)律性。
假如你不想這樣壓縮信息,而希望用圖形隨時間的動態(tài)變化,來體現(xiàn)附加的時間維度,該怎么辦?
這時,你就需要使用 gganimate 這個動畫包的功能了。
gganimate 目前的開發(fā)維護者,是 Thomas Lin Pedersen 。這是他的 github 頁面地址。

他把原先的 gganimate 包接管了過來,仿照 ggplot 的風格,對語法進行了修改和補充,使其能夠無縫融入到 ggplot 語句里,很方便地調(diào)用。
因為可以用動態(tài)體現(xiàn)時間維度,所以我們這次依然繪制柱狀圖。語句如下:
carriers_jan %>%
ggplot(aes(x=carrier, y=n, fill=carrier)) +
geom_bar(stat='identity', position='identity') +
transition_time(mydate)

圖動起來了,是吧?
解釋一下語句。
與之前靜態(tài)柱狀圖的區(qū)別,也是去掉了時間的限定那一句 filter(mydate == ymd('20130101')) ,以便描繪整個兒一月份的情況。
另一個顯著差別,是加入了最后一行語句, transition_time(mydate) ,這也是圖像能夠動起來的關鍵。
根據(jù) gganimate 官方的說明,圖形轉(zhuǎn)換可以有多個不同類型語句來控制。因為我們恰好有 mydate 這個時間數(shù)據(jù)列,所以可以使用最自然而簡單的 transition_time() 方法。
transition_time(mydate) 根據(jù)時間信息對數(shù)據(jù)框進行切片,然后分別加以展示。圖像因而動了起來。
不過,這里有個很嚴重的問題------你根本就看不清,當前的動態(tài)結(jié)果對應哪個時間。對不對?
咱們需要改進一下。
改進的方法很簡單:加入圖片標題,顯示時間,并且讓標題對應著一起變化。
修改后的代碼如下:
carriers_jan %>%
ggplot(aes(x=carrier, y=n, fill=carrier)) +
geom_bar(stat='identity', position='identity') +
transition_time(mydate) +
labs(title='{frame_time}')

這下,你一眼就可以從標題中,看到當前動圖對應的時間了。
這里我們用到了 ggplot 的 labs() 函數(shù),這個函數(shù)負責圖片的標記設定,除了標題以外,你還可以設置橫縱軸說明等內(nèi)容。
我們用 title 參數(shù)設置標題內(nèi)容。標題需要變化,所以我們得傳入一個可以變化的量給 title 參數(shù)。
我們傳入的是 {frame_time} ,這就是我們剛才提到的, gganimate 自動切片所用的時間數(shù)據(jù)。 傳入?yún)?shù)時,不要忘了需要將其包裹在雙引號里,作為字符串類型傳入。
小結(jié)
本文給你展示了 R 環(huán)境繪制動態(tài)統(tǒng)計圖的方法,具體包含以下知識點:
- 如何讀入
.RData格式的數(shù)據(jù)文件; - 如何利用
ggplot命令映射變量,選擇統(tǒng)計圖類型(包括柱狀圖、散點圖和折線圖等); - 如何使用
gganimate的transition_time()方法繪制基于時間數(shù)據(jù)的動態(tài)圖; - 如何通過
labs設置,動態(tài)顯示時間,以便于和圖像的變化對應。
為了展示樣例的最小化,本文的動態(tài)統(tǒng)計圖非常簡單,技術含量并不高。
拋磚引玉。希望你舉一反三,繪制出更有價值、內(nèi)容也更加豐富的動態(tài)統(tǒng)計圖來。
如果你對 ggplot2 繪圖包感興趣,想詳細了解其語法,可以讀作者 Hadley Wickham 自己寫的書《ggplot2:數(shù)據(jù)分析與圖形藝術》。

如果你想了解 gganimate 包的更多用法,可以閱讀官方文檔,或者看這段作者的演講視頻。

希望這些資源,能對你今后可視化溝通、展示自己的數(shù)據(jù)分析結(jié)果,有所幫助。
給你留個思考題:
本文中的數(shù)據(jù),是從《如何用4行 R 語句,快速探索你的數(shù)據(jù)集?》一文中的 nycflights13 數(shù)據(jù)集,通過轉(zhuǎn)換(data manipulation)得來的。
你能不能自己利用 R 或者 Python 語句,完成這一轉(zhuǎn)化過程呢?
歡迎留言,把你的思考和解決過程分享給大家。
小提示:
- 如果你用 R ,可以參考 dplyr 包的文檔;
- 如果你用 Python ,可以參考《推薦Python數(shù)據(jù)框Pandas視頻教程》一文。
喜歡請點贊。還可以微信關注和置頂我的公眾號“玉樹芝蘭”(nkwangshuyi)。
如果你對數(shù)據(jù)科學感興趣,不妨閱讀我的系列教程索引貼《如何高效入門數(shù)據(jù)科學?》,里面還有更多的有趣問題及解法。