データ分析を行ってみたいがデータが思うように揃わないということは多々あります。ところが色々と調べてみると意外にも記録として様々な形で分散してることがあります。ただ、このような記録はそのままでは分析データとして使えないために整理する必要があります。整理するにしても手動では手間がかかり過ぎたり間違いが混入したりと容易に予想できますし、そのようなデータは大抵手順などが属人化し過ぎて再現(復元)が困難です。
そこで、散らばっている様々な記録を分析データとして扱えるようにする処理がデータハンドリングです。一般的に統計分析処理においてデータ分析に費やす時間は全体の一割程度と言われます。残りの九割は分析データとして扱えるように行う処理、すなわち、データハンドリングに費やされていることになります。逆に言えば、データハンドリングはデータ分析において重要な工程なのです。
データハンドリングはデータ分析には必須ですが、手間がかかる作業は自動化したいものです。特にデータ量が多くなってくると手動でのデータハンドリングには限界があります。分析環境とデータハンドリング環境は別の環境でも構いませんが、全体の作業効率を考えると分析環境でデータハンドリングができるのがベストです。そこで、活用したいのが R のデータハンドリング用パッケージと言えるtidyverse
パッケージです。本資料ではtidyverse
パッケージの中でも整然データ(Tidy Data)を作るのに必須と言えるdplyr
パッケージとtidyr
パッケージを中心に解説していきます。
整然データ(Tidy Data)とはJournal of Statistical Software Vol.59(2014)に掲載されたHadley Wickhamの論文 Tidy Data において提唱されたデータ分析に有用な概念です。簡潔に日本語で整理された情報は 整然データとは何か や 整然データってなに? で公開されていますので、まずは、こちらで整然データ(Tidy Data)の概念を把握して下さい。
整然データと対をなすのは雑然データ(Messy Data)と言われるものです。雑然データはデータ分析環境にとっては扱いにくいデータなのですが、人間にとっては理解しやすい構造のために多用されています。概ね記録は雑然データとして残されています。
このように雑然データはクロス集計されたような形式であり行と列が交差する部分を読み取らないとデータの意味が理解できない形式です。一方、整然データは雑然データに比べて冗長ですが行を読み取るだけで一つのデータの意味が理解できる形式になります。
上記から分かるように整然データとは以下の満たしたデータ形式のことです。
Rをはじめとした分析環境はデータ形式が整然データであることを要求しますので、データハンドリングの基本として整然データを作れるようになるのが一番のポイントとなります。
では、実際にdplyr
パッケージとtidyr
パッケージを用いたデータハンドリングの具体例を学んでいきましょう。
dplyr
パッケージやtidyr
パッケージを始めとしたtidyverse
パッケージではパイプ演算子%>%
を用いて記述されることが殆どですので、まずはパイプ演算子%>%
を覚える必要があります。
パイプ演算子は
演算子です。最も簡単な事例で見てみましょう。1 + 2
をパイプ演算子を使って記述すると以下のようになります。
1 %>%
+ 2
## [1] 3
関数の場合はsummary(iris)
をパイプ演算子を使って記述すると以下のようになります。
iris %>%
summary()
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## Min. :4.300 Min. :2.000 Min. :1.000 Min. :0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350 Median :1.300
## Mean :5.843 Mean :3.057 Mean :3.758 Mean :1.199
## 3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max. :2.500
## Species
## setosa :50
## versicolor:50
## virginica :50
##
##
##
パイプ演算子のメリットは、演算子左右のデータ型が一致していれば数珠つなぎのような記述が可能なことです。例えばアヤメの種別毎に平均値を計算し表として整形する場合は以下のようにパイプ演算子でつなげることで処理できます。
iris %>%
dplyr::group_by(Species) %>%
dplyr::summarise_if(is.numeric, funs(mean), na.rm = TRUE)
この様にパイプ演算子は中間変数を作成することなく処理の手順通りに記述できます。中間変数がなくなることでコードの可読性も上がります。
では、パイプ演算子以外のデータハンドリング処理に必要な関数を見ていきましょう。
はじめに利用するデータを説明します。データはnycflight13
パッケージに含まれるデータセットを用います。このデータは2013年のNYC発のフライトデータで以下のようなデータセットで構成されています。データが巨大ですので各データ共最初の3行のみの表示です。
nycflight13
パッケージの中心となるデータです。項目数が多いですが基本となる飛行データで約33万レコードあります。
データをサンプリングする関数はサンプル数を数字で指定するdplyr::sample_n
関数と比率で指定するdplyr::sample_frac
関数があります。
sample_n(tbl, size, replace = FALSE, weight = NULL, .env = NULL)
sample_frac(tbl, size = 1, replace = FALSE, weight = NULL, .env = NULL)
対象となるデータから指定行数(この場合は100行)のデータをランダムサンプリングします。
nycflights13::flights %>%
dplyr::sample_n(100) # 100個のデータをランダムサンプリング
対象となるデータから指定比率(1を100%とするのでこの場合は0.1%)でデータをランダムサンプリングします。
nycflights13::flights %>%
dplyr::sample_frac(0.001) # 0.1%のデータをランダムサンプリング
データを任意の条件で行選択する場合はdplyr::filter
関数を用います。条件式には比較演算子の他に論理演算子等が使えます。
filter(.data, ...)
ランダムサンプリングした100行のデータからアメリカン航空(AA)のフライトデータを抜き出すには以下のように指定します。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::filter(carrier == "AA") # 航空会社がAAと一致するデータのみ抽出
nycflights13::flights
は項目数が多いので表示項目を限定してみましょう。項目(列)を選択するにはdplyr::select
関数を用います。
select(.data, ...)
ランダムサンプリングした100行のデータからアメリカン航空(AA)のフライトデータを抜き出し、year
, month
, day
, dep_time
, carrier
, origin
, dest
のみを表示するには以下のように指定します。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::filter(carrier == "AA") %>% # 航空会社がAAと一致するデータのみ抽出
dplyr::select(year, month, day, dep_time, carrier, origin, dest)
このままでは分かりにくいので項目名を日本語に変更してみましょう。項目名(列名)を変更するにはdplyr::rename
関数を用います。ただし、日本語項目名(列名)は意図しない動作を引き起こす可能性もありますので、表示する時のみに用いるのが無難です。
rename(.data, ...)
year
, month
, day
, dep_time
, carrier
, origin
, dest
を年月日
、出発時間
、航空会社
、出発地
、目的地
に変更するには以下のように指定します。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::filter(carrier == "AA") %>% # 航空会社がAAと一致するデータのみ抽出
dplyr::select(year, month, day, dep_time, carrier, origin, dest) %>%
dplyr::rename('年' = year, '月' = month, '日' = day, '出発時間' = dep_time,
'航空会社' = carrier, '出発地' = origin, '目的地' = dest)
dplyr::select
関数でも名前を変更すことは可能です。以下のように項目を指定する際に名前も指定することで選択と同時に名前を変更することが可能です。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::filter(carrier == "AA") %>% # 航空会社がAAと一致するデータのみ抽出
dplyr::select('年' = year, '月' = month, '日' = day, '出発時間' = dep_time,
'航空会社' = carrier, '出発地' = origin, '目的地' = dest)
dplyr::select/rename
関数で項目(列)を選択する際には以下のような補助関数群が使えます。詳しくはヘルプなどで確認してください。
# 前方一致
starts_with(match, ignore.case = TRUE, vars = current_vars())
# 後方一致
ends_with(match, ignore.case = TRUE, vars = current_vars())
# 部分一致
contains(match, ignore.case = TRUE, vars = current_vars())
# 正規表現
matches(match, ignore.case = TRUE, vars = current_vars())
# 通し番号
num_range(prefix, range, width = NULL, vars = current_vars())
# ベクトル指定
one_of(..., vars = current_vars())
# (残り)全て
everything(vars = current_vars())
データセット(データフレーム)に新たな項目(列)をdplyr::mutate
関数を用いて追加することが可能です。
ここではarr_delay
(到着遅れ時間)からdep_delay
(出発遅れ時間)を引いて実遅延時間をdplyr::mutate
関数を用いて計算し、dep_time
, arr_tiem
, carrier
, origin
, dest
, delay
のみを表示させてみます。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
dplyr::select(dep_time, arr_time, carrier, origin, dest, delay)
dplyr::mutate
関数にはmutateファミリーとも言える3つの関数があります。
関数 | 機能 |
---|---|
mutate_all |
全ての項目(列)に対して |
mutate_at |
指定した項目(列)に対して(補助関数が使えます) |
mutate_if |
指定した条件を満たす項目に対して |
後述のdplyr::summarize
関数にも同様に以下の3つの関数があります。
関数 | 機能 |
---|---|
summarize_all |
全ての項目(列)に対して |
summarize_at |
指定した項目(列)に対して(補助関数が使えます) |
summarize_if |
指定した条件を満たす項目に対して |
なお、dplyr::mutate_each/summarize_each
関数は廃止される予定ですので使わない様にしてください。
項目(列)毎に要約(集計)するにはdplr:summarise
関数を用います。
summarise(.data, ...)
summarize(.data, ...) # summarise関数のエイリアス
数値項目(列)に対して平均値と標準偏差を求めてみましょう。対象が数値項目ですので、ここではdplyr::summarise_if
関数を用いてみます。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
dplyr::select(-year, -month, -day) %>% # 年月日を除く
dplyr::summarise_if(is.numeric, funs(mean(., na.rm = TRUE),
sd(., na.rm = TRUE)))
要約(集計)は行の値毎、例えば到着地毎とか航空会社毎とかに行いたい場合があります。この場合にはdplyr::group_by
関数を用いてグループ化します。
group_by(.data, ..., add = FALSE)
ungroup(x, ...) # グループ化を解除する場合に用いる
例えば目的地毎の平均遅延時間を計算したいとします。この場合、まず、実遅延時間を計算し、目的に毎にデータをグループ化します。最後にdplyr::summarize_at
関数を用いて遅延時間の平均値を求めます。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
group_by(dest) %>% # 目的地毎にグループ化
dplyr::summarise_at(vars(delay), funs(mean(., na.rm = TRUE)))
グループ化は複数の項目(列)に対して適用することも可能です。目的地と航空会社の2項目でグループ化し、各グループに対する平均遅延時間を計算してみましょう。このような計算はクロス集計ともいいます。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
group_by(dest, carrier) %>% # 目的地と航空会社でグループ化
dplyr::summarise_at(vars(delay), funs(mean(., na.rm = TRUE)))
整然データのままでは人間が情報を把握するのは困難ですので雑然データの形式に変更してみます。これにはtidyr::spread
関数を用います。
spread(data, key, value, fill = NA, convert = FALSE, drop = TRUE, sep = NULL)
carrier
(航空会社)をキー(列)にしてdest
(目的地)毎の雑然データ形式にするには以下のように指定します。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
group_by(dest, carrier) %>% # 目的地と航空会社でグループ化
dplyr::summarise_at(vars(delay), funs(round(mean(., na.rm = TRUE), 0))) %>%
# 平均遅延時間を計算
tidyr::spread(key = carrier, value = delay)
目的地と航空会社の表示にコードが使われていますので、これらを分かりやすく目的地の空港名と航空会社名にしてみましょう。結合にはtidyr::_join
関数群を用います。処理イメージはチートシート(PDF) を参照してください。
inner_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
left_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
right_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
full_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
semi_join(x, y, by = NULL, copy = FALSE, ...)
anti_join(x, y, by = NULL, copy = FALSE, ...)
データセットの項で説明したように航空会社名はairline
データセットに空港名はairport
データセットにありますので、これらのデータセットを結合して名前を取り込みます。
nycflights13::flights %>%
dplyr::sample_n(100) %>% # 100個のデータをランダムサンプリング
dplyr::mutate(delay = arr_delay - dep_delay) %>%
# 実遅延時間を計算する
dplyr::select(dep_time, arr_time, carrier, origin, dest, delay) %>%
# 対象項目を減らす
dplyr::left_join(nycflights13::airlines) %>%
# 航空会社データと結合
dplyr::left_join(nycflights13::airports, by = c("dest" = "faa")) %>%
# 空港データと結合
dplyr::select(carrier_name = name.x, dest_name = name.y, delay) %>%
# 項目名を変更・選択
dplyr::group_by(dest_name, carrier_name) %>%
# 目的地名と航空会社名でグループ化
dplyr::summarise_at(vars(delay), funs(round(mean(., na.rm = TRUE), 1))) %>%
# 平均遅延時間を計算
tidyr::spread(key = carrier_name, value = delay)
## Joining, by = "carrier"
CC BY-NC-SA 4.0 , Sampo Suzuki [2019-05-10(JST)]