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 の関数をいくつか紹介します。
例として、 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
この 1999 と 2000 はそれぞれ、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 の例では、
従って、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 では、既存のデータに追加の列を作成することができます。
例えば先述の 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 を使って、何かの変数ごとにデータをグループ化することも可能です。
ここでは例を変えて、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(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
です。
今度は、欲しい列だけを抽出します。 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
とできます。
ベース関数の 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
データを横に繋げる際に、 ~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(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(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(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 で補完されています。