医療職からデータサイエンティストへ

統計学、機械学習に関する記事をまとめています。

もっと早く知りたかった高速データフレーム処理~dplyrの使い方~

Rで扱うことの多いデータフレームですが、みなさんはどのように処理をしているでしょうか?

私は既存の{base}パッケージで処理を行なっていましたが、{dplyr}パッケージを知ってからというもの手放せなくなりました。

この{dplyr}を使うと驚くほど簡単に、そして高速にデータフレーム処理を行うことができます。

今回は既存の{base}パッケージと比較しながら、その実力をまとめてみます。

最後まで読んだあかつきには、あなたもきっと虜になっているはず。

辞書代わりにもどうぞ!

ちなみにパイプを使った記述で真価を発揮するため、慣れていない方はこちらをご参考ください。

medi-data.hatenablog.com

今回扱うデータは男女の身長、体重、仕事の分野(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や正規表現を駆使する高度な方法しか私は思いつきません(;_;)

www.medi-08-data-06.work

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は記述が少し変わっていて、条件\sim返り値とします。また、引数はベクトルで渡す必要があるので、.$として変数にアクセスしています。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を使いこなせると色々できますね!

エクササイズ

さて、それでは{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注意

dplyrを使いこなす!基礎編 - Qiita

データの解析手法そのものよりも、いかに効率よくデータを解析するかが書かれています。Rに慣れ始めた頃に読むのがおすすめです!

Rではじめるデータサイエンス

Rではじめるデータサイエンス