続・tidyverse（再）入門

この記事は[R Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rlang)の9日目に書いた[「tidyverse（再）入門」](https://qiita.com/yutakanzawa/items/8952cce11be058a44e76)の内容を補完するものです。その記事の中で「冬休みの宿題」としたbase Rからtidyverseへの書き換えについて書きました。もう春休みですが...

## TL;DR

## この記事で扱わないこと
* ggplot2[^1]
    * baseのplot関数に比べて綺麗なグラフが描ける。
* 機械学習（モデルの構築と検証）[^2]
    * [tidymodels](https://www.tidyverse.org/blog/2018/08/tidymodels-0-0-1/)という、tidyverseの姉妹パッケージがある。

## 準備
### 環境
OS: macOS Mojave 10.14.6
R: 3.6.2 "Dark and Stormy Night"
tidyverse: 1.3.0

ライブラリを読み込みます(`reshape2`は後で使用します)。

```R
library(reshape2)
library(tidyverse)
```

```R:library(tidyverse)
─ Attaching packages ───────────────────── tidyverse 1.3.0 ─
✓ ggplot2 3.2.1     ✓ purrr   0.3.3
✓ tibble  2.1.3     ✓ dplyr   0.8.3
✓ tidyr   1.0.2     ✓ stringr 1.4.0
✓ readr   1.3.1     ✓ forcats 0.4.0
─ Conflicts ────────────────────── tidyverse_conflicts() ─
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
```

### データ
Rに標準装備されているデータセットの1つ`infert`を例として使います。自然流産もしくは誘発流産（人工妊娠中絶）の後の妊娠有無に関する症例対照研究のデータセット[^3]です。

```R
dat <- datasets::infert
head(dat)
summary(dat)
```

```R:head(dat)
  education age parity induced case spontaneous stratum pooled.stratum
1    0-5yrs  26      6       1    1           2       1              3
2    0-5yrs  42      1       1    1           0       2              1
3    0-5yrs  39      6       2    1           0       3              4
4    0-5yrs  34      4       2    1           0       4              2
5   6-11yrs  35      3       1    1           1       5             32
6   6-11yrs  36      4       2    1           1       6             36
```

```R:summary(dat)
  education        age            parity         induced            case       
 0-5yrs : 12   Min.   :21.00   Min.   :1.000   Min.   :0.0000   Min.   :0.0000  
 6-11yrs:120   1st Qu.:28.00   1st Qu.:1.000   1st Qu.:0.0000   1st Qu.:0.0000  
 12+ yrs:116   Median :31.00   Median :2.000   Median :0.0000   Median :0.0000  
               Mean   :31.50   Mean   :2.093   Mean   :0.5726   Mean   :0.3347  
               3rd Qu.:35.25   3rd Qu.:3.000   3rd Qu.:1.0000   3rd Qu.:1.0000  
               Max.   :44.00   Max.   :6.000   Max.   :2.0000   Max.   :1.0000  
  spontaneous        stratum      pooled.stratum 
 Min.   :0.0000   Min.   : 1.00   Min.   : 1.00  
 1st Qu.:0.0000   1st Qu.:21.00   1st Qu.:19.00  
 Median :0.0000   Median :42.00   Median :36.00  
 Mean   :0.5766   Mean   :41.87   Mean   :33.58  
 3rd Qu.:1.0000   3rd Qu.:62.25   3rd Qu.:48.25  
 Max.   :2.0000   Max.   :83.00   Max.   :63.00 
```

## 行、列の抽出
### 条件による行の抽出
* `[` -> `filter`

baseでは`[`演算子、tidyverseでは`filter`関数を使います。いずれも、条件を演算子ないし関数の中に記述します。

例として、`parity`（出産回数）が3以上である行（サブセット）を取り出してみましょう。

```R:base
high_parity_base <- dat[dat$parity >= 3, ]
```

```R:tidyverse
high_parity_tidy <- dat %>% filter(parity >= 3)
```

複数条件でも同様です。先程の出産回数の条件に、誘発流産`induced`の経験がないという条件を加えます。

```R:base
high_parity_not_induced_base <- dat[dat$parity >= 3 & dat$induced == 0, ]
```

```R:tidyverse
high_parity_not_induced_tidy <- dat %>% filter(parity >= 3 & induced == 0)
```

### 名前による列の選択
* `[`, `$` -> `select`

baseでは`[`演算子または`$`演算子、tidyverseでは`select`関数を使います。いずれも取り出したい列の名前を指定します。

例として、教育レベル`education`を取り出してみましょう。

```R:base
education_base_bracket <- dat['education']
education_base_doller <- dat$education
```

`[`演算子は列をデータフレーム（のサブセット）として取り出しますが、`$`演算子はベクトルとして取り出します[^4]。`select`関数は`[`演算子と同じくデータフレームとして取り出します。

```R:tidyverse
education_tidy <- dat %>% select(education)
```

ベクトルにしたい場合は、`select()`の後に`unlist()`を適用します。

```R:tidyverse
education_tidy_vector <- dat %>% select(education) %>% unlist()
```

複数の場合も同様です。先程の`education`に加えて自然流産の回数`spontaneous`も取り出します。

```R:base
education_spontaneous_base <- dat[, c('education', 'spontaneous')]
```

```R:tidyverse
education_spontaneous_tidy <- dat %>% select(education, spontaneous)
```

### 位置による列の選択
* `[` -> `select`

baseでは`[`演算子、tidyverseでは`select`関数を使います。いずれも、名前で選ぶ場合と同じ要領で、列番号を指定します。

例として、1列目と3列目を取り出してみましょう。

```R:base
cols_1_3_base <- dat[, c(1, 3)]
```

```R:tidyverse
cols_1_3_tidy <- dat %>% select(1, 3)
```

## 加工
### 列の追加
* `[`, `$` -> `mutate`

baseでは`[`演算子または`$`演算子、tidyverseでは`mutate`関数を使います。いずれも追加したい列の名前とその値を与えます。

例として、年齢`age`が中央値より高いかどうかを表す列`age_high`を追加してみましょう（ここでは区別のために接尾辞を付けています）。

```R:base
dat$age_high_base_1 <- dat$age > median(dat$age)
dat['age_high_base_2'] <- dat$age > median(dat$age)
```

```R:tidyverse
dat <- dat %>% mutate(age_high_tidy = age > median(age))
```

`mutate`関数は複数の列を一度に追加することができます。

```R:tidyverse
dat <- dat %>% mutate(dummy_tidy_1 = 1,
                      dummy_tidy_2 = 'あ')
```

### 列の削除
* `NULL`の代入 -> `select`

baseでは列に`NULL`を代入することでその列をデータフレームから削除できます[^5]。tidyverseでは`select`関数を使い、削除したい列名に`-`を付けます。

例として、先ほど追加した列を削除してみましょう[^6]。

```R:base
dat$age_high_base_1 <- NULL
dat['age_high_base_2'] <- NULL
```

```R:tidyverse
dat <- dat %>% select(-age_high_tidy)
```

複数の列をまとめて削除する場合は、`c()`を用います。

```R:base
dat[c('age_high_base_1', 'age_high_base_2')] <- NULL
```

```R:tidyverse
dat <- dat %>% select(-c(dummy_tidy_1, dummy_tidy_2))
```

### 縦横変換
* `reshape2::melt`, `data.table::melt` -> `pivot_longer`
* `reshape2::dcast`, `data.table::dcast` -> `pivot_wider`

Rでデータフレームの縦横変換をする場合、reshape2の`melt`と`dcast`もしくはdata.tableの`melt`と`dcast`が使われることが多いと思います。`tidyverse`では`pivot_longer`と`pivot_wider`を使います[^7]。

reshape2もtidyverseもbaseそのものではありませんが、tidyverse以外で標準的に使用されているライブラリです。ここではreshape2を例としますが、data.tableでも同様です。

準備として、ID列を追加します（相当する列がないため）。

```R:tidyverse
dat <- dat %>% rowid_to_column("id")
```

まず、`id`, `stratum`, `pooled.stratum`, `case`をキーとし、`age`, `parity`, `induced`, `spontaneous`を値として、縦持ちに変換してみましょう。

```R:reshape2
dat_melt_reshape2 <- melt(dat, id.vars = c('id', 'stratum', 'pooled.stratum', 'case'),
                          measure.vars = c('age', 'parity',
                                           'induced', 'spontaneous'))
```

`pivot_longer`は与えられたデータフレーム全体を処理するので、予め必要な列を`select`で選んでおいて、キーとなる列を`-c()`で指定します。

```R:tidyverse
dat_melt_tidy <- dat %>%
  select(c(id, stratum, pooled.stratum, case, age, parity, induced, spontaneous)) %>%
  pivot_longer(-c(id, stratum, pooled.stratum, case))
```

次に、先ほど縦持ちに変換したデータフレームを横持ちに変換してみましょう。変換に含めなかった`education`列を除けば、元の形に戻ります。

```R:reshape2
dat_dcast_reshape2 <- dcast(dat_melt_reshape2,
                            id + stratum + pooled.stratum + case ~ variable,
                            value.var = 'value')
```

```R:tidyverse
dat_dcast_tidy <- dat_melt_tidy %>%
  pivot_wider(id_cols = c(id, stratum, pooled.stratum, case),
              names_from = name, values_from = value)
```

## 集約
### 単純集計

単純集計についてはbaseで十分だと思います。tidyverseにおいて集約をする関数はその名も`summarise`[^8]です。

例として、年齢`age`の平均値を計算してみましょう。

```R:base
avg_age_base <- mean(dat$age)
```

```R:tidyverse
avg_age_tidy <- dat %>% summarise(avg_age = mean(age))
```

ここで、`avg_age_base`はベクトルですが、`avg_age_tidy`はデータフレームです[^9]。

### Group by
* `aggregate` -> `group_by` + `summarise`

baseでは`aggregate`を使いますが、tidyverseでは`group_by`と`summarise`を組み合わせます。

例として、教育レベル`education`ごとの年齢の平均値を計算してみましょう。

```R:base
avg_age_by_education_base <- aggregate(age ~ education, dat, mean)
```

```R:tidyverse
avg_age_by_education_tidy <- dat %>%
  group_by(education) %>% summarise(avg_age = mean(age))
```

複数の集約関数を適用することはbaseの`aggregate`ではできません（ここでは割愛しますが、data.tableの`aggregate`を使います）。`summarise`では以下のようにします（例として平均値と中央値を算出）。

```R:tidyverse
avg_med_age_by_education_tidy <- dat %>%
  group_by(education) %>% summarise(avg_age = mean(age), median_age = median(age))
```

## まとめ

baseとtidyverseの比較を簡単にまとめました。より実務に即したケースや抜け漏れがあれば今後追加していく予定です。

Enjoy!

[^1]: 夏休みの宿題とします。
[^2]: 冬休みの宿題とします。
[^3]: 詳細は`?infert`を実行してヘルプを参照。
[^4]: ただし、1つの列に限ります。
[^5]: または、残したい列のみを指定して再帰代入します。
[^6]: 存在しない列に`NULL`を代入してもエラーは発生しませんが、`select`で存在しない列を指定するとエラーが発生します。
[^7]: tidyr 1.0.0以上
[^8]: `summarize`もエイリアスです。
[^9]: tidyverseを使ってベクトルで値を返すには次のようにします（が大袈裟です）。`avg_age_tidy_vector <- dat %>% select(age) %>% unlist() %>% mean()`