Rで扱うことの多いデータフレームですが、みなさんはどのように処理をしているでしょうか?
私は既存の{base}パッケージで処理を行なっていましたが、{dplyr}パッケージを知ってからというもの手放せなくなりました。
この{dplyr}を使うと驚くほど簡単に、そして高速にデータフレーム処理を行うことができます。
今回は既存の{base}パッケージと比較しながら、その実力をまとめてみます。
最後まで読んだあかつきには、あなたもきっと虜になっているはず。
辞書代わりにもどうぞ!
- count
- rename
- select,slice
- mutate
- filter
- arrange
- summarise
- goup_by
- pivot_wider、pivot_longer
- エクササイズ
- まとめ
ちなみにパイプを使った記述で真価を発揮するため、慣れていない方はこちらをご参考ください。
今回扱うデータは男女の身長、体重、仕事の分野(a,b,c)を集計した100人の擬似健康診断検診データです。
下記のコードをコピペして実行すればkensinデータフレームが出来上がります。
>#男性データの作成 men <- data_frame( height = round(rnorm(60,170,10)), weight = round(rnorm(60,60,10)), jobs = sample(c("a","b","c"),60,replace = T), sex = rep("m",60) ) #女性データの作成 women <- data_frame( height = round(rnorm(40,150,10)), weight = round(rnorm(40,40,10)), jobs = sample(c("a","b","c"),40,replace = T), sex = rep("w",40) ) #マージして行をシャッフルする kensin <- bind_rows(men,women) kensin <- kensin[sample(1:100,100),] # A tibble: 100 x 4 height weight jobs sex <dbl> <dbl> <chr> <chr> 1 169 57 c m 2 162 48 b m 3 170 69 a m 4 175 59 a m 5 142 51 c w 6 186 71 a m 7 166 70 b m 8 172 61 a m 9 151 41 a w 10 191 55 b m # ... with 90 more rows
{dplyr}で作成したデータフレームは、より使いやすく進化させたtibble型データフレームとなっていますが、{dplyr}の場合は扱い方に大きな違いはないので気にしないでください。
従来のデータフレーム型がいい!って方はdata_frame()をdata.frame()に書き換えればOKです。
それでは{dplyr}の実力を体験していきましょう。
count
まずは、データの概略を掴むためにクロス集計表を作成してみましょう。ここでは、要素の個数を数えるcountを紹介します。これは簡単で、指定した要因の要素数を数えます。
count(data, 数えたい要素の列名)
> #職業ごとの人数 > kensin %>% count(jobs) # A tibble: 3 x 2 jobs n <chr> <int> 1 a 37 2 b 32 3 c 31 >#性別ごとの人数 > kensin %>% count(sex) # A tibble: 2 x 2 sex n <chr> <int> 1 m 60 2 w 40 > #性別、職業ごとの人数 > kensin %>% count(jobs,sex) # A tibble: 6 x 3 jobs sex n <chr> <chr> <int> 1 a m 19 2 a w 18 3 b m 20 4 b w 12 5 c m 21 6 c w 10
男性60人、女性40人、職業はだいたい均等に30人ぐらいずつのデータセットになっていますね。
ここからクロス集計表を作るには、{tidyr}パッケージのspread関数を使います。{tidyr}パッケージもデータフレームの加工に使えるので、またどこかでご紹介します。
{base}ではtable関数を使います。
>#クロス集計 > kensin %>% count(jobs,sex) %>% #tidyrのspread関数を使う spread(key=sex,value=n) # A tibble: 3 x 3 jobs m w <chr> <int> <int> 1 a 19 18 2 b 20 12 3 c 21 10 >#{base}のtableを使って > table(kensin$jobs,kensin$sex) m w a 19 18 b 20 12 c 21 10
tableのが簡単そうに見えますが、パイプが使えないのが難点です。
rename
次に列名を変更するrename関数です。height,weightの最初一文字を大文字にしてみます。
rename(data,変更後の列名= 変更したい列)
> kensin %>% rename(Height=height,Weight = weight) %>% head() # A tibble: 6 x 4 Height Weight jobs sex <dbl> <dbl> <chr> <chr> 1 177 86 a m 2 153 43 a w 3 164 61 b m 4 169 35 b w 5 167 53 a m 6 158 39 b w
列名を変更することができました。ちなみにrename(Height=1,Weight=2)など列番号で指定することもできます。
ここで注意するのは、関数を実行した時点では列名は変化していないということです。処理後の結果を反映させるには、代入演算子を使って以下のようにします。
#実行した時点では処理は反映されない > print(colnames(kensin)) [1] "height" "weight" "jobs" "sex" >#結果を代入 > kensin %>% + rename(Height=1,Weight=2)->kensin #反映されている > print(colnames(kensin)) [1] "Height" "Weight" "jobs" "sex"
これはパイプを使った書き方全てにおいて共通していて、実行した時点では処理が反映されないことに気をつけましょう。
通常の方法では、colnamesを使って以下のようにします。
> colnames(kensin) <- c("Height","Weight",colnames(kensin[,3:ncol(kensin)])) > colnames(kensin) [1] "Height" "Weight" "jobs" "sex"
全ての列名を指定しなければ変更できないので、めんどーです。
select,slice
指定した列名もしくは列番号でデータを取り出したい時に使うのがselect関数、行番号で指定するのがslice関数です。例えば性別と仕事の列を取り出して、最初の5行を取り出してみます。
select(data,列名),slice(data,行番号)
> kensin %>% select(jobs,sex) %>% #列番号でもOK select(3,4) slice(1:5) # A tibble: 6 x 2 jobs sex <chr> <chr> 1 c m 2 b m 3 a m 4 a m 5 c w 6 a m
これを通常の方法で行うと
> kensin[c(1:5),c("jobs","sex")] # A tibble: 6 x 2 jobs sex <chr> <chr> 1 c m 2 b m 3 a m 4 a m 5 c w 6 a m
となりますね。selectのすごいところは列名と列番号が混じっていても使えるところにあります。
>#{baes}は列名か列番号で統一しないとエラーが出る > kensin[c(1:5),c("jobs",4)] エラー: Column `4` not found Call `rlang::last_error()` to see a backtrace #列名と列番号が混じっていても使える > kensin %>% select(c(jobs,4)) %>% slice(1:5) # A tibble: 5 x 2 jobs sex <chr> <chr> 1 c m 2 b m 3 a m 4 a m 5 c w
列名をダブルクオートで囲わなくても認識してくれるのも楽ですね。 また、今回のように4列のデータであれば問題ありませんが、数十列、数百列となった場合にある条件にマッチした列名だけ取り出したくなることもあります。
そんな時は以下のようにします。
>#指定した文字で始まる列名を取り出すstarts_with() > kensin %>% select(starts_with("H")) %>% slice(1) # A tibble: 1 x 1 Height <dbl> 1 169 >#指定した文字で終わる列名を取り出すends_with() > kensin %>% select(ends_with("x"))%>% slice(1) # A tibble: 1 x 1 sex <chr> 1 m >#指定した文字を含む列名を取り出すcontains() > kensin %>% select(contains("t"))%>% slice(1) # A tibble: 1 x 2 Height Weight <dbl> <dbl> 1 169 57
他にも正規表現が使えるmatches()などもあります。
これを通常の方法で行おうとすると、grepや正規表現を駆使する高度な方法しか私は思いつきません(;_;)
mutate
mutateはselectと違って、新たな列を追加で作成することができます。
既存の列の値を使うこともできるので、身長と体重を使ってBMI(体重/身長mの二乗)という列と、個人を表すID列を作成してみます。
mutate(新しい列名=処理の式)
> kensin %>% mutate(BMI=Weight/(Height/100)^2,ID=seq(1:100)) ->kensin > kensin %>% head() # A tibble: 6 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 169 57 c m 20.0 1 2 162 48 b m 18.3 2 3 170 69 a m 23.9 3 4 175 59 a m 19.3 4 5 142 51 c w 25.3 5 6 186 71 a m 20.5 6 >
しっかり追加されていますね。今回の結果は代入演算子で反映させておきます。
{base}パッケージのみで実行するとtransformを使って以下のようになります。
> transform(kensin,BMI=kensin$Weight/(kensin$Height/100)^2,ID=seq(1:100)) %>% + head() Height Weight jobs sex BMI ID 1 169 57 c m 19.95728 1 2 162 48 b m 18.28989 2 3 170 69 a m 23.87543 3 4 175 59 a m 19.26531 4 5 142 51 c w 25.29260 5 6 186 71 a m 20.52260 6
少し読みにくいですね。mutateを使った方がスッキリ書けそうです。
条件分岐(if_else,recorde,case_when)
(2月7日追記)
このmutateを使って、分岐させた変数を作成することもできます。例えば、BMIが25以上の人を1、それ以外の人を0とした変数を作成したい場合はif_elseを使って
kensin %>% mutate(class=if_else(BMI>=25,1,0)) A tibble: 100 x 7 Height Weight jobs sex BMI ID class <dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> 1 159 65 b m 25.7 1 1 2 163 40 a w 15.1 2 0 3 143 51 c w 24.9 3 0 4 160 32 c w 12.5 4 0 5 185 63 a m 18.4 5 0 6 162 63 b m 24.0 6 0 7 165 63 b m 23.1 7 0 8 170 68 a m 23.5 8 0 9 169 59 b m 20.7 9 0 10 171 62 a m 21.2 10 0
とします。{base}パッケージにもifelse()がありますが、こちらの方が厳しく返り値を判断しているらしく、真と偽で返り値の型が違った場合はエラーになります。(例えば、真の場合に文字、偽の場合に数字など)
複数の条件で分岐させたいときは、if_elseを入れ子にするかcase_whenを使います。BMIが25以上の人を"over",18.5未満の人を"under"、それ以外を"normal"としたclassを作ってみます。
#if_elseの入れ子 kensin %>% mutate(class=if_else(BMI>=25,"over", if_else(BMI<18.5,"under","normal"))) #case_when kensin %>% mutate(class=case_when( .$BMI >= 25 ~ "over", .$BMI <18.5 ~ "under", TRUE~"normal" )) # A tibble: 100 x 7 Height Weight jobs sex BMI ID class <dbl> <dbl> <chr> <chr> <dbl> <int> <chr> 1 159 65 b m 25.7 1 over 2 163 40 a w 15.1 2 under 3 143 51 c w 24.9 3 normal 4 160 32 c w 12.5 4 under 5 185 63 a m 18.4 5 under 6 162 63 b m 24.0 6 normal 7 165 63 b m 23.1 7 normal 8 170 68 a m 23.5 8 normal 9 169 59 b m 20.7 9 normal 10 171 62 a m 21.2 10 normal
どちらも結果は同じです。case_whenは記述が少し変わっていて、条件返り値とします。また、引数はベクトルで渡す必要があるので、.$として変数にアクセスしています。TRUEはそれ以外という意味です。
特定の値を他の値に変換したい場合はrecodeを使います。jobsのaを1、bを2、cを3とする変数を作ってみます。
kensin %>% mutate(jobclass = recode(jobs,"a"=1,"b"=2,"c"=3)) # A tibble: 100 x 7 Height Weight jobs sex BMI ID jobclass <dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> 1 159 65 b m 25.7 1 2 2 163 40 a w 15.1 2 1 3 143 51 c w 24.9 3 3 4 160 32 c w 12.5 4 3 5 185 63 a m 18.4 5 1 6 162 63 b m 24.0 6 2 7 165 63 b m 23.1 7 2 8 170 68 a m 23.5 8 1 9 169 59 b m 20.7 9 2 10 171 62 a m 21.2 10
.defaultや.missingという引数を指定すると、指定しなかった値や欠損値の場合にも対応できます。また、recode_factorを使うとfactor型で変数を作成することもできます。
これら3つはどれも同じようなことができるので、使いやすい方法を選ぶと良いかもしれませんね!
(2月7日追記終わり)
filter
このfilter関数は名前の示す通り、条件に合致する行を抜き出してくれる関数になります。{base}パッケージではsubset関数と同じ役割です。
論理演算子も使えるので、男性かつ肥満傾向(BMIが25以上)の人を抜き出すこともできます。
filter(data,条件式)
> #男性かつBMI25以上の人を抜き出す > kensin %>% + filter(sex=="m" & BMI > 25) %>% + head() # A tibble: 6 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 166 70 b m 25.4 7 2 155 70 a m 29.1 12 3 151 62 a m 27.2 27 4 166 77 a m 27.9 52 5 148 62 b m 28.3 58 6 177 80 a m 25.5 60
もう少し複雑な条件を指定することもできて、職業がbまたはcで、男性かつ肥満傾向の人を抜き出す場合は以下のように書きます。
> kensin %>% + filter((jobs == "b" | jobs == "c") & sex=="m" & BMI > 25) %>% + head() # A tibble: 5 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 166 70 b m 25.4 7 2 148 62 b m 28.3 58 3 154 62 b m 26.1 65 4 169 84 c m 29.4 83 5 166 78 c m 28.3 97
{base}のsubset関数も全く同じように記述されます。
>#subsetを使う > kensin %>% + subset((jobs == "b" | jobs == "c") & sex=="m" & BMI > 25) %>% + head() # A tibble: 5 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 166 70 b m 25.4 7 2 148 62 b m 28.3 58 3 154 62 b m 26.1 65 4 169 84 c m 29.4 83 5 166 78 c m 28.3 97
見たところsubsetとfilterの違いはないように見えますが、処理できるデータフレームのサイズと処理速度はfilterの方が優秀みたいです。
もし詳しい方がいらっしゃいましたらご教授ください。
arrange
arrangeは指定した列を降順、もしくは昇順に並び替えます。試しに身長で並び替えてみましょう。
arrange(data,並び替えたい列名)
> kensin %>% + arrange(Height) %>% + head() # A tibble: 6 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 128 41 b w 25.0 15 2 133 27 a w 15.3 53 3 133 26 a w 14.7 65 4 136 28 b w 15.1 7 5 139 42 b w 21.7 16 6 139 29 a w 15.0 32
身長が低い人順に並びました。降順にしたい場合はdesc(descendingの意味)をつけると降順に並びます。また複数条件を指定することもできるので、身長で降順に並び替えた後、体重で昇順にしたい場合は、
> kensin %>% + arrange(desc(Height),Weight) %>% + head() # A tibble: 6 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 194 62 a m 16.5 92 2 191 64 c m 17.5 24 3 184 68 a m 20.1 30 4 183 67 a m 20.0 62 5 183 75 c m 22.4 40 6 182 86 a m 26.0 100
と書きます。
{base}では、データフレームの行指定にorder関数を使って以下のようにします。
>#order関数は行番号が返る > order(desc(kensin$Height),kensin$Weight)[1:10] [1] 92 24 30 62 40 100 34 67 35 10 > kensin[order(desc(kensin$Height),kensin$Weight),] # A tibble: 100 x 6 Height Weight jobs sex BMI ID <dbl> <dbl> <chr> <chr> <dbl> <int> 1 194 62 a m 16.5 92 2 191 64 c m 17.5 24 3 184 68 a m 20.1 30 4 183 67 a m 20.0 62 5 183 75 c m 22.4 40 6 182 86 a m 26.0 100 7 181 60 b m 18.3 34 8 180 65 c m 20.1 67 9 179 39 c m 12.2 35 10 179 61 c m 19.0 10 # ... with 90 more rows
データフレームがネストしているように見えてとても読みにくいですね。 arrangeを使ってスマートにいきましょう。
summarise
ここまで読んで{base}だけでもできそうだし、やっぱいいやーと思っているそこのあなた!
ここからが{dplyr}の真の実力です。
summariseは任意の列に対して複数の関数を適応できます。
summarise(data,処理関数(列名))
例えば、身長と体重の最大値、最小値、平均値を計算したい場合は、
> kensin %>% + summarise(MeanH=mean(Height),MinH=min(Height),MaxH=max(Height), + MeanW=mean(Weight),MinW=min(Weight),MaxW=max(Weight)) # A tibble: 1 x 6 MeanH MinH MaxH MeanW MinW MaxW <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 162. 128 194 52.5 14 86
とします。これを{base}で実装しようとするとapplyを使って中で関数を定義するしか方法がないので、ややこしくなります。
> kensin[,c(1,2)] %>% + apply(2,function(x){ + return(c(Mean=mean(x),Max=max(x),Min=min(x))) + }) Height Weight Mean 161.69 52.48 Max 194.00 86.00 Min 128.00 14.00
また、複数列に対して処理を行う場合はsummarise_atを使うとより簡単に書くことができます。(以前はsummarise_eachが使われいましたが、もう古いらしい...)
summarise_at(vars(列名),funs(関数))
> kensin %>% + summarise_at(vars(Height,Weight),funs(mean,min,max)) # A tibble: 1 x 6 Height_mean Weight_mean Height_min Weight_min Height_max Weight_max <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 162. 52.5 128 14 194 86
goup_by
さて、ついにdplyrの大御所{goup_by}の紹介です。このgroup_byは、上記のsummarizeと一緒に使うことが多いです。例えば男女別の身長と体重の平均値を知りたいととします。すると通常は、tapplyを使って
> tapply(kensin$Height,kensin$sex,mean) m w 169.9167 149.3500 > tapply(kensin$Weight,kensin$sex,mean) m w 61.41667 39.07500
tapplyは一列ずつしか処理が出来ないため、今回のように複数列に渡って処理したい場合は、なんども実行する必要があります。
次はgroup_byとsummariseを使って処理をしてみましょう。
> kensin %>% + group_by(sex) %>% + summarise(mean(Height),mean(Weight)) # A tibble: 2 x 3 sex `mean(Height)` `mean(Weight)` <chr> <dbl> <dbl> 1 m 170. 61.4 2 w 149. 39.1 >#summarise_atを使った書き方 > kensin %>% + group_by(sex) %>% + summarise_at(vars(Height,Weight),mean)
このようにgroup_byで性別によってグルーピングした後、summariseを使うと一発で集計できます。また、性別、職業の二要因でグルーピングすることもできて、
> kensin %>% + group_by(sex,jobs) %>% + summarise(mean(Height),mean(Weight)) # A tibble: 6 x 4 # Groups: sex [?] sex jobs `mean(Height)` `mean(Weight)` <chr> <chr> <dbl> <dbl> 1 m a 173. 62.5 2 m b 168. 63.3 3 m c 169. 58.7 4 w a 148 37.9 5 w b 148. 36.9 6 w c 153. 43.7
はい!これも一発ですね。とても簡単です。
また、mutateを組み合わせると性別ごとの平均身長を列に加えたりもできます。
> kensin %>% + group_by(sex) %>% + mutate(MeanH = mean(Height)) %>% + head() # A tibble: 6 x 7 # Groups: sex [2] Height Weight jobs sex BMI ID MeanH <dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> 1 157 71 b m 28.8 1 170. 2 143 32 a w 15.6 2 149. 3 160 52 c m 20.3 3 170. 4 149 33 a w 14.9 4 149. 5 167 53 a m 19.0 5 170. 6 156 57 a w 23.4 6 149.
group_byを使いこなせると色々できますね!
pivot_wider、pivot_longer
(2021/3/20追記)
エクササイズ
さて、それでは{dplyr}を一連の流れで使ってみましょう。課題は以下の通りです。 データセットは、一番最初の初期状態にしておいてください。
- renameを使って、sexをSexに、jobsをJobsに変換する。
- mutateを使って、BMI列を追加する。
- selectでBMI、Sex、Jobsを取り出す。
- sliceで上から80人取り出す。
- filterで職業がaまたはbで、BMIが25以上の人を選ぶ
- arrangeでBMI降順に並び替える
- 結果を変数obesityに代入
- obesityをgroup_byで職業ごとにグルーピングする。
- summariseで職業ごとのBMIの平均値を集計する。
まずは通常の方法で処理します。
> colnames(kensin) <- c(colnames(kensin)[1:2],"Jobs","Sex") > kensin <- transform(kensin,BMI=kensin$weight/(kensin$height/100)^2) > kensin[1:80,c("BMI","Jobs","Sex")] %>% + subset((Jobs=="a"|Jobs=="b") & BMI >= 25 ) -> obesity >obesity[order(desc(obesity$BMI)),]->obesity > > tapply(obesity$BMI,obesity$Jobs,mean) a b 26.68885 27.06386
途中でなんども代入が必要になったり、変数と関数がごちゃごちゃしていたり、とても読みにくいです。
{dplyr}を使った処理が以下になります。
> #エクササイズ > kensin %>% + rename(Jobs=jobs,Sex = sex) %>% + mutate(BMI=weight/(height/100)^2) %>% + select(BMI,Jobs,Sex) %>% + slice(1:80) %>% + filter((Jobs=="a"|Jobs=="b") & BMI >= 25 ) %>% + arrange(desc(BMI))-> obesity > obesity # A tibble: 7 x 3 BMI Jobs Sex <dbl> <chr> <chr> 1 29.6 b m 2 28.8 b m 3 28.1 a w 4 27.5 a m 5 26.2 b m 6 26.0 a m 7 25.7 b m > > obesity %>% + group_by(Jobs) %>% + summarise(mean(BMI)) # A tibble: 2 x 2 Jobs `mean(BMI)` <chr> <dbl> 1 a 26.7 2 b 27.1
どのタイミングでどんな処理をしているのかすぐにわかりますね。また、パイプで繋げることで、一連の流れで処理できるのでとてもスマートです!
まとめ
今回は{dplyr}を使ったスマートなデータ処理を紹介しました。{base}と比較することで、その実力が実感できたかと思います。
他にもデータフレームのマージなど様々な機能があるので、ぜひ活用してみましょう!
※本記事は筆者が個人的に学んだことをまとめた記事なります。数学の記法や詳細な理論、筆者の勘違い等で誤りがあった際はご指摘頂けると幸いです。
参考
https://www.rstudio.com/wp-content/uploads/2015/09/data-wrangling-japanese.pdf pdf注意
データの解析手法そのものよりも、いかに効率よくデータを解析するかが書かれています。Rに慣れ始めた頃に読むのがおすすめです!
- 作者:Hadley Wickham,Garrett Grolemund
- 発売日: 2017/10/25
- メディア: 単行本(ソフトカバー)