天天看點

第二章 使用tidyverse整理、操作和繪制資料

本章内容包括:

  • 什麼是tidyverse
  • 什麼是整潔資料
  • 如何安裝和加載tidyverse
  • 如何使用tidyverse的tibble、dplyr、ggplot2、tidyr和purrr包

我真的很興奮能開始教你們機器學習。但在我們深入讨論之前,我想教你們一些技巧,這些技巧會讓你們的學習體驗更簡單、更有效。這些技能也将提高你的一般資料科學和R程式設計技能。

想象一下,我請你給我造一輛車(朋友之間的典型請求)。你可以用老式的:你可以購買金屬,玻璃和所有的部件,手工切割所有的部件,錘打成型,然後鉚接在一起。這輛車可能看起來很漂亮,工作起來也很完美,但這需要很長時間,如果你不得不再做一輛,你很難準确地記住你做了什麼。

相反,你可以在工廠裡使用現代化的機械臂。你可以給他們程式設計,讓他們把這些碎片切割和彎曲成預定義的形狀,然後讓他們為你組裝這些碎片。在這種情況下,制造汽車對您來說會更快、更簡單,而且将來很容易重複相同的過程。

現在想象一下,我向你提出一個更合理的請求,要求你重新組織和繪制一個資料集,準備将其傳遞給機器學習管道。你可以用R基函數來做這個,它們會很好地工作。但是代碼會很長,不太容易被人讀懂(是以在一個月内你很難記住你做了什麼),而且情節會很麻煩。

相反,您可以使用更現代的方法,使用tidyverse包家族中的函數。這些函數将有助于使資料操作過程更簡單、可讀性更強,并允許您用最少的輸入生成非常吸引人的圖形。

2.1 什麼是tidyverse,什麼是整潔的資料?

本書的目的是讓你掌握将機器學習方法應用于你的資料的技能。雖然我不打算涵蓋資料科學的所有其他方面(我也不可能在一本書中),但我确實想向您介紹tidyverse。在您将資料輸入機器學習算法之前,資料需要采用算法樂于使用的格式。

tidyverse是一個“為資料科學設計的R包的集合”,建立的目的是使R中的資料科學任務更簡單,更可讀,更可複制。這些包是“固執己見”的,因為它們被設計成使包作者認為是好的實踐的任務變得容易,而使他們認為是壞的實踐的任務變得困難。這個名字來自于整潔資料的概念,一種資料結構,其中:

  • 每一行代表一個觀測,
  • 每一列代表一個變量。

請看表2 - 1中的資料。想象一下,我們讓四個跑步者接受一個新的訓練方案。我們想知道這個訓練方案是否改善了他們的跑步時間,是以我們記錄了他們在新訓練開始前(第0個月)和之後三個月的最佳時間。

表2 - 1 混亂資料的一個例子該表包含四名跑步者在開始新的訓練方案之前以及之後三個月的跑步時間。

第二章 使用tidyverse整理、操作和繪制資料

這是一個資料的例子。你知道為什麼嗎?好吧,讓我們回到我們的規則。每一行是否代表一個觀察結果?沒有。實際上,我們每行有四個觀測(每個月一個)。每列是否代表一個變量?沒有。此資料中隻有三個變量:運動員,月份,和最佳時間,但我們有5列!

同樣的資料以整齊的格式看起來會怎樣?表2 - 2展示了這些。

表2 - 2 本表包含與表2 - 1相同的資料,但格式簡潔

第二章 使用tidyverse整理、操作和繪制資料

這一次,我們将包含月份辨別符的列(以前是Month)用作單獨的列,将儲存每個運動員的最佳時間的列用作每個月的最佳時間。每行是否代表一個觀測?是的! 每列是否代表一個變量?是的! 是以這些資料是整齊的格式。

確定資料格式整齊是任何機器學習管道的重要早期步驟,是以tidyverse包含tidyr包,可以幫助您實作這一點。tidyverse中的其他軟體包與tidyr以及其他軟體包一起工作,以幫助您:

  • 以合理的方式組織和顯示資料(tibble)
  • 操作和子集化資料(dplyr)
  • 繪制資料(ggplot2)
  • 用函數式程式設計方法替換for循環(purrr)

tidyverse中所有可用的操作都可以使用base R代碼實作,但我強烈建議您将tidyverse合并到您的工作中,因為它可以幫助您保持代碼更簡單、更具可讀性和可複制性。

tidyverse的核心和可選包我将教你使用tidyverse的tibble、dplyr、ggplot2、tidyr和purrr包。這些構成了“核心”tidyverse軟體包,以及:

  • readr,用于将資料從外部檔案讀入R
  • forcats,用于使用因子
  • stringr,用于使用字元串

除了這些可以一起加載的核心包之外,tidyverse還包括許多需要單獨加載的可選包。

要了解更多關于tidyverse的其他工具,請參閱加勒特和威克姆(O'Reilly Media,Inc.,科學2016)。

2.2 加載tidyverse

tidyverse的軟體包可以一起安裝和加載(推薦):

install.packages(“tidyverse”)
library(tidyverse)           

或根據需要單獨安裝和加載:

install.packages(c(“tibble”, “dplyr”, “ggplot2”, “tidyr”, "purrr"))
library(tibble)
library(dplyr)
library(ggplot2)
library(tidyr)
library(purrr)           

2.3 什麼是tibble軟體包及其功能

如果您曾經在R中做過任何形式的資料科學或分析,您肯定會遇到資料幀(data frame)作為存儲矩形資料的結構。

資料框工作正常,并且在很長一段時間内,是存儲具有不同類型列的矩形資料的唯一方法(與隻能處理相同類型資料的矩陣相反),但是對于資料科學家不喜歡的資料框方面,幾乎沒有做什麼改進。

注:如果每一行的元素數等于列數,每一列的元素數等于行數,則資料為矩形。資料并不總是這樣的!

tibble軟體包引入了一個新的資料結構tibble,以“保留那些經受住時間考驗的特性,放棄那些曾經很友善但現在令人沮喪的特性”(cran.r-project.org/web/packages/tibble/vignettes/tibble.html)。讓我們看看這意味着什麼。

2.3.1 建立tibbles

使用tibble()函數建立tibble與建立資料框的工作原理相同:

清單2.1 使用tibble() 建立tibbles

myTib <- tibble(x = 1:4,
y = c("london", "beijing", "las vegas", "berlin"))
myTib
# A tibble: 4 × 2 
      x y        
  <int> <chr>    
1     1 london   
2     2 beijing  
3     3 las vegas
4     4 berlin           
  • line4: 告訴我們這是一個4行2列的tibble
  • line5: 變量名
  • line6: 變量類<int>=整數,<chr>=字元

如果您習慣于使用資料框,則會立即注意到資料框列印方式的兩個不同之處:

  1. Tibbles告訴你它們是一個Tibble以及它們的次元,
  2. 當你列印它們的時候,Tibbles告訴你每個變量的類型

第二個特性在避免由于不正确的變量類型而導緻的錯誤方面特别有用。

提示:列印tibble時,<int>表示整數變量,<chr>表示字元變量,<dbl>表示浮點數(十進制),<lgl>表示邏輯變量。

2.3.2 将現有資料幀轉換為tibble

正如可以使用as.data.frame()函數将對象強制轉換為資料框一樣,也可以使用as_tibble()函數将對象強制轉換為tibble:

清單2.2 将資料框轉換為tibble

myDf <- data.frame(x = 1:4,
                   y = c("london", "beijing", "las vegas", "berlin"))
dfToTib <- as_tibble(myDf)
dfToTib
# A tibble: 4 × 2
      x y        
  <int> <chr>    
1     1 london   
2     2 beijing  
3     3 las vegas
4     4 berlin           

注:在本書中,我們将使用已經内置到R中的資料。通常,我們需要将資料從.csv檔案讀入R會話。要将資料作為tibble加載,請使用函數read_csv()。read_csv()來自readr包,該包在調用時加載,是read.csv()的tidyverse版本。

2.3.3 資料幀和tibble之間的差異

如果您習慣于使用資料框,您會注意到tibble的一些不同之處。在本節中,我總結了資料幀和tibble之間最顯著的差別。

TIBLES不轉換您的資料類型

人們在建立資料框時遇到的一個常見問題是,預設情況下,他們會将字元串變量轉換為因子。這可能很煩人,因為這可能不是處理變量的最佳方式。要防止這種轉換,必須提供stringsAsFactors = FALSE參數。

相反,tibbles預設情況下不會将字元串變量轉換為因子。這種行為是可取的,因為将資料自動轉換為某些類型可能會導緻令人沮喪的錯誤:

清單2.3 Tibbles不能将字元串轉換為因子

myDf <- data.frame(x = 1:4,
                   y = c("london", "beijing", "las vegas", "berlin"))
myDfNotFactor <- data.frame(x = 1:4,
                            y = c("london", "beijing", "las vegas", "berlin"),
                            stringsAsFactors = FALSE)
myTib <- tibble(x = 1:4,
                y = c("london", "beijing", "las vegas", "berlin"))
class(myDf$y)
#[1] "factor"
class(myDfNotFactor$y)
#[1] "character"
class(myTib$y)
#[1] "character"           

如果你想讓一個變量成為tibble中的一個因子,你隻需要把這個函數包裝在factor()的c()函數中:

myTib <- tibble(x = 1:4,
y = factor(c("london", "beijing", "las vegas", "berlin")))
myTib           

輸出簡潔,與資料大小無關

列印資料框時,所有列都列印到控制台(預設情況下),是以很難檢視早期變量和案例。當你列印一個tibble時,它隻列印前10行和适合你螢幕的列數(預設情況下),這樣更容易快速了解資料。請注意,未列印的變量名列在輸出的底部。運作清單2.4中的代碼,并将starwars tibble(包含在dplyr中,在調用library(tidyverse)時可用)的輸出與轉換為資料幀時的輸出進行對比:

清單2.4 starwars資料作為一個tibble和資料幀

data(starwars)
starwars
as.data.frame(starwars)           

提示:data() 函數将包含在base R或R包中的資料集加載到全局環境中。使用不帶參數的data() 列出目前加載的包可用的所有資料集。

使用“[”進行子集化将始終傳回另一個tibble對象

當子集化資料框時,如果保留多個列,則操作符将傳回另一個資料框,如果隻保留一個列,則操作符将傳回矢量。當設定tibble的子集時,操作符[将傳回另一個tibble。如果您希望顯式地将tibble列作為向量傳回,請始終使用或操作符。這種行為是可取的,因為我們應該在[[ 或$中明确表示我們是否想要向量或矩形資料結構,以避免bug:

清單2.5 使用[、[[和$對tibbles進行子集化

myDf[, 1]
myTib[, 1]
myTib[[1]]
myTib$x           
第二章 使用tidyverse整理、操作和繪制資料

注: 一個例外是,如果你的資料幀子集使用沒有逗号的單一索引(如myDF[1])。在本例中,[操作符将傳回單個列資料幀,但該方法不允許我們合并行和列的子集。

變量按順序建立

在建構tibble時,變量是按順序建立的,以便後面的變量可以引用前面定義的變量。這意味着我們可以在同一個函數調用中動态地建立引用其他變量的變量:

清單2.6 按順序建立變量

sequentialTib <- tibble(nItems = c(12, 45, 107),
cost = c(0.5, 1.2, 1.8),
totalWorth = nItems * cost)
sequentialTib
# A tibble: 3 × 3
  nItems  cost totalWorth
   <dbl> <dbl>      <dbl>
1     12   0.5         6 
2     45   1.2        54 
3    107   1.8       193.           

2.4 dplyr包是什麼和它做什麼

在處理資料時,我們經常需要對資料執行一些操作,例如:

  • 隻選擇感興趣的行和/或列,
  • 建立新的變量,
  • 擷取彙總統計資訊
  • 按照某些變量的升序或降序排列資料。

在執行這些操作時,我們需要維護的資料中可能還有一個自然的分組結構。dplyr包允許我們以一種非常直覺的方式執行這些操作。讓我們來看一個例子。

2.4.1 使用dplyr操作CO2資料集

讓我們在r中加載内置的二氧化碳資料集。我們有84個案例和5個變量,記錄了不同植物在不同條件下對二氧化碳的吸收。我将用這個資料集來教你一些基本的dplyr技能。

清單2.7 探索CO2資料集

library(tibble)
data(CO2)
CO2tib <- as_tibble(CO2)
CO2tib

# A tibble: 84 × 5
   Plant Type   Treatment   conc uptake
   <ord> <fct>  <fct>      <dbl>  <dbl>
 1 Qn1   Quebec nonchilled    95   16  
 2 Qn1   Quebec nonchilled   175   30.4
 3 Qn1   Quebec nonchilled   250   34.8
 4 Qn1   Quebec nonchilled   350   37.2
 5 Qn1   Quebec nonchilled   500   35.3
 6 Qn1   Quebec nonchilled   675   39.2
 7 Qn1   Quebec nonchilled  1000   39.7
 8 Qn2   Quebec nonchilled    95   13.6
 9 Qn2   Quebec nonchilled   175   27.3
10 Qn2   Quebec nonchilled   250   37.1
# … with 74 more rows
# ℹ Use `print(n = ...)` to see more rows           

假設我們隻想要第1、2、3和5列,我們可以使用select()函數來做到這一點。在代碼清單2.8中的select()函數調用中,第一個參數是資料,然後提供希望選擇的列的編号或名稱,用逗号分隔。

清單2.8 使用select()函數選擇列

library(dplyr)
selectedData <- select(CO2tib, 1, 2, 3, 5)
selectedData

# A tibble: 84 × 4
   Plant Type   Treatment  uptake
   <ord> <fct>  <fct>       <dbl>
 1 Qn1   Quebec nonchilled   16  
 2 Qn1   Quebec nonchilled   30.4
 3 Qn1   Quebec nonchilled   34.8
 4 Qn1   Quebec nonchilled   37.2
 5 Qn1   Quebec nonchilled   35.3
 6 Qn1   Quebec nonchilled   39.2
 7 Qn1   Quebec nonchilled   39.7
 8 Qn2   Quebec nonchilled   13.6
 9 Qn2   Quebec nonchilled   27.3
10 Qn2   Quebec nonchilled   37.1
# … with 74 more rows
# ℹ Use `print(n = ...)` to see more rows           

現在讓我們假設我們希望過濾我們的資料,隻包括那些uptake大于16的病例。我們可以使用filter()函數來實作這一點。filter()的第一個參數同樣是資料,第二個參數是一個邏輯表達式,将對每一行進行計算。我們可以用逗号分隔多個條件。

清單2.9 使用filter()函數過濾行

filteredData <- filter(selectedData, uptake > 16)
filteredData

# A tibble: 66 × 4
   Plant Type   Treatment  uptake
   <ord> <fct>  <fct>       <dbl>
 1 Qn1   Quebec nonchilled   30.4
 2 Qn1   Quebec nonchilled   34.8
 3 Qn1   Quebec nonchilled   37.2
 4 Qn1   Quebec nonchilled   35.3
 5 Qn1   Quebec nonchilled   39.2
 6 Qn1   Quebec nonchilled   39.7
 7 Qn2   Quebec nonchilled   27.3
 8 Qn2   Quebec nonchilled   37.1
 9 Qn2   Quebec nonchilled   41.8
10 Qn2   Quebec nonchilled   40.6
# … with 56 more rows
# ℹ Use `print(n = ...)` to see more rows           

接下來,我們将按單個植物分組,并對資料進行彙總,以得到每組内uptake的平均值和标準差。我們可以分别使用group_by()和summarize()函數來實作這一點。

在group_by()函數中,第一個參數是資料,後面是分組變量。我們可以通過用逗号分隔多個變量來分組。當我們列印groupedData時,除了在資料上面得到一個訓示,即它們被分組、根據哪個變量以及有多少組外,沒有多大變化。這告訴我們,我們應用的任何進一步操作都将在分組的基礎上執行。

清單2.10 使用group_by()函數分組資料

groupedData <- group_by(filteredData, Plant)
groupedData

# A tibble: 66 × 4
# Groups:   Plant [11]
   Plant Type   Treatment  uptake
   <ord> <fct>  <fct>       <dbl>
 1 Qn1   Quebec nonchilled   30.4
 2 Qn1   Quebec nonchilled   34.8
 3 Qn1   Quebec nonchilled   37.2
 4 Qn1   Quebec nonchilled   35.3
 5 Qn1   Quebec nonchilled   39.2
 6 Qn1   Quebec nonchilled   39.7
 7 Qn2   Quebec nonchilled   27.3
 8 Qn2   Quebec nonchilled   37.1
 9 Qn2   Quebec nonchilled   41.8
10 Qn2   Quebec nonchilled   40.6
# … with 56 more rows
# ℹ Use `print(n = ...)` to see more rows           

提示: 你可以通過在ungroup()函數中包裝來移除tibble中的分組結構。

在summary() 函數中,第一個參數是資料,在第二個參數中,我們命名要建立的新變量,後跟一個=号,然後是該變量的定義。我們可以建立任意多的新變量,用逗号分隔,是以在清單2 - 11中,我建立了兩個彙總變量,每組的uptake均值(meanUp)和uptake的标準差(sdUp)。現在,當我們列印summarizedData時,我們可以看到,除了分組變量之外,原始變量已被我們剛剛建立的彙總變量所替換。

清單2.11 使用summary() 函數建立變量彙總

summarizedData <- summarize(groupedData, meanUp = mean(uptake),
sdUp = sd(uptake))
summarizedData

# A tibble: 11 × 3
   Plant meanUp   sdUp
   <ord>  <dbl>  <dbl>
 1 Qn1     36.1  3.42 
 2 Qn2     38.8  6.07 
 3 Qn3     37.6 10.3  
 4 Qc1     32.6  5.03 
 5 Qc3     35.5  7.52 
 6 Qc2     36.6  5.14 
 7 Mn3     26.2  3.49 
 8 Mn2     29.9  3.92 
 9 Mn1     29.0  5.70 
10 Mc3     18.4  0.826
11 Mc1     20.1  1.83            

最後,我們将從現有變量中變異出一個新變量,以計算每組的變異系數,然後按照我們剛剛建立的新變量的順序排列資料。我們可以用mutate() 和arrange() 函數來實作這一點。

對于mutate() 函數,第一個參數是資料,第二個參數是要建立的新變量的名稱,後面跟一個=号,最後是它的定義。我們可以建立任意多的新變量,用逗号分隔它們。

清單2.12 使用mutate() 函數建立新變量

mutatedData <- mutate(summarizedData, CV = (sdUp / meanUp) * 100)
mutatedData

# A tibble: 11 × 4
   Plant meanUp   sdUp    CV
   <ord>  <dbl>  <dbl> <dbl>
 1 Qn1     36.1  3.42   9.48
 2 Qn2     38.8  6.07  15.7 
 3 Qn3     37.6 10.3   27.5 
 4 Qc1     32.6  5.03  15.4 
 5 Qc3     35.5  7.52  21.2 
 6 Qc2     36.6  5.14  14.1 
 7 Mn3     26.2  3.49  13.3 
 8 Mn2     29.9  3.92  13.1 
 9 Mn1     29.0  5.70  19.6 
10 Mc3     18.4  0.826  4.48
11 Mc1     20.1  1.83   9.11           

TIP dplyr函數中的參數求值是連續的,這意味着我們可以通過引用meanUp和sdUp變量來定義summary()函數中的CV變量,即使它們還沒有建立!

arrange() 函數将資料作為第一個參數,後面跟着我們希望用來排列用例的變量。我們可以用逗号分隔多個列來進行排列,其中它将按照第一個的順序排列案例,并使用後續變量來打破束縛。

清單2.13 使用arrange() 函數按變量排列tibble

arrangedData <- arrange(mutatedData, CV)
arrangedData

# A tibble: 11 × 4
   Plant meanUp   sdUp    CV
   <ord>  <dbl>  <dbl> <dbl>
 1 Mc3     18.4  0.826  4.48
 2 Mc1     20.1  1.83   9.11
 3 Qn1     36.1  3.42   9.48
 4 Mn2     29.9  3.92  13.1 
 5 Mn3     26.2  3.49  13.3 
 6 Qc2     36.6  5.14  14.1 
 7 Qc1     32.6  5.03  15.4 
 8 Qn2     38.8  6.07  15.7 
 9 Mn1     29.0  5.70  19.6 
10 Qc3     35.5  7.52  21.2 
11 Qn3     37.6 10.3   27.5           

提示:如果你想按照變量值的順序排列tibble,降序隻需要将變量包裝在desc():arrange(mutatedData, desc(CV))。

2.4.2 将dplyr功能連結在一起

我們在2.4.1節中所做的一切都可以使用base R實作,但我希望你能看到dplyr函數,或者它們經常被稱為動詞(因為它們是人類可讀的,并且清楚地暗示了它們的作用),有助于使代碼更簡單,更容易被人類閱讀。但dplyr的強大之處真正來自于将這些功能連結在一起成為直覺的、順序的過程的能力。

在CO2資料操作的每個階段,我們都儲存中間資料并對其應用下一個函數。這非常繁瑣,在R環境中建立了許多不必要的資料對象,并且不便于人類閱讀。相反,我們可以使用管道操作符%>%,它在我們加載dplyr時可用。管道傳遞左側函數的輸出,作為右側函數的第一個參數。讓我們看一個基本的例子:

library(dplyr)
c(1, 4, 7, 3, 5) %>% mean()
#[1] 4           

%>%操作符擷取左側c() 函數的輸出(一個長度為5的向量),并且将其“輸送”到mean() 函數的第一個參數中。是以我們可以使用%>%操作符來連結多個函數結合在一起,使代碼更簡潔,更易于閱讀。

還記得我說過每個dplyr函數的第一個參數是資料嗎?這非常重要和有用的原因是,它允許我們将資料從上一個操作傳輸到下一個操作,是以,我們在2.4.1節中經曆的整個資料操作過程變為:

清單2.14 将dplyr操作與運算符連結在一起

arrangedData <- CO2tib %>%
select(c(1:3, 5)) %>%
filter(uptake > 16) %>%
group_by(Plant) %>%
summarize(meanUp = mean(uptake), sdUp = sd(uptake)) %>%
mutate(CV = (sdUp / meanUp) * 100) %>%
arrange(CV)
arrangedData

# A tibble: 11 × 4
   Plant meanUp   sdUp    CV
   <ord>  <dbl>  <dbl> <dbl>
 1 Mc3     18.4  0.826  4.48
 2 Mc1     20.1  1.83   9.11
 3 Qn1     36.1  3.42   9.48
 4 Mn2     29.9  3.92  13.1 
 5 Mn3     26.2  3.49  13.3 
 6 Qc2     36.6  5.14  14.1 
 7 Qc1     32.6  5.03  15.4 
 8 Qn2     38.8  6.07  15.7 
 9 Mn1     29.0  5.70  19.6 
10 Qc3     35.5  7.52  21.2 
11 Qn3     37.6 10.3   27.5           

從頭到尾通讀代碼,每次遇到%>%操作符時,說“然後”。您可以這樣了解:“擷取CO2資料,然後選擇這些列,然後過濾這些行,然後根據這個變量分組,然後用這些變量進行彙總,然後改變這個新變量,然後按照這個變量的順序排列,并将輸出儲存為arrangedData。” 您是否明白,這就是您可能用簡單的英語向同僚解釋資料操作過程的方式? 這就是dplyr的強大之處: 能夠以一種合乎邏輯的、人類可讀的方式執行複雜的資料操作。

提示: 通常在%>%操作符之後開始一個新行,以幫助使代碼更容易閱讀。

2.5 什麼是ggplot2包和它做什麼

在R中,有三種主要的繪圖系統:

  • Base graphics
  • lattice
  • ggplot2

可以說,ggplot2是資料科學家中最流行的系統,由于它是整潔世界的一部分,我們将在本書中使用這個系統來繪制我們的資料。ggplot2中的“gg”代表圖形文法,這是一種思想流派,它認為任何資料圖形都可以通過将資料與圖元件層(如軸、标記、網格線、點、條和線)組合而成。通過像這樣分層繪圖元件,您可以使用ggplot2以非常直覺的方式建立具有交流性和吸引力的繪圖。

讓我們加載R附帶的iris資料集,并建立它的兩個變量的散點圖。這些資料是由Edgar Anderson于1935年收集并發表的,包含了三種鸢尾植物花瓣和萼片的長度和寬度測量。

圖2.1用ggplot2建立的散點圖萼片。長度變量被映射到x美學,萼片寬度變量映射到y美學。通過添加theme_bw()層應用了一個黑白主題。

建立圖2.1中的圖表的代碼如清單2.15所示。函數ggplot()将您提供的資料作為第一個參數,而函數aes()将作為第二個參數(稍後将詳細讨論這個問題)。這将擷取資料,并基于資料建立一個繪圖環境、軸和軸标簽。

aes()函數是美學映射的簡稱,如果您習慣了以R為基礎的繪圖,那麼它可能對您來說是新的。美學是一種可以由資料中的變量控制的圖形特征。美學的例子包括x軸、y軸、顔色、形狀、大小,甚至繪制在圖上的資料點的透明度。在清單2.15中的函數調用中,我們要求ggplot 分别映射Sepal.Length和Sepal.Width變量到x軸和y軸。

清單2.15 使用ggplot()函數繪制資料

library(ggplot2)
data(iris)
myPlot <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +
geom_point() +
theme_bw()
myPlot           

提示: 注意,我們不需要用引号括起來; ggplot很聰明!

我們用+符号完成了這條線,我們用它向我們的圖形添加額外的層(我們可以添加任意多的層來建立我們想要的圖形)。約定是,當我們向我們的圖中添加額外的層時,我們使用+完成目前層,并将下一層放置在新的直線上。這有助于保持可讀性。

重要:當向初始的ggplot()函數調用添加圖層時,每一行都需要以+結尾; 你不能把+放在一個新的行上。

下一層是一個名為geom_point()的函數。geom代表幾何對象,是用來表示資料的圖形元素,如點、條、線、盒、須等,是以生成這些層的函數都被命名為geom_[圖形元素]。例如,讓我們在我們的圖中添加兩個新層,geom_density_2d(),它添加密度輪廓,以及geom_smooth(),它将帶有置信帶的平滑線貼合到資料中。

第二章 使用tidyverse整理、操作和繪制資料

圖2.2 與圖中相同的散點圖,添加了2D密度等高線和平滑線作為圖層(分别使用geom_density_2d()和geom_smooth函數)。

圖2.2中的圖是相當複雜的,要在以R為基數的情況下實作同樣的效果需要很多行代碼。看看清單2.16,看看使用ggplot2實作這一點有多容易!

清單2.16 向ggplot對象添加額外的geom層

myPlot +
geom_density_2d() +
geom_smooth()           

注意,您可以将ggplot儲存為一個命名對象,并簡單地向該對象添加新層,而不是每次都從頭建立plot。

最後,突出顯示資料中的分組結構通常很重要,我們可以通過添加顔色或形狀美感映射來實作這一點,如圖2.3所示。

第二章 使用tidyverse整理、操作和繪制資料

圖2.3與圖相同的散點圖,其中物種變量映射到形狀和顔色美學。

生成這些圖的代碼如清單2.17所示。它們之間的唯一差別是物種是作為形狀或顔色美學的論據而給出的。

清單2.17 将物種映射到形狀和顔色美學

ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, shape = Species)) +
geom_point() +
theme_bw()
ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, col = Species)) +
geom_point() +
theme_bw()           

注意,當添加除x和y之外的美學映射時,ggplot如何自動生成一個圖例?對于基礎圖形,你必須手動生成這些内容!

關于ggplot,我想教給你們的最後一件事,是它極其強大的面向面功能。有時,我們可能希望建立資料的子圖,其中每個子圖或facet顯示屬于資料中某些組的資料。

第二章 使用tidyverse整理、操作和繪制資料

圖2.4顯示了相同的資料,但不同的iris物種被繪制在不同的子圖或“小平面”上。

圖2 - 4顯示了相同的iris資料,但這次是由“Species”變量分面。建立該圖的代碼如清單2. 18所示。我隻是在ggplot 2調用中添加了一個facet_wrap()層,并指定我希望它按(~)Species劃分面。

清單2.18 使用group_by()函數對資料進行分組

ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +
facet_wrap(~ Species) +
geom_point() +
theme_bw()           

雖然你可以用ggplot2做的事情比這裡介紹的要多得多(包括定制幾乎所有東西的外觀),但我隻想讓你了解如何建立基本的繪圖,以複制你在整本書中看到的那些繪圖。如果你想把你的資料可視化技能提升到一個新的水準,我強烈推薦Hadley Wickham的ggplot2(Springer International Publishing,2016)。

提示: ggplot上繪圖元素的順序很重要!繪圖元素實際上是按順序分層的,是以稍後在ggplot調用中添加的元素将位于所有其他元素的頂部。重新排序geom_density_2d()和geom_point() 函數,用于建立圖2.2,并仔細觀察發生了什麼(圖可能看起來一樣,但實際上不是!)

2.6 tidyr包是什麼及其功能

在第2.1節中,我們看了一個不整潔的資料示例,然後看了以整潔格式重組後的相同資料。作為資料科學家,我們通常無法控制資料的格式,我們通常不得不将雜亂的資料重組為整齊的格式,以便将其傳遞到機器學習管道中。讓我們做一個不整潔的tibble并将其轉換為整潔的格式。

在代碼表2.19中,我們有一小部分虛構的患者資料,其中患者的體重指數(BMI)是在一些虛構的幹預開始後的第0個月、第3個月和第6個月測量的。資料是否整齊?不,資料裡隻有3個變量:

  • 患者ID
  • 測量月份
  • BMI測量

但是我們有4列! 此外,每行不包含單個觀察的資料,它們都包含對該患者進行的所有觀察。

清單2.19 淩亂的tibble

library(tibble)
library(tidyr)
patientData <- tibble(Patient = c("A", "B", "C"),
Month0 = c(21, 17, 29),
Month3 = c(20, 21, 27),
Month6 = c(21, 22, 23))
patientData

# A tibble: 3 × 4
  Patient Month0 Month3 Month6
  <chr>    <dbl>  <dbl>  <dbl>
1 A           21     20     21
2 B           17     21     22
3 C           29     27     23           

要将這個淩亂的tibble轉換成整潔的tibble,可以使用tidyr的gather()函數。

gather()函數将資料作為它的第一個參數。key參數定義了新變量的名稱,該變量将表示我們正在“收集”的列。在本例中,我們收集的列被命名為Month0、Month3和Month6,是以我們将儲存這些“鍵”的新列稱為Month。value參數定義了新變量的名稱,該變量将表示我們正在收集的列中儲存的資料。在本例中,這些值是BMI測量值,是以我們将表示這些“值”的新列稱為BMI。

最後一個參數是一個向量,它定義了要“收集”哪些變量并将其轉換為鍵值對。通過使用-Patient in,我們告訴gather()使用除了辨別變量Patient之外的所有變量。

清單2.20 使用gather()函數整理資料

tidyPatientData <- gather(patientData, key = Month,
value = BMI, -Patient)
tidyPatientData

# A tibble: 9 × 3
  Patient Month    BMI
  <chr>   <chr>  <dbl>
1 A       Month0    21
2 B       Month0    17
3 C       Month0    29
4 A       Month3    20
5 B       Month3    21
6 C       Month3    27
7 A       Month6    21
8 B       Month6    22
9 C       Month6    23           

我們可以通過輸入以下代碼來實作相同的結果(注意,它們傳回的tibbles是相同的):

清單2.21 選擇收集列的不同方法

gather(patientData, key = Month, value = BMI, Month0:Month6)
gather(patientData, key = Month, value = BMI, c(Month0, Month3, Month6))

# A tibble: 9 × 3
  Patient Month    BMI
  <chr>   <chr>  <dbl>
1 A       Month0    21
2 B       Month0    17
3 C       Month0    29
4 A       Month3    20
5 B       Month3    21
6 C       Month3    27
7 A       Month6    21
8 B       Month6    22
9 C       Month6    23           

側邊欄 轉換資料到寬格式

patientData tibble中的資料結構稱為寬格式,其中單個病例的觀察結果被放置在同一行中,跨越多個列。大多數情況下,我們希望使用整潔的資料,因為它使生活變得更簡單:我們可以立即看到我們有哪些變量,分組結構很清楚,大多數函數被設計成可以輕松地使用整潔的資料。然而,在一些罕見的情況下,我們需要将整潔的資料轉換為寬格式,這可能是因為我們需要的函數期望使用這種格式的資料。我們可以使用spread()函數将整潔的資料轉換為寬格式:

spread(tidyPatientData, key = Month, value = BMI)

# A tibble: 3 × 4
  Patient Month0 Month3 Month6
  <chr>    <dbl>  <dbl>  <dbl>
1 A           21     20     21
2 B           17     21     22
3 C           29     27     23           

它的使用與gather的用法完全相反:我們提供鍵和值參數作為使用gather()函數建立的鍵和值列的名稱,該函數将這些參數轉換為寬格式。

2.7 purrr包是什麼和它做什麼

我要向您展示的最後一個tidyverse包是purrr(帶有三個“r”)。R為我們提供了将其用作函數式程式設計語言的工具。這意味着它為我們提供了一種工具,可以将所有的計算視為傳回其值的數學函數,而不改變工作空間中的任何東西。

注:當函數不傳回一個值(比如畫一個圖或改變一個環境),這些被稱為函數。不産生任何副作用的副作用函數稱為純函數。

代碼清單2.22顯示了一個簡單的函數示例,它可以産生副作用,也可以不産生副作用。pure()函數傳回a+1的值,但不會改變全局環境中的任何東西。side_effects()函數使用超指派操作符<<-在全局環境中重新指派對象a。每次運作pure()函數,它都會給出相同的輸出,但是運作side_effect()函數每次都會給出一個新值(并且也會影響後續pure()函數調用的輸出)。

清單2.22 建立一個數字向量清單

a <- 20
pure <- function() {
a <- a + 1
a
}
side_effect <- function() {
a <<- a + 1
a
}
c(pure(), pure())
#[1] 21 21
c(side_effect(), side_effect())
#[1] 21 22           

在沒有副作用的情況下調用函數通常是可取的,因為這樣更容易預測函數将做什麼。如果函數沒有副作用,可以用不同的實作替換它,而不會破壞代碼中的任何東西。

這樣做的一個重要後果是,for循環在單獨使用時會産生不必要的副作用(如修改現有變量),是以可以将其封裝在其他函數中。将for循環封裝在其中的函數,允許我們疊代vector/list的每個元素(包括資料幀或tibbles的列和行),對該元素應用一個函數,并傳回整個疊代過程的結果。

注如果您熟悉apply()系列的base R函數,那麼purrr包中的函數可以幫助我們實作同樣的功能,但是使用了一緻的文法和一些友善的特性。

2.7.1 用map()替換for循環

purrr包為我們提供了一組函數,允許我們對清單的每個元素應用一個函數。我們使用哪一個purrr函數取決于輸入的數量和我們想要的輸出,在本節中,我将示範這個包中最常用函數的重要性。假設您有一個包含三個數值向量的清單:

清單2.23 建立數值向量清單

listOfNumerics <- list(a = rnorm(5),
                       b = rnorm(9),
                       c = rnorm(10))
listOfNumerics           
第二章 使用tidyverse整理、操作和繪制資料

現在,假設我們想對三個清單元素分别應用一個函數,比如傳回每個元素長度的函數。我們可以使用一個for循環來完成這個任務,length()疊代每個清單元素,并将長度儲存為一個新清單的元素,我們預先定義了這個新清單以儲存時間。

清單2.24 使用循環對清單疊代函數

elementLengths <- vector("list", length = 3)
for(i in seq_along(listOfNumerics)) {
elementLengths[[i]] <- length(listOfNumerics[[i]])
}
elementLengths

[[1]]
[1] 5

[[2]]
[1] 9

[[3]]
[1] 10           

這段代碼很難讀懂,需要我們預定義一個空向量來防止循環變慢,并且有一個副作用:如果你再次運作循環,它将覆寫elementLengths清單。

相反,我們可以用map()函數替換for循環。第一個參數是映射族函數就是我們要疊代的資料。第二個參數是我們應用于每個清單元素的函數。看一下圖2.5,它展示了map()函數是如何将函數應用于清單/向量的每個元素,并傳回包含輸出的清單的。

第二章 使用tidyverse整理、操作和繪制資料

圖2.5 map()函數以向量或清單為輸入,對每個元素單獨應用函數,并傳回傳回值的清單

在此示例中,map()函數将length()函數應用于listOfNumerics清單,并将這些值作為清單傳回。請注意,map()函數還使用輸入元素的名稱作為輸出元素(a、b和c)的名稱。

清單2.25 使用map()疊代函數周遊清單

map(listOfNumerics, length)
$a
[1] 5

$b
[1] 9

$c
[1] 10           

注: 如果您熟悉apply系列函數,那麼map()就是lapply()的purrr等價物。

我希望你能馬上看到,與我們建立的for循環相比,它的代碼編寫起來簡單多了,閱讀起來也容易多了!

2.7.2 傳回原子向量而不是清單

是以map()函數總是傳回一個清單。但是,如果我們不想傳回清單,而是想傳回原子向量呢?purrr包為我們提供了許多函數來實作這一點:

  • map_dbl(),傳回雙精度(小數)向量;
  • map_chr(),傳回字元向量;
  • map_int(),傳回整數向量;
  • map_lgl(),傳回邏輯向量.

這些函數中的每一個都傳回由其字尾指定的類型的原子向量。這樣,我們就不得不考慮并預先确定輸出資料的類型。例如,我們可以像前面一樣使用map_int()函數傳回每個listOfNumerics清單元素的長度。就像map()一樣,map_int()函數應用length()函數添加到清單中的每個元素,但傳回的輸出是一個整數向量。我們可以使用map_chr()函數來做同樣的事情,它将輸出強制為字元向量,但是map_lgl()函數抛出錯誤,因為它不能将輸出強制為邏輯向量。

注: 強制我們顯式地聲明想要傳回的輸出類型可以防止意外輸出類型的bug。

清單2.26 使用map_int()、map_chr()和map_lgl()

map_int(listOfNumerics, length)
map_chr(listOfNumerics, length)
map_lgl(listOfNumerics, length)           
第二章 使用tidyverse整理、操作和繪制資料

最後,我們可以使用map_df()函數傳回一個tibble,而不是一個清單。

清單2.27 使用map_df()傳回tibble

map_df(listOfNumerics, length)
# A tibble: 1 × 3
      a     b     c
  <int> <int> <int>
1     5     9    10           

2.7.3 在map()家族中使用匿名函數

有時候我們想對清單中的每個元素應用一個函數,但我們還沒有定義這個函數.我們動态定義的函數被稱為匿名函數,當我們應用的函數使用頻率不高,不需要将其指派給對象時,匿名函數會很有用。使用基數R,我們通過簡單地調用function()函數來定義一個匿名函數:

清單2.28 使用'function(.)定義匿名函數

map(listOfNumerics, function(.) . + 2)           

重要:注意到匿名函數中的.了嗎?這隻表示map()目前正在疊代的元素。

函數(.)後的表達式是函數的主體。這種文法沒有任何問題,因為它工作得非常好,但是purrr為我們提供了function(.)的簡寫。它隻是~(潮汐)符号。是以,我們可以将map()調用簡化為:

map(listOfNumerics, ~. + 2)           

我們可以簡單地将function(.)替換為~。

2.7.4 使用walk()産生函數的副作用

有時候我們需要疊代一個函數以獲得它的副作用。可能最常見的例子是當我們想要産生一系列的情節。在這種情況下,我們可以使用walk()函數對清單的每個元素應用一個函數,以産生該函數的副作用。walk()函數還傳回我們傳遞給它的原始輸入資料,是以它對于繪制一系列管道操作中的中間步驟非常有用。下面是一個walk()的例子,它被用來為清單中的每個元素建立一個單獨的直方圖:

par(mfrow = c(1, 3))
walk(listOfNumerics, hist)           

圖2.6顯示了結果圖。

第二章 使用tidyverse整理、操作和繪制資料

圖2.6 使用walk()周遊hist()函數的結果

但是,如果我們想使用每個清單元素的名稱,作為每個直方圖的标題呢?我們可以使用iwalk()函數來實作這一點,該函數使每個元素的名稱或索引可用。在提供給iwalk()的函數中,可以使用.x引用要周遊的清單元素,使用.y引用它的名稱/索引。

iwalk(listOfNumerics, ~hist(.x, main = .y))           

注意:每個函數都有一個i版本,讓我們引用每個map() i元素的名稱/索引。

得到的圖如圖2.7所示。注意,現在每個直方圖的标題顯示了它所繪制的清單元素的名稱。

第二章 使用tidyverse整理、操作和繪制資料

圖2.7 使用iwalk()對清單中的每個元素“周遊”hist()的結果

2.7.5 同時周遊多個清單

有時,我們希望周遊的資料并不包含在單個清單中。假設我們要将清單中的每個元素乘以一個不同的值。我們可以将這些值存儲在一個單獨的清單中,并使用map2()函數同時周遊兩個清單,用第一個清單中的元素乘以第二個清單中的元素。這一次,我們不再使用.來引用資料,而是分别使用.x和.y來特别引用第一個和第二個清單。

multipliers <- list(0.5, 10, 3)
map2(.x = listOfNumerics, .y = multipliers, ~.x * .y)           
第二章 使用tidyverse整理、操作和繪制資料

想象一下,您想要疊代三個或更多的清單,而不是隻疊代兩個清單。pmap()函數允許我們同時周遊多個清單。當我想測試一個函數的多個參數組合時,我使用pmap()。rnorm()函數從正态分布中抽取一個随機樣本,它有三個參數:n(樣本的數量)、mean(分布的中心)和sd(标準差)。我們可以為每個組合建立一個值清單,然後使用pmap()疊代每個清單,以在每個組合上運作該函數。我首先使用expand.grid()函數建立一個包含輸入向量的每個組合的資料幀。由于資料幀實際上隻是列的清單,是以為pmap()提供一個列将在資料幀中的每一列上疊代一個函數。本質上,我們要求pmap()疊代的函數将使用資料幀的每一行包含的參數運作。是以,pmap()将傳回8個不同的随機樣本,一個對應于資料幀中每個參數的組合。

由于所有map-family函數的第一個參數是我們希望周遊的資料,是以可以使用%>%操作符将它們連結在一起。在這裡,我将pmap()傳回的随機樣本管道到iwalk()函數中,為每個樣本繪制單獨的直方圖,用索引标記。

注釋:該函數調用簡單地将繪圖裝置par(mfrow = c(2,4))分割為兩行四列作為基本繪圖。

清單2.29 使用pmap()疊代多個清單

arguments <- expand.grid(n = c(100, 200),
mean = c(1, 10),
sd = c(1, 10))
arguments

    n mean sd
1 100    1  1
2 200    1  1
3 100   10  1
4 200   10  1
5 100    1 10
6 200    1 10
7 100   10 10
8 200   10 10

par(mfrow = c(2, 4))
pmap(arguments, rnorm) %>%
iwalk(~hist(.x, main = paste("Element", .y)))           

清單2.29代碼生成的圖形如圖2.7所示。

第二章 使用tidyverse整理、操作和繪制資料

圖2.8 pmap()函數用于疊代rnorm()函數在三個參數向量上。pmap()的輸出通過管道傳遞到iwalk(),以便在每個随機樣本上疊代hist()函數。

如果你還沒有記住我們剛剛講過的所有的tidyverse函數,也不要擔心,我們将在整本書的機器學習過程中使用這些工具。使用tidyverse工具,我們還可以做比這裡介紹的更多的事情,但是對于解決您将遇到的最常見的資料操作問題來說,這些已經足夠了。現在你已經掌握了如何使用這本書的知識,讓我們深入了解機器學習的理論。

2.8 小結

  • Tidy資料是矩形資料,其中每一行是一個單獨的觀察,每一列是一個變量。
  • 在将資料傳遞到機器學習函數之前,確定資料是整潔的格式通常是很重要的。
  • Tibbles是資料幀的現代版,它有更好的列印矩形資料的規則,永不改變變量類型,并總是傳回另一個tibble時,使用[
  • dplyr包為資料操作過程提供了人類可讀的類動詞函數,其中最重要的是select(),filter(),group_by(),summarize()和arrange()
  • dplyr最強大的方面是能夠使用管道%>%操作符一起執行函數,該操作符在函數的左側傳遞函數的輸出,作為函數右側的第一個參數
  • ggplot2包是一個現代且流行的R繪圖系統,它允許您以一種簡單、分層的方式建立有效的繪圖
  • tidyr包提供了重要的函數gather(),它允許您輕松地将不整潔的資料轉換為整潔的格式(與此函數相反的是spread(),它将整潔的資料轉換為寬格式
  • purrr包為我們提供了一種簡單、一緻的方式,可以疊代地在清單中的每個元素上應用函數