What’s the tidyverse ?

tidyverse は Hadley Wickham らによって作られた、データサイエンス用のパッケージ集です。
ggplot2 , dplyr , tibble などが含まれています。
重要な特徴として、C言語ベースで書かれているため、一般的なRのコードよりも動作が早いことが挙げられます。
tidyverse に含まれる関数には、Rに最初から入っている関数と動作がほとんど同じであるものも少なくないですが、動作が早かったり、より細かく調整できたりします。
そのため、実用的にRを使っている人の多くが、tidyverseを活用してコードを書いていると思います。
tidyverse の入門書としては、Hadley らが書いた R for Data Science があります。

how the tidyverse makes data science faster, easier and more fun

を教えてくれる本で、英語・日本語の本が書店で販売されている他、英語版なら オンラインで無料で閲覧することができます。
包括的な説明は上記サイトに任せるとして(私自身も読んでいる途中です)、以下では私がコードを書く際によく使う tidyverse の関数をいくつか紹介します。

pivot_longer

例として、 library(tidyverse) をすると読み込まれる、 table4a を使います。

library(tidyverse)
table4a
> # A tibble: 3 × 3
>   country     `1999` `2000`
> * <chr>        <int>  <int>
> 1 Afghanistan    745   2666
> 2 Brazil       37737  80488
> 3 China       212258 213766

この 19992000 はそれぞれ、1999年と2000年の人口を表しているようです。
このままでも国ごとの人口の平均や変化率を求めることはできますが、Rでtidyverseを扱うにはやや不向きな形をしているので、

> # A tibble: 6 × 3
>   country      year population
>   <chr>       <int>      <int>
> 1 Afghanistan  1999        745
> 2 Afghanistan  2000       2666
> 3 Brazil       1999      37737
> 4 Brazil       2000      80488
> 5 China        1999     212258
> 6 China        2000     213766

という形に直したいと思います。知っている限りでは、この形に直した方が何かと処理が楽です。

こういった、「横長のデータを縦長に変形する」作業は、 pivot_longer() で行います。
反対に、「縦長のデータを横長に変形する」作業は、 pivot_wider() です。

pivot_longer()では、

  • data= でどのデータについて処理をするのかを、
  • cols= でどの列名をこれから変数として扱うかを、
  • names_to= で新たに変数として扱う列名につける名前を、
  • values_to= で今まで列に入っていた値に新たに付け直す名前を、

それぞれ指定します。今回の table4a の例では、

  • table4a というデータについて
  • ‘1999’ , ‘2000’ という列名を (country 以外を)
  • year という変数として扱い
  • ‘1999’, ‘2000’ に入っていた値に population という名前を付け直します。

従って、tmp という名前をつけて結果を保存するとしたら、コードの書き方は、

tmp <- pivot_longer(data = table4a, cols = -country, names_to = "year", values_to = "population")
tmp
> # A tibble: 6 × 3
>   country     year  population
>   <chr>       <chr>      <int>
> 1 Afghanistan 1999         745
> 2 Afghanistan 2000        2666
> 3 Brazil      1999       37737
> 4 Brazil      2000       80488
> 5 China       1999      212258
> 6 China       2000      213766

です。 cols=-country は、 country 以外 の全ての列を指しています。
よく見ると、 year の class が <chr> すなわち character になってしまっています。
pivot_longer 上で names_transform= を使って直すこともできますが、少し複雑なのでシンプルに、

tmp$year <- as.integer(tmp$year)

や、後述の mutate などで直すことにします。

mutate

mutate では、既存のデータに追加の列を作成することができます。
例えば先述の table4a を変形した tmp というデータについて、 log(population) の列を logP として追加してみましょう。
mutate は、 mutate(列名=列の中身) という書き方をします。
なお、ただ mutate を実行しただけでは、追加の列はその場限りとなってしまうので、 tmp に追加した列を今後も扱いたいならば、
tmp <- tmp %>% mutate~ とするか、 tmp2 <- tmp %>% mutate(~ などの書き方をしましょう。

tmp2 <- tmp %>%
    mutate(logP = log(population))
tmp2
> # A tibble: 6 × 4
>   country     year  population  logP
>   <chr>       <chr>      <int> <dbl>
> 1 Afghanistan 1999         745  6.61
> 2 Afghanistan 2000        2666  7.89
> 3 Brazil      1999       37737 10.5 
> 4 Brazil      2000       80488 11.3 
> 5 China       1999      212258 12.3 
> 6 China       2000      213766 12.3

新たに列を追加できました。
ついでに、 year が <chr> なのも mutate で直しましょう。 mutate の列名に既存の列名を指定すると、その列を上書きすることができます。

(tmp2 <- tmp2 %>%
    mutate(year = as.integer(year)))
> # A tibble: 6 × 4
>   country      year population  logP
>   <chr>       <int>      <int> <dbl>
> 1 Afghanistan  1999        745  6.61
> 2 Afghanistan  2000       2666  7.89
> 3 Brazil       1999      37737 10.5 
> 4 Brazil       2000      80488 11.3 
> 5 China        1999     212258 12.3 
> 6 China        2000     213766 12.3

mutate では複数の列を同時に作れるので、まとめて書くことも可能です。

(tmp3 <- tmp %>%
    mutate(logP = log(population), year = as.integer(year)))
> # A tibble: 6 × 4
>   country      year population  logP
>   <chr>       <int>      <int> <dbl>
> 1 Afghanistan  1999        745  6.61
> 2 Afghanistan  2000       2666  7.89
> 3 Brazil       1999      37737 10.5 
> 4 Brazil       2000      80488 11.3 
> 5 China        1999     212258 12.3 
> 6 China        2000     213766 12.3

group_by

group_by を使って、何かの変数ごとにデータをグループ化することも可能です。
ここでは例を変えて、Rに元から入っている、 Titanic というデータを使ってみることにします。
Titanic は、客室のグレード・性別・子供/大人 ごとに、タイタニック号の事故で亡くなった人数と生還した人数をまとめています。
まず、元のデータが表形式なので、データフレームに変形します。 tibble() はよく tidyverse で扱われるデータ形式です。

(titanicdata <- as.tibble(Titanic))
> # A tibble: 32 × 5
>    Class Sex    Age   Survived     n
>    <chr> <chr>  <chr> <chr>    <dbl>
>  1 1st   Male   Child No           0
>  2 2nd   Male   Child No           0
>  3 3rd   Male   Child No          35
>  4 Crew  Male   Child No           0
>  5 1st   Female Child No           0
>  6 2nd   Female Child No           0
>  7 3rd   Female Child No          17
>  8 Crew  Female Child No           0
>  9 1st   Male   Adult No         118
> 10 2nd   Male   Adult No         154
> # … with 22 more rows

例えば Class ごとに、データをまとめてみましょう。

(titanicdata <- group_by(titanicdata, Class))
> # A tibble: 32 × 5
> # Groups:   Class [4]
>    Class Sex    Age   Survived     n
>    <chr> <chr>  <chr> <chr>    <dbl>
>  1 1st   Male   Child No           0
>  2 2nd   Male   Child No           0
>  3 3rd   Male   Child No          35
>  4 Crew  Male   Child No           0
>  5 1st   Female Child No           0
>  6 2nd   Female Child No           0
>  7 3rd   Female Child No          17
>  8 Crew  Female Child No           0
>  9 1st   Male   Adult No         118
> 10 2nd   Male   Adult No         154
> # … with 22 more rows

データの中身に変化はありませんが、上に Groups. という表示が追加されました。

この状態で mutate(num_of_people=sum(n)) としてみましょう。

(titanicdata <- titanicdata %>%
    mutate(num_of_people = sum(n)))
> # A tibble: 32 × 6
> # Groups:   Class [4]
>    Class Sex    Age   Survived     n num_of_people
>    <chr> <chr>  <chr> <chr>    <dbl>         <dbl>
>  1 1st   Male   Child No           0           325
>  2 2nd   Male   Child No           0           285
>  3 3rd   Male   Child No          35           706
>  4 Crew  Male   Child No           0           885
>  5 1st   Female Child No           0           325
>  6 2nd   Female Child No           0           285
>  7 3rd   Female Child No          17           706
>  8 Crew  Female Child No           0           885
>  9 1st   Male   Adult No         118           325
> 10 2nd   Male   Adult No         154           285
> # … with 22 more rows

よく見てみると、各Classでnum_of_peopleが等しくなっています。これは、各Classごとに、人数の合計を計算したためです。

次に、各Classごとに生存率 survival_rate を計算してみましょう。
Survived が Yes/No だと不便なので 1/0 のダミー変数に変えて、\(Survived\ \cdot\ n\)の合計が生存者数になるようにします。
\(survival\_rate=\frac{100 \times\sum (Survived\ \cdot\ n)}{num\_of\_people}\) です。

(titanicdata <- titanicdata %>%
    mutate(Survived = if_else(Survived == "Yes", 1, 0), survival_rate = sum(Survived *
        n) * 100/num_of_people))
> # A tibble: 32 × 7
> # Groups:   Class [4]
>    Class Sex    Age   Survived     n num_of_people survival_rate
>    <chr> <chr>  <chr>    <dbl> <dbl>         <dbl>         <dbl>
>  1 1st   Male   Child        0     0           325          62.5
>  2 2nd   Male   Child        0     0           285          41.4
>  3 3rd   Male   Child        0    35           706          25.2
>  4 Crew  Male   Child        0     0           885          24.0
>  5 1st   Female Child        0     0           325          62.5
>  6 2nd   Female Child        0     0           285          41.4
>  7 3rd   Female Child        0    17           706          25.2
>  8 Crew  Female Child        0     0           885          24.0
>  9 1st   Male   Adult        0   118           325          62.5
> 10 2nd   Male   Adult        0   154           285          41.4
> # … with 22 more rows

ここでは、 if_else という関数を使っています。
これは if_else(CONDITION,V1,V2) という書き方をして、 CONDITION が満たされるなら V1, 満たされないなら V2 を返します。
今回だと、 Survived="Yes" が成り立つ行に1、その他の行には0が返されています。

ところで、生存率だけを見たい場合、各Classにつき1行、合計4行だけあれば十分なはずです。
列についても、Classとsurvival_rateの2列だけあれば十分です。
こういった場合、 titanicdata %>% distinct(Class,survival_rate) とすれば、Classとsurvival_rateのユニークな組み合わせだけを返してくれます。

titanicdata %>%
    distinct(Class, survival_rate)
> # A tibble: 4 × 2
> # Groups:   Class [4]
>   Class survival_rate
>   <chr>         <dbl>
> 1 1st            62.5
> 2 2nd            41.4
> 3 3rd            25.2
> 4 Crew           24.0

また、 titanicdata %>% distinct(Class,survival_rate,.keep_all=TRUE) とすれば、Classとsurvival_rateの組み合わせが重複しないように行を消しつつ、他の列は消去せずにデータを返してくれます。

titanicdata %>%
    distinct(Class, survival_rate, .keep_all = TRUE)
> # A tibble: 4 × 7
> # Groups:   Class [4]
>   Class Sex   Age   Survived     n num_of_people survival_rate
>   <chr> <chr> <chr>    <dbl> <dbl>         <dbl>         <dbl>
> 1 1st   Male  Child        0     0           325          62.5
> 2 2nd   Male  Child        0     0           285          41.4
> 3 3rd   Male  Child        0    35           706          25.2
> 4 Crew  Male  Child        0     0           885          24.0

それ以外の方法として、列名を打つのが面倒なため、私が group_by() と併せてよく使うのが slice_head() です。
slice_head() はデータの先頭行だけを返す関数ですが、 group_by() と併用すると、各グループの先頭行を返してくれます。
例えば今はClassでgroup_byされているので、

titanicdata %>%
    slice_head()
> # A tibble: 4 × 7
> # Groups:   Class [4]
>   Class Sex   Age   Survived     n num_of_people survival_rate
>   <chr> <chr> <chr>    <dbl> <dbl>         <dbl>         <dbl>
> 1 1st   Male  Child        0     0           325          62.5
> 2 2nd   Male  Child        0     0           285          41.4
> 3 3rd   Male  Child        0    35           706          25.2
> 4 Crew  Male  Child        0     0           885          24.0

で、 titanicdata %>% distinct(Class,survival_rate,.keep_all=TRUE) と同じことができます。

 

さて、 group_by でグルーピングしたままだと、全体に対して何か処理を行いたい時に、グループ単位で処理されてしまい困ることがあります。
トラブルを避けるために、グループ分けして行いたい処理が終わった際には、 ungroup() を実行します。

(titanicdata <- titanicdata %>%
    ungroup())
> # A tibble: 32 × 7
>    Class Sex    Age   Survived     n num_of_people survival_rate
>    <chr> <chr>  <chr>    <dbl> <dbl>         <dbl>         <dbl>
>  1 1st   Male   Child        0     0           325          62.5
>  2 2nd   Male   Child        0     0           285          41.4
>  3 3rd   Male   Child        0    35           706          25.2
>  4 Crew  Male   Child        0     0           885          24.0
>  5 1st   Female Child        0     0           325          62.5
>  6 2nd   Female Child        0     0           285          41.4
>  7 3rd   Female Child        0    17           706          25.2
>  8 Crew  Female Child        0     0           885          24.0
>  9 1st   Male   Adult        0   118           325          62.5
> 10 2nd   Male   Adult        0   154           285          41.4
> # … with 22 more rows

filter

データのうち、特定の条件を満たす行だけを抽出する際には filter(CONDITION) を使います。
base関数(Rに最初からあるやつ)のsubset()とほとんど同じです。
例えばタイタニックのデータから女性のデータだけ抽出したい際には、

titanicdata %>%
    filter(Sex == "Female")
> # A tibble: 16 × 7
>    Class Sex    Age   Survived     n num_of_people survival_rate
>    <chr> <chr>  <chr>    <dbl> <dbl>         <dbl>         <dbl>
>  1 1st   Female Child        0     0           325          62.5
>  2 2nd   Female Child        0     0           285          41.4
>  3 3rd   Female Child        0    17           706          25.2
>  4 Crew  Female Child        0     0           885          24.0
>  5 1st   Female Adult        0     4           325          62.5
>  6 2nd   Female Adult        0    13           285          41.4
>  7 3rd   Female Adult        0    89           706          25.2
>  8 Crew  Female Adult        0     3           885          24.0
>  9 1st   Female Child        1     1           325          62.5
> 10 2nd   Female Child        1    13           285          41.4
> 11 3rd   Female Child        1    14           706          25.2
> 12 Crew  Female Child        1     0           885          24.0
> 13 1st   Female Adult        1   140           325          62.5
> 14 2nd   Female Adult        1    80           285          41.4
> 15 3rd   Female Adult        1    76           706          25.2
> 16 Crew  Female Adult        1    20           885          24.0

です。

select

今度は、欲しい列だけを抽出します。 titanicdata[c("Age","Survived")] などでもできますが、

titanicdata %>%
    select(Age, Survived)
> # A tibble: 32 × 2
>    Age   Survived
>    <chr>    <dbl>
>  1 Child        0
>  2 Child        0
>  3 Child        0
>  4 Child        0
>  5 Child        0
>  6 Child        0
>  7 Child        0
>  8 Child        0
>  9 Adult        0
> 10 Adult        0
> # … with 22 more rows

でも動作します。これの良いところは、 starts_with() などの関数を使って、まとめて選択できる点です。
例えば s から始まる列だけを taitanicdata から抽出したい場合、

titanicdata %>%
    select(starts_with("s"))
> # A tibble: 32 × 3
>    Sex    Survived survival_rate
>    <chr>     <dbl>         <dbl>
>  1 Male          0          62.5
>  2 Male          0          41.4
>  3 Male          0          25.2
>  4 Male          0          24.0
>  5 Female        0          62.5
>  6 Female        0          41.4
>  7 Female        0          25.2
>  8 Female        0          24.0
>  9 Male          0          62.5
> 10 Male          0          41.4
> # … with 22 more rows

とできます。

arrange

ベース関数の sort() と同じように、 arrange(キー列) とすることで、キー列に応じてデータを並び替えられます。
arrange(desc(キー列)) とすることで、降順(大きい順)に並び替えられます。

titanicdata %>%
    arrange(num_of_people)
> # A tibble: 32 × 7
>    Class Sex    Age   Survived     n num_of_people survival_rate
>    <chr> <chr>  <chr>    <dbl> <dbl>         <dbl>         <dbl>
>  1 2nd   Male   Child        0     0           285          41.4
>  2 2nd   Female Child        0     0           285          41.4
>  3 2nd   Male   Adult        0   154           285          41.4
>  4 2nd   Female Adult        0    13           285          41.4
>  5 2nd   Male   Child        1    11           285          41.4
>  6 2nd   Female Child        1    13           285          41.4
>  7 2nd   Male   Adult        1    14           285          41.4
>  8 2nd   Female Adult        1    80           285          41.4
>  9 1st   Male   Child        0     0           325          62.5
> 10 1st   Female Child        0     0           325          62.5
> # … with 22 more rows

left_join / right_join / full_join

データを横に繋げる際に、 ~join シリーズを使います。
例として、今度はスターウォーズシリーズのキャラクターの情報がまとまった、 starwars というデータを使いましょう。
データをくっつける作業をするために、 color を含む列とその他の列にデータを分けます。
また、欠損値の例のために、名前順に starwars を並び替えた後、 color を含む方は 1行目を、含まない方は 2行目を削除しておきます。

(stcolor <- arrange(starwars, name)[-1, ] %>%
    select(name, contains("color")))
> # A tibble: 86 × 4
>    name                hair_color skin_color          eye_color
>    <chr>               <chr>      <chr>               <chr>    
>  1 Adi Gallia          none       dark                blue     
>  2 Anakin Skywalker    blond      fair                blue     
>  3 Arvel Crynyd        brown      fair                brown    
>  4 Ayla Secura         none       blue                hazel    
>  5 Bail Prestor Organa black      tan                 brown    
>  6 Barriss Offee       black      yellow              blue     
>  7 BB8                 none       none                black    
>  8 Ben Quadinaros      none       grey, green, yellow orange   
>  9 Beru Whitesun lars  brown      light               blue     
> 10 Bib Fortuna         none       pale                pink     
> # … with 76 more rows
(stothers <- arrange(starwars, name)[-2, ] %>%
    select(name, !contains("color")))
> # A tibble: 86 × 11
>    name   height  mass birth_year sex    gender homeworld species films vehicles
>    <chr>   <int> <dbl>      <dbl> <chr>  <chr>  <chr>     <chr>   <lis> <list>  
>  1 Ackbar    180    83       41   male   mascu… Mon Cala  Mon Ca… <chr… <chr [0…
>  2 Anaki…    188    84       41.9 male   mascu… Tatooine  Human   <chr… <chr [2…
>  3 Arvel…     NA    NA       NA   male   mascu… <NA>      Human   <chr… <chr [0…
>  4 Ayla …    178    55       48   female femin… Ryloth    Twi'lek <chr… <chr [0…
>  5 Bail …    191    NA       67   male   mascu… Alderaan  Human   <chr… <chr [0…
>  6 Barri…    166    50       40   female femin… Mirial    Mirial… <chr… <chr [0…
>  7 BB8        NA    NA       NA   none   mascu… <NA>      Droid   <chr… <chr [0…
>  8 Ben Q…    163    65       NA   male   mascu… Tund      Toong   <chr… <chr [0…
>  9 Beru …    165    75       47   female femin… Tatooine  Human   <chr… <chr [0…
> 10 Bib F…    180    NA       NA   male   mascu… Ryloth    Twi'lek <chr… <chr [0…
> # … with 76 more rows, and 1 more variable: starships <list>

では、これらのデータを、 name をキーにして横に繋げましょう。
~join シリーズでは、 ~join(DATA1,DATA2,by="キー列") という書き方をします。
見やすくするためにもう一度 arrange(name) をしています。

left_join

left_join(stcolor, stothers, by = "name") %>%
    arrange(name)
> # A tibble: 86 × 14
>    name    hair_color skin_color  eye_color height  mass birth_year sex   gender
>    <chr>   <chr>      <chr>       <chr>      <int> <dbl>      <dbl> <chr> <chr> 
>  1 Adi Ga… none       dark        blue          NA    NA       NA   <NA>  <NA>  
>  2 Anakin… blond      fair        blue         188    84       41.9 male  mascu…
>  3 Arvel … brown      fair        brown         NA    NA       NA   male  mascu…
>  4 Ayla S… none       blue        hazel        178    55       48   fema… femin…
>  5 Bail P… black      tan         brown        191    NA       67   male  mascu…
>  6 Barris… black      yellow      blue         166    50       40   fema… femin…
>  7 BB8     none       none        black         NA    NA       NA   none  mascu…
>  8 Ben Qu… none       grey, gree… orange       163    65       NA   male  mascu…
>  9 Beru W… brown      light       blue         165    75       47   fema… femin…
> 10 Bib Fo… none       pale        pink         180    NA       NA   male  mascu…
> # … with 76 more rows, and 5 more variables: homeworld <chr>, species <chr>,
> #   films <list>, vehicles <list>, starships <list>

left_join では左側にだけ含まれている人物 Adi Gallia について、右側のデータの列(height~)が全て欠損値 NA であるとして合体させています。
反対に、右側にだけ含まれている人物に関するデータは落としています。

right_join

right_join(stcolor, stothers, by = "name") %>%
    arrange(name)
> # A tibble: 86 × 14
>    name    hair_color skin_color  eye_color height  mass birth_year sex   gender
>    <chr>   <chr>      <chr>       <chr>      <int> <dbl>      <dbl> <chr> <chr> 
>  1 Ackbar  <NA>       <NA>        <NA>         180    83       41   male  mascu…
>  2 Anakin… blond      fair        blue         188    84       41.9 male  mascu…
>  3 Arvel … brown      fair        brown         NA    NA       NA   male  mascu…
>  4 Ayla S… none       blue        hazel        178    55       48   fema… femin…
>  5 Bail P… black      tan         brown        191    NA       67   male  mascu…
>  6 Barris… black      yellow      blue         166    50       40   fema… femin…
>  7 BB8     none       none        black         NA    NA       NA   none  mascu…
>  8 Ben Qu… none       grey, gree… orange       163    65       NA   male  mascu…
>  9 Beru W… brown      light       blue         165    75       47   fema… femin…
> 10 Bib Fo… none       pale        pink         180    NA       NA   male  mascu…
> # … with 76 more rows, and 5 more variables: homeworld <chr>, species <chr>,
> #   films <list>, vehicles <list>, starships <list>

right_join では反対のことが起こり、右側のデータにだけ含まれていた Ackbar について、 color 情報が全てNAになっています。
また、先ほど height~NA だった Adi Gallia はデータから消えています。

full_join

full_join(stcolor, stothers, by = "name") %>%
    arrange(name)
> # A tibble: 87 × 14
>    name    hair_color skin_color  eye_color height  mass birth_year sex   gender
>    <chr>   <chr>      <chr>       <chr>      <int> <dbl>      <dbl> <chr> <chr> 
>  1 Ackbar  <NA>       <NA>        <NA>         180    83       41   male  mascu…
>  2 Adi Ga… none       dark        blue          NA    NA       NA   <NA>  <NA>  
>  3 Anakin… blond      fair        blue         188    84       41.9 male  mascu…
>  4 Arvel … brown      fair        brown         NA    NA       NA   male  mascu…
>  5 Ayla S… none       blue        hazel        178    55       48   fema… femin…
>  6 Bail P… black      tan         brown        191    NA       67   male  mascu…
>  7 Barris… black      yellow      blue         166    50       40   fema… femin…
>  8 BB8     none       none        black         NA    NA       NA   none  mascu…
>  9 Ben Qu… none       grey, gree… orange       163    65       NA   male  mascu…
> 10 Beru W… brown      light       blue         165    75       47   fema… femin…
> # … with 77 more rows, and 5 more variables: homeworld <chr>, species <chr>,
> #   films <list>, vehicles <list>, starships <list>

full_join ではデータは全て保持されて、片方のデータにしかない人物( Ackbar, Adi Gallia )については、足りないデータが NA で補完されています。