2019-06-25如何使用 ggplot2 ?

作者:黃寶臣 數據科學/科學哲學碩士/本科生物狗
知乎原文:
https://www.zhihu.com/question/24779017/answer/38750383

ggplot2是最偉大的繪圖工具之一,復制代碼就可以畫出專業級的圖片,但新手經常會遇到這樣那樣的報錯。這一篇知乎文章清楚的解釋了困擾初學者的疑惑,如aes一會兒在ggplot里,一會兒又跑到geom里,加不加括號等等。

總結來說有以下幾點:

  • ggplot2的核心理念是將繪圖與數據分離,數據相關的繪圖與數據無關的繪圖分離
  • ggplot2是按圖層作圖
  • ggplot2保有命令式作圖的調整函數,使其更具靈活性
  • ggplot2將常見的統計變換融入到了繪圖中。

1、ggplot2的邏輯。

ggplot2的邏輯在我看來其實是真正實現了一個圖層疊加的概念:一句語句代表一張圖,然后再有最小的單元圖層。這個與其他命令式的繪圖完全不同,來做個比較:

#這是基于graphic包里例子
x <- rnorm(100,14,5)
y <- x + rnorm(100,0,1)
plot(x,y)
text(13,20, expression(x[1] == x[2]))

輸出的圖是這樣的:

image

我們可以看到這種繪圖方式實際上是按命令添加的,以plot開始,可以以任何方式結束,每加上一個元素,實際上都是以一句單獨的命令來實現的。這樣做的缺點就是,其實不符合人對于畫圖的一般認識。其次,就是,我們沒有一個停止繪圖的標志,這使得有時候再處理的時候就會產生一些困惑。優勢其實也有,在做參數修改的時候,我們往往可以很方便地直接用一句單獨的命令修改,譬如對于x軸的調整,覺得不滿意就可以寫命令直接調整。而ggplot2則意味著要重新作圖。再來看ggplot2的代碼:

x <- rnorm(100,14,5) 
y <- x + rnorm(100,0,1) 
ggplot(data= NULL, aes(x = x, y = y)) +  #開始繪圖
  geom_point(color = "darkred") +  #添加點
  annotate("text",x =13 , y = 20,parse = T,
           label = "x[1] == x[2]") #添加注釋

畫出的結果如下:

image

我們可以發現,ggplot的繪圖有以下幾個特點:第一,有明確的起始(以ggplot函數開始)與終止(一句語句一幅圖);其二,圖層之間的疊加是靠“+”號實現的,越后面其圖層越高。

其次就是對于分組數據的處理,其實這方面,lattice已經做得很好了,不過我會在后面更仔細地敘述ggplot2是怎么看分組數據的繪圖的。

2. ggplot2的要素

我們這里不談qplot(quickly plotting)方法,單純談ggplot方法。不談底層的實現思想,我們簡單地理解,ggplot圖的元素可以主要可以概括如下:最大的是plot(指整張圖,包括background和title),其次是axis(包括stick,text,title和stick)、legend(包括backgroud、text、title)、facet這是第二層次,其中facet可以分為外部strip部分(包括backgroud和text)和內部panel部分(包括backgroud、boder和網格線grid,其中粗的叫grid.major,細的叫grid.minor)。大致見下圖,這部分內容的熟悉程度直接影響到對于theme的掌握,因此希望大家留心。

image

3. ggplot2圖層以及其他函數的分類

好了,現在把這些理念的東西講完了之后,下面來理解ggplot2里的繪圖命令。

ggplot2里的所有函數可以分為以下幾類:

  • 用于運算(我們在此不講,如fortify_,mean_等)

  • 初始化、展示繪圖等命令(ggplot,plot,print等)

  • 按變量組圖(facet_等)

  • 真正的繪圖命令(stat_,geom_,annotate),這三類就是實現一個函數一個圖層的核心函數。

  • 微調圖型:嚴格意義上說,這一類函數不是再實現圖層,而是在做局部調整。

  • scale_:直譯為標尺,這就是與aes內的各種美學(shape、color、fill、alpha)調整有關的函數。

  • guides:調整所有的text。

  • coord_:調整坐標。

  • theme:調整不與數據有關的圖的元素的函數。

4. 繪圖
第一步:初始化。ggplot2風格的繪圖的第一步就是初始化,說白了就是載入數據空間、選擇數據以及選擇默認aes。

p <- ggplot(data = , aes(x = , y = ))

data就是載入你要畫的數據所在的數據框,指定為你的繪圖環境,載入之后,就可以免去寫大量的$來提取data.frame之中的向量。當然,如果你的數據都是向量,也可不指定,但是要在申明中標注data = NULL,不然就會得到不必要的報錯。
第二個是重頭戲,即aes,是美學(aesthetic)的縮寫。這是在ggplot2初學者眼里最不能理解的東西,甚至很多老手也會在猶豫,什么時候要把參數寫在aes里,什么時候要寫在aes外。我們做一個簡單的,不非常恰當的解釋:任何與數據向量順序相關,需要逐個指定的參數都必須寫在aes里。這之后我們會進一步解釋,現在我們初始化的時候,最好只是把關于位置的x和y指定一下就好。

第二部,繪制圖層。

很多人在解釋ggplot2的時候喜歡說,ggplot2繪圖有兩種函數,一類是geom_,繪圖用的;一類是stat_,統計變換用的。這樣說不是不對,只是很不恰當,很多人就會問出一些問題,比如,統計變換竟然是做運算用的,為什么可以用來畫圖?為什么stat_bin和geom_histgram畫出來的圖是一樣,竟然一樣,為什么要重復?
事實上,**任何一個ggplot2圖層都包括stat和geom倆部分,或者說兩個步驟(其實還包括position)。 **而stat_identity則表示不做任何的統計變換。

我們來舉個例子,還是上面的代碼,為了更直觀,我在此作了修改:

x <- c(rnorm(100,14,5),rep(20,20)) 
y <- c(rnorm(100,14,5) + rnorm(100,0,1),rep(20,20))
ggplot(data= NULL, aes(x = x, y = y)) +  #開始繪圖
  geom_point(color = "darkred")

做出的圖如下:

image

我們查看碼源,就知道geom_point的默認stat是identity,即不做任何統計變換:

> geom_point
function (mapping = NULL, data = NULL, stat = "identity", position = "identity", 
    na.rm = FALSE, ...) 
{
    GeomPoint$new(mapping = mapping, data = data, stat = stat, 
        position = position, na.rm = na.rm, ...)
}
<environment: namespace:ggplot2>

大家可以發現,我在(20,20)這個點的數據事實上是有20個的,但由于沒做統計轉換(20,20)這個點被畫了20次,因此我們理論上看到的點其實是最后一次畫的那個點。可能這不夠直觀,沒關系,我們調整一下透明度到10%:

ggplot(data= NULL, aes(x = x, y = y)) +  #開始繪圖
  geom_point(color = "darkred",alpha = 0.1)

得到如下圖:

image

這樣應該就很明顯了,由于(20,20)點被畫了20次,所以透明度會疊加為20*10% = 200%實際只展現100%。

我們現在就使用坐標轉換來重新畫這個圖:

ggplot(data= NULL, aes(x = x, y = y)) + #開始繪圖
geom_point(color = "darkred",stat = "sum")

好了,解釋一下,stat_sum實際的意思就是按照某一點占所有點出現頻率然后換算成大小來作圖,因此,以上代碼就可以得到下面這張圖,因為(20,20)這個點出現頻率為20/120=16.667%:

image

好了,我們可以發現了,一個單純的geom_point里面也是帶有stat_的,因此,其實geom_和stat_實際上是一回事。可能你會問了,那照我的說法,以上這幅圖用的是geom_point里的一個參數,而不是再用stat_sum,這是一回事嗎?bingo!這個問題相當好,的確,按照以上的推理,應該存在一種以stat_sum作為主函數的方法來繪制這幅圖,搞不好,里面還有個參數geom,要設置成“point”。我們來實踐一下吧:

ggplot(data= NULL, aes(x = x, y = y)) +  #開始繪圖
  stat_sum(color = "darkred",geom = "point")
image

尼瑪,還真可以,還長得一模一樣。

現在就講通了,對于有過經驗的同學現在應該重新修正這個觀點——stat_和geom_是兩種繪圖方法。這是錯的,其實它們是ggplot2每一個圖層繪制都必須有的,是一個圖層的一體兩面

在這一步之中,我們也要回到我們在第一步時出現的問題,aes到底是什么?為什么說任何與數據向量順序相關,需要逐個指定的參數都必須寫在aes里?什么時候color、shape、size、fill寫外面,什么時候寫里面?

aes實際上做的是將aes里的向量的順序逐個地繪制。譬如以下代碼(轉自geom_point幫助文檔中的實例):

p <- ggplot(mtcars, aes(wt, mpg)) #<---- code 1
p + geom_point(aes(colour = qsec)) #<---- code 2

結果是:

image

我們來分析一下ggplot2是怎么作圖的。首先,我們來看一下mtcars這個數據集長什么樣:

> head(mtcars)
                   mpg cyl disp  hp drat    wt  qsec vs am gear carb
Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1

code 1: ggplot首先載入了這個mtcars的集合,然后指定給了mpg作為其x坐標位置,wt為y坐標位置。
code 2: 指定了qsec作為其染色的標準(分組),qsec為numeric變量,因此,應該選擇連續型的標尺,而不是分組染色。然后開始繪制,讀取mtcarsmpg[1]、mtcarswt[1],確定位置,然后為其染成mtcars$qsec[1]顏色;再繪制第二點。。。

因此,aes里的美學特征其實就是按照向量順序指定每個位置的美學特征,大家可以比較tapply函數的寫法。

好了,現在問題就來了。我想為所有點的顏色都染成綠色,怎么辦?其實很簡單,如果不需要指定這么一個染色的順序,而選擇將整個圖層染成一種顏色,則只需要將color寫在aes外:

p + geom_point(color = "green") 

哦,怪不得寫在aes里染出來的顏色不是綠色,但為什么寫到里面就不可以了,為了寫到里面,然出來的是粉色?

image

好了,我們再來分析一下把color = "green"寫到了aes里,到底發生了什么。

p + geom_point(aes(colour = "green"))

首先,數據的初始化跟上面那個例子是相同的。然后,因為color放到了aes里,于是ggplot開始搜索mtcars里面的向量了,發現沒有叫"green"的,然后又找了global,也沒有。于是,ggplot就開始把它認作了一個新的向量。等等,有個問題,我要按照這個向量來分別染色,而事實上,這個向量長度為1,怎么辦?ggplot就先把他展開成了factor(rep("green",nrow(mtcars)),levels = unique("green")),bingo!現在開始染色了。啊第一個數據mtcarsmpg[1]、mtcarswt[1],其顏色變量是"green",因子水平是1,染成默認調色第一種,哦,就是這個蛋蛋的粉紅色;再染第二個,還是"green",因子水平也是1,染成蛋蛋的粉紅色;... 終于完成了,咦?怎么都是蛋蛋的粉紅色。

通過舉了這個染色的例子大家應該都弄懂了,aes到底在干什么了。其他的美學特征其實也是完全一致的。只是需要解釋group=1的意思就是說不做分組來進行繪圖。什么?還是搞不清該放aes里面還是外面?那就記著想統一整個圖層時就放到aes外,想分成不同組調整,并且已經有一個與x、y長度一致的分組變量了,那就放到aes里。

在這一步里,還要要說的就是我們要講的是ggplot2大致內置了哪些圖:

  • 點(point, text):往往只有x、y指定位置,有shape但沒有fill
  • 線(line,vline,abline,hline,stat_function等):一般是基于函數來處理位置
  • 射(segment):特征是指定位置有xend和yend,表示射線方向
  • 面(tile, rect):這類一般有xmax,xmin,ymax,ymin指定位置
  • 棒(boxplot,bin,bar,histogram):往往是二維或一維變量,具有width屬性
  • 帶(ribbon,smooth):透明是特征是透明的fill
  • 補:包括rug圖,誤差棒(errorbar,errorbarh)

然后,就是按照你的需要一步步加圖層了(使用“+”)。

第三部,加注釋。所有注釋的實現都是通過annotate函數實現的,其實annotate就是一個最簡單的geom_單元,它一次只添加一個位置上的圖形(可以通過設置向量來實現同時繪制多個圖形,但這個理念和注釋的理念有所偏差)。annotate的geom就是指定注釋的類型,其屬性按照geom的不同而發生變化。

第四步,調整。這里的調整主要是使用微調圖形這大類的函數做美學特征、坐標軸、標題、繪圖主題的調整。這部分也就是繼承了命令式作圖的思想,使ggplot2的靈活性增加。

如何搜索你要用什么美學特征調整函數,其實就是按照美學特征的名字來,例如,你要調整的是fill,就找scale_fill_之后就有一些不同的染色方法(關于色彩,如果有時間還會添加相關知識);調整的是橫坐標標尺,就找scale_x_然后后面跟上你的橫坐標類型;其他雷同。

在調整主題這方面,值得褒獎的是,theme函數其實最妙的地方是將對于數據相關的美學調整和與數據無關的美學調整分離了。譬如說,我們要改變x軸的顏色,或者panel的底色,這個其實與數據處理無關,這種分離就會使得我們可以如此流程化地操作作圖,而不需要在考慮數據的時候還要關注到與數據無關的美學參數。有人有時候會覺得ggplot2很奇怪的地方就是為什么調整legend的時候,有時要用scale_,有時又要用theme,其實這都是對于ggplot2這個設計理念的不理解,作者的設計思路是要將數據處理與數據美學分開,數據美學與數據無關的調整分開。
其次,theme函數采用了四個簡單地函數來調整所有的主題特征:element_text調整字體,element_line調整主題內的所有線,element_rect調整所有的塊,element_blank清空。這種設計相當地棒。

由此,一個極具誠意的作圖應該長成下面這個樣子:

ggplot(data = , aes(x = , y = )) + 
    geom_XXX(...) + ... + stat_XXX(...) + ... +
    annotate(...) + ... +
    scale_XXX(...) + coord_XXX(...) + guides(...) + theme(...)

5. ggplot2的一些缺點

  • 公式支持不好,自帶的plotmath公式無法滿足很多需求
  • 無法針對多個legends進行調整
  • 效率不高,繪圖速度較慢,這也表示二次開發的可能性不高
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容