🐙

R言語でデータ解析をする際の前処理が苦手な方へ

2023/01/29に公開

Rで前処理が苦手な方へ

はじめに

R言語は統計解析を行うためのツールで、専門家によるパッケージ開発が盛んなこともありデータ解析にはもってこいのプログラミング言語です。

10年前に比べるとRに関する書籍も多く出版されており、パッケージの使い方を紹介する内容もよく見られます。

データ処理で使われる tidyverse はとても便利ですよね。 shell 環境と同じようなパイプ演算子 %>% が使えることも気に入っています。

それでもなお、実際に使っている人からはRでの解析が難しい、特に、データの扱いが難しいという声をよく聞きます。

上記のような便利なパッケージを紹介しても大きな改善には至っていないようです。

その根本的な原因は、プログラミング言語としてのRをよく知らないことと、内部でどのような処理がなされているか分かっていないことではないかと思います。

筆者もこれまでRを使ってきましたが、スキル不足という大きな壁に何度もぶち当たってきました。筆者はプログラミングについてカンがいいほうではなく、だいたい数撃ちゃ当たる方式でトライしてきました。そのとき気づいたのが、既にある便利なものを使うより、自分でイチから作ったほうが理解が深まり、どんな問題でも何となく解決のための方法の予想ができるようになってきました。今でも才能ではなく経験だけでプログラム書いています。

こんな感じなので、同じように感じている方に向けて届くような情報を公開していきたいと思います。

もう少し絞って説明すると、例えば、 dplyr のようなパッケージを使って一時はうまく行っても、他のデータ処理を行うときに詰まってしまうような方、内部処理を理解した上で、自ら関数を組み上げてツールを作ってしまうほうが理解が早い方には参考になる情報だと思います。

Rで多用される list 型データについて

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