R言語でデータ解析をする際の前処理が苦手な方へ
Rで前処理が苦手な方へ
はじめに
R言語は統計解析を行うためのツールで、専門家によるパッケージ開発が盛んなこともありデータ解析にはもってこいのプログラミング言語です。
10年前に比べるとRに関する書籍も多く出版されており、パッケージの使い方を紹介する内容もよく見られます。
データ処理で使われる tidyverse はとても便利ですよね。 shell 環境と同じようなパイプ演算子 %>%
が使えることも気に入っています。
それでもなお、実際に使っている人からはRでの解析が難しい、特に、データの扱いが難しいという声をよく聞きます。
上記のような便利なパッケージを紹介しても大きな改善には至っていないようです。
その根本的な原因は、プログラミング言語としてのRをよく知らないことと、内部でどのような処理がなされているか分かっていないことではないかと思います。
筆者もこれまでRを使ってきましたが、スキル不足という大きな壁に何度もぶち当たってきました。筆者はプログラミングについてカンがいいほうではなく、だいたい数撃ちゃ当たる方式でトライしてきました。そのとき気づいたのが、既にある便利なものを使うより、自分でイチから作ったほうが理解が深まり、どんな問題でも何となく解決のための方法の予想ができるようになってきました。今でも才能ではなく経験だけでプログラム書いています。
こんな感じなので、同じように感じている方に向けて届くような情報を公開していきたいと思います。
もう少し絞って説明すると、例えば、 dplyr のようなパッケージを使って一時はうまく行っても、他のデータ処理を行うときに詰まってしまうような方、内部処理を理解した上で、自ら関数を組み上げてツールを作ってしまうほうが理解が早い方には参考になる情報だと思います。
list
型データについて
Rで多用される list型って、結局何なんでしょうね?
Rを使い始めた当初、筆者の中で list型 は厄介者でした。
それまでExcelの表計算しか使ったことがなかった筆者は、配列構造はすぐに理解できましたが、listは使いどころがない無用の長物でした。
しかし、Rでは多くのベース関数の引数でlist型が指定されていたり、関数の戻り値もlist型だったりして、list型から data.frame などに変換する手間がめんどうでした。
そこからlist型の有用な使い方が分かってくると、積極的に使っていくようになりました。
data.frame型はlist型の派生であり、ほぼ一緒 ということは非常に驚きました。
何でこの話を最初にするかと言うと、listを理解すればRでのプログラミングが楽しくなるからです。
list型の構造について
list型データの特徴を簡単に挙げると以下の通りです。
- listに格納するデータはどんなデータ型でもOK
- list型の中ににlist型データを格納することができる(入れ子構造もOK)
- 格納するデータには名前を付けることができる
(d <- list(x = c(1:5),
y = letters[1:15],
z = array(1:20, dim = c(5,2,2)),
l = list(1:5, 10:100)))
$x
[1] 1 2 3 4 5
$y
[1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o"
$z
, , 1
[,1] [,2]
[1,] 1 6
[2,] 2 7
[3,] 3 8
[4,] 4 9
[5,] 5 10
, , 2
[,1] [,2]
[1,] 11 16
[2,] 12 17
[3,] 13 18
[4,] 14 19
[5,] 15 20
$l
$l[[1]]
[1] 1 2 3 4 5
$l[[2]]
[1] 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
[20] 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
[39] 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
[58] 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
[77] 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
データ数も、データ型も異なるオブジェクトがlist型に格納されました。これだけでもすごいですね。
list型空のデータの抜き出しは以下のとおりです。
d$x
[1] 1 2 3 4 5
d[["x"]]
[1] 1 2 3 4 5
d["x"]
$x
[1] 1 2 3 4 5
細かい違いはありますが、概ねデータフレームと同じですね。
上記のように d$x
とするとベクトルが反ってきます。 c()
や rbind()
等で結合してベクトルや配列にすることもできます。
また、 unlist()
すると、list内のオブジェクトが全てベクトルになって反ってきます。
rbind(d[1:2])
x y
[1,] integer,5 character,15
unlist(d)
x1 x2 x3 x4 x5 y1 y2 y3 y4 y5 y6 y7 y8
"1" "2" "3" "4" "5" "a" "b" "c" "d" "e" "f" "g" "h"
y9 y10 y11 y12 y13 y14 y15 z1 z2 z3 z4 z5 z6
"i" "j" "k" "l" "m" "n" "o" "1" "2" "3" "4" "5" "6"
z7 z8 z9 z10 z11 z12 z13 z14 z15 z16 z17 z18 z19
"7" "8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19"
z20 l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12
"20" "1" "2" "3" "4" "5" "10" "11" "12" "13" "14" "15" "16"
l13 l14 l15 l16 l17 l18 l19 l20 l21 l22 l23 l24 l25
"17" "18" "19" "20" "21" "22" "23" "24" "25" "26" "27" "28" "29"
l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38
"30" "31" "32" "33" "34" "35" "36" "37" "38" "39" "40" "41" "42"
l39 l40 l41 l42 l43 l44 l45 l46 l47 l48 l49 l50 l51
"43" "44" "45" "46" "47" "48" "49" "50" "51" "52" "53" "54" "55"
l52 l53 l54 l55 l56 l57 l58 l59 l60 l61 l62 l63 l64
"56" "57" "58" "59" "60" "61" "62" "63" "64" "65" "66" "67" "68"
l65 l66 l67 l68 l69 l70 l71 l72 l73 l74 l75 l76 l77
"69" "70" "71" "72" "73" "74" "75" "76" "77" "78" "79" "80" "81"
l78 l79 l80 l81 l82 l83 l84 l85 l86 l87 l88 l89 l90
"82" "83" "84" "85" "86" "87" "88" "89" "90" "91" "92" "93" "94"
l91 l92 l93 l94 l95 l96
"95" "96" "97" "98" "99" "100"
次に、データ数が同じlist型データを準備します。
(d2 <- list(x = runif(10, 0, 10),
y = runif(10, 0, 100)))
$x
[1] 5.2967100 2.3751208 9.8763862 0.5272705 9.4968724 5.7194029 7.3730325
[8] 3.2488324 9.2558413 9.7858033
$y
[1] 32.73107 89.52468 50.60004 73.32611 59.21830 79.13014 72.45658 40.02664
[9] 54.72018 18.54085
上述したとおり、data.frameはlistの派生データ型 であるため、次の操作でプロットもできます。
plot(d2)
また、 do.call()
関数を使えば、list型データを一度に処理することができます。
do.call(c, d2)
x1 x2 x3 x4 x5 x6 x7
5.2967100 2.3751208 9.8763862 0.5272705 9.4968724 5.7194029 7.3730325
x8 x9 x10 y1 y2 y3 y4
3.2488324 9.2558413 9.7858033 32.7310655 89.5246827 50.6000413 73.3261087
y5 y6 y7 y8 y9 y10
59.2182959 79.1301421 72.4565848 40.0266358 54.7201792 18.5408549
do.call(rbind, d2)
[,1] [,2] [,3] [,4] [,5] [,6] [,7]
x 5.29671 2.375121 9.876386 0.5272705 9.496872 5.719403 7.373033
y 32.73107 89.524683 50.600041 73.3261087 59.218296 79.130142 72.456585
[,8] [,9] [,10]
x 3.248832 9.255841 9.785803
y 40.026636 54.720179 18.540855
do.call(cbind, d2)
x y
[1,] 5.2967100 32.73107
[2,] 2.3751208 89.52468
[3,] 9.8763862 50.60004
[4,] 0.5272705 73.32611
[5,] 9.4968724 59.21830
[6,] 5.7194029 79.13014
[7,] 7.3730325 72.45658
[8,] 3.2488324 40.02664
[9,] 9.2558413 54.72018
[10,] 9.7858033 18.54085
例えば、 lapply()
で得られた結果を do.call()
で配列に変換すると、見た目で分かりやすくなります。
(ret <- lapply(d2, mean))
$x
[1] 6.295527
$y
[1] 57.02746
do.call(c, ret)
x y
6.295527 57.027459
同じ結果は Map()
関数を使うことでも得られます。
Map(mean, d2)
$x
[1] 6.295527
$y
[1] 57.02746
listが分かると、このあたりのベクトル処理ができる apply
系の関数の使いどころが見えてきます。
サンプルデータを使ったデータ処理の例
Rに標準の airquality データを使って処理をやってみます。
data(airquality)
df <- airquality
str(df)
'data.frame': 153 obs. of 6 variables:
$ Ozone : int 41 36 12 18 NA 28 23 19 8 NA ...
$ Solar.R: int 190 118 149 313 NA NA 299 99 19 194 ...
$ Wind : num 7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
$ Temp : int 67 72 74 62 56 66 65 59 61 69 ...
$ Month : int 5 5 5 5 5 5 5 5 5 5 ...
$ Day : int 1 2 3 4 5 6 7 8 9 10 ...
データを扱いやすいように少し手を加えます。日付型の指定や曜日の設定です。
## 扱いやすいようにデータを成形
df$date <- as.Date(paste(1973, df$Month, df$Day, sep = "-"), tz = "America/Chicago")
wdaylist <- c("日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日")
df$weekday <- factor(weekdays(df$date), levels = wdaylist)
str(df)
'data.frame': 153 obs. of 8 variables:
$ Ozone : int 41 36 12 18 NA 28 23 19 8 NA ...
$ Solar.R: int 190 118 149 313 NA NA 299 99 19 194 ...
$ Wind : num 7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
$ Temp : int 67 72 74 62 56 66 65 59 61 69 ...
$ Month : int 5 5 5 5 5 5 5 5 5 5 ...
$ Day : int 1 2 3 4 5 6 7 8 9 10 ...
$ date : Date, format: "1973-05-01" "1973-05-02" ...
$ weekday: Factor w/ 7 levels "日曜日","月曜日",..: 3 4 5 6 7 1 2 3 4 5 ...
このデータを使って、 lapply と同じくらい使用頻度がある tapply を使って統計量を算出します。
tapply(df$Ozone, df$weekday, mean, na.rm = TRUE)
日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日
44.38889 37.43750 46.47368 44.64706 38.62500 31.60000 50.33333
このように、曜日毎のOzone濃度平均値が得られます。
他にも、日付を指定して月ごと、数日毎の集計も算出することができます。
## 月別に集計
tapply(df$Ozone, df$Month, mean, na.rm = TRUE)
5 6 7 8 9
23.61538 29.44444 59.11538 59.96154 31.44828
## 10日毎に集計
tapply(df$Ozone, round(df$date, -1), mean, na.rm = TRUE)
1973-05-05 1973-05-15 1973-05-25 1973-06-04 1973-06-14 1973-06-24 1973-07-04
23.12500 16.66667 30.25000 33.00000 31.85714 13.00000 73.87500
1973-07-14 1973-07-24 1973-08-03 1973-08-13 1973-08-23 1973-09-02 1973-09-12
39.00000 61.11111 54.22222 55.00000 55.85714 72.72727 25.44444
1973-09-22 1973-10-02
19.40000 17.33333
## 日射量別に集計
tapply(df$Ozone, round(df$Solar.R, -2), mean, na.rm = TRUE)
0 100 200 300
11.60000 26.22727 59.34146 45.12121
もっと使いやすく、列名でまとめる情報を指定できるように関数を定義します。
tapply2 <- function(df, group, var, f) {
lapply(df[, var], function(m) {
tapply(m, list(df[, group]), f, na.rm = TRUE)
})
}
引数には df: オブジェクト名 , group: 集計の基準とするdfの列名 , var: 集計対象とするdfの列名 , f: 関数名 を指定します。
tapply2(df, "weekday", c("Ozone", "Wind", "Solar.R"), mean)
$Ozone
日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日
44.38889 37.43750 46.47368 44.64706 38.62500 31.60000 50.33333
$Wind
日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日
10.031818 8.947619 10.600000 9.868182 10.022727 10.604545 9.581818
$Solar.R
日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日
222.4737 193.5000 191.9091 158.4091 177.5000 174.9524 188.1500
結果がリストで反ってきますので、分かりやすくテーブル形式になるように関数を修正します。
tapply2 <- function(df, group, var, f) {
d <- lapply(df[, var], function(m) {
tapply(m, list(df[, group]), f, na.rm = TRUE)
})
do.call(rbind, d)
}
先ほどと同じように実行すると、以下の結果が反ってきます。
tapply2(df, "weekday", c("Ozone", "Wind", "Solar.R"), mean)
日曜日 月曜日 火曜日 水曜日 木曜日 金曜日
Ozone 44.38889 37.437500 46.47368 44.647059 38.62500 31.60000
Wind 10.03182 8.947619 10.60000 9.868182 10.02273 10.60455
Solar.R 222.47368 193.500000 191.90909 158.409091 177.50000 174.95238
土曜日
Ozone 50.333333
Wind 9.581818
Solar.R 188.150000
このようにするとかなり分かりやすくなりました。
この処理自体は dplyr パッケージを使って以下のように実行したときと同じ結果です。(列名と行名が逆になっていますが出てくる結果は同じです)
library(dplyr)
df %>% select(1:3, 8) %>%
group_by(weekday) %>%
summarise_all(mean, na.rm = TRUE)
# A tibble: 7 × 4
weekday Ozone Solar.R Wind
<fct> <dbl> <dbl> <dbl>
1 日曜日 44.4 222. 10.0
2 月曜日 37.4 194. 8.95
3 火曜日 46.5 192. 10.6
4 水曜日 44.6 158. 9.87
5 木曜日 38.6 178. 10.0
6 金曜日 31.6 175. 10.6
7 土曜日 50.3 188. 9.58
まとめ
このように、 list型 はR言語内で重要なデータ型です。
使いどころが分かると自前で様々な関数を作成することができ、処理の高速化にも繋がります。
実際に、上記の実行時間を計測してみるとその違いも見えてきます。
system.time(df %>% select(1:3, 8) %>%
group_by(weekday) %>%
summarise_all(mean, na.rm = TRUE))
ユーザ システム 経過
0.010 0.000 0.011
system.time(tapply2(df, "weekday", c("Ozone", "Wind", "Solar.R"), mean))
ユーザ システム 経過
0.006 0.000 0.006
使い勝手や拡張性も関係しているので、これらは関数使用の一面でしかありませんが、大規模な処理を行う際は参考になると思います。
データの前処理で行き詰まっている方は、list型のデータ処理について勉強してみてください。きっと役に立つと思います。
Discussion