データ分析を行ってみたいがデータが思うように揃わないということは多々あります。ところが色々と調べてみると意外にも記録として様々な形で分散してることがあります。ただ、このような記録はそのままでは分析データとして使えないために整理する必要があります。整理するにしても手動では手間がかかり過ぎたり間違いが混入したりと容易に予想できますし、そのようなデータは大抵手順などが属人化し過ぎて再現(復元)が困難です。
 

データハンドリング

そこで、散らばっている様々な記録を分析データとして扱えるようにする処理がデータハンドリングです。一般的に統計分析処理においてデータ分析に費やす時間は全体の一割程度と言われます。残りの九割は分析データとして扱えるように行う処理、すなわち、データハンドリングに費やされていることになります。逆に言えば、データハンドリングはデータ分析において重要な工程なのです。
 

処理の自動化

データハンドリングはデータ分析には必須ですが、手間がかかる作業は自動化したいものです。特にデータ量が多くなってくると手動でのデータハンドリングには限界があります。分析環境とデータハンドリング環境は別の環境でも構いませんが、全体の作業効率を考えると分析環境でデータハンドリングができるのがベストです。そこで、活用したいのが R のデータハンドリング用パッケージと言えるtidyverseパッケージです。本資料ではtidyverseパッケージの中でも整然データ(Tidy Data)を作るのに必須と言えるdplyrパッケージとtidyrパッケージを中心に解説していきます。
 

整然データとは

整然データ(Tidy Data)とはJournal of Statistical Software Vol.59(2014)に掲載されたHadley Wickhamの論文 Tidy Data において提唱されたデータ分析に有用な概念です。簡潔に日本語で整理された情報は 整然データとは何か 整然データってなに? で公開されていますので、まずは、こちらで整然データ(Tidy Data)の概念を把握して下さい。
 

雑然データ

整然データと対をなすのは雑然データ(Messy Data)と言われるものです。雑然データはデータ分析環境にとっては扱いにくいデータなのですが、人間にとっては理解しやすい構造のために多用されています。概ね記録は雑然データとして残されています。
 

雑然データと整然データ

言葉だけでは分かりにくいので具体な比較してみましょう。こちらが雑然データです。
こちらが上記の雑然データを整理して整然データとしたものです。

このように雑然データはクロス集計されたような形式であり行と列が交差する部分を読み取らないとデータの意味が理解できない形式です。一方、整然データは雑然データに比べて冗長ですが行を読み取るだけで一つのデータの意味が理解できる形式になります。  

整然データの定義

上記から分かるように整然データとは以下の満たしたデータ形式のことです。

  1. 個々の変数が一つの列になっている
  2. 個々の観測(値)が一つの行になっている
  3. 個々の観測(値)の構成単位の累計が一つの表になっている
  4. 個々の値は一つのセルになっている

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)

この様にパイプ演算子は中間変数を作成することなく処理の手順通りに記述できます。中間変数がなくなることでコードの可読性も上がります。

では、パイプ演算子以外のデータハンドリング処理に必要な関数を見ていきましょう。  

Data Overview

はじめに利用するデータを説明します。データはnycflight13パッケージに含まれるデータセットを用います。このデータは2013年のNYC発のフライトデータで以下のようなデータセットで構成されています。データが巨大ですので各データ共最初の3行のみの表示です。  

flights

nycflight13パッケージの中心となるデータです。項目数が多いですが基本となる飛行データで約33万レコードあります。

 

airlines

航空会社のコードと航空会社名のデータです。

 

airports

空港コード、空港名、位置座標、タイムゾーン等のデータです。

 

planes

飛行機コード、製造年、製造メーカ、モデル等のデータです。

 

weather

フライト日の空港気象に関するデータです。

 

sample

データをサンプリングする関数はサンプル数を数字で指定する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)

 

sample_n

対象となるデータから指定行数(この場合は100行)のデータをランダムサンプリングします。

nycflights13::flights %>% 
  dplyr::sample_n(100)                   # 100個のデータをランダムサンプリング

 

sample_frac

対象となるデータから指定比率(1を100%とするのでこの場合は0.1%)でデータをランダムサンプリングします。

nycflights13::flights %>% 
  dplyr::sample_frac(0.001)              # 0.1%のデータをランダムサンプリング

 

filter

データを任意の条件で行選択する場合はdplyr::filter関数を用います。条件式には比較演算子の他に論理演算子等が使えます。

filter(.data, ...)

 

filter

ランダムサンプリングした100行のデータからアメリカン航空(AA)のフライトデータを抜き出すには以下のように指定します。

nycflights13::flights %>% 
  dplyr::sample_n(100) %>%               # 100個のデータをランダムサンプリング
  dplyr::filter(carrier == "AA")         # 航空会社がAAと一致するデータのみ抽出

 

select

nycflights13::flightsは項目数が多いので表示項目を限定してみましょう。項目(列)を選択するにはdplyr::select関数を用います。

select(.data, ...)

 

select

ランダムサンプリングした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)

 

rename

このままでは分かりにくいので項目名を日本語に変更してみましょう。項目名(列名)を変更するにはdplyr::rename関数を用います。ただし、日本語項目名(列名)は意図しない動作を引き起こす可能性もありますので、表示する時のみに用いるのが無難です。

rename(.data, ...)

 

rename

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)

 

selectによるrename

dplyr::select関数でも名前を変更すことは可能です。以下のように項目を指定する際に名前も指定することで選択と同時に名前を変更することが可能です。

nycflights13::flights %>% 
  dplyr::sample_n(100) %>%               # 100個のデータをランダムサンプリング
  dplyr::filter(carrier == "AA") %>%     # 航空会社がAAと一致するデータのみ抽出
  dplyr::select('年' = year, '月' = month, '日' = day, '出発時間' = dep_time,
                '航空会社' = carrier, '出発地' = origin, '目的地' = dest)

 

Select Helpers

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())

 

mutate

データセット(データフレーム)に新たな項目(列)をdplyr::mutate関数を用いて追加することが可能です。  

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)

 

Scoped Variants

dplyr::mutate関数にはmutateファミリーとも言える3つの関数があります。

関数 機能
mutate_all 全ての項目(列)に対して
mutate_at 指定した項目(列)に対して(補助関数が使えます)
mutate_if 指定した条件を満たす項目に対して

後述のdplyr::summarize関数にも同様に以下の3つの関数があります。

関数 機能
summarize_all 全ての項目(列)に対して
summarize_at 指定した項目(列)に対して(補助関数が使えます)
summarize_if 指定した条件を満たす項目に対して

なお、dplyr::mutate_each/summarize_each関数は廃止される予定ですので使わない様にしてください。  

summarise

項目(列)毎に要約(集計)するにはdplr:summarise関数を用います。

summarise(.data, ...)
summarize(.data, ...)       # summarise関数のエイリアス

 

summarise_if

数値項目(列)に対して平均値と標準偏差を求めてみましょう。対象が数値項目ですので、ここでは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)))

 

group_by

要約(集計)は行の値毎、例えば到着地毎とか航空会社毎とかに行いたい場合があります。この場合にはdplyr::group_by関数を用いてグループ化します。

group_by(.data, ..., add = FALSE)
ungroup(x, ...)                     # グループ化を解除する場合に用いる

 

summarize_at

例えば目的地毎の平均遅延時間を計算したいとします。この場合、まず、実遅延時間を計算し、目的に毎にデータをグループ化します。最後に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)))

 

group_by

グループ化は複数の項目(列)に対して適用することも可能です。目的地と航空会社の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)))

 

spread

整然データのままでは人間が情報を把握するのは困難ですので雑然データの形式に変更してみます。これにはtidyr::spread関数を用います。

spread(data, key, value, fill = NA, convert = FALSE, drop = TRUE, sep = NULL)

 

spread

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)

 

join

目的地と航空会社の表示にコードが使われていますので、これらを分かりやすく目的地の空港名と航空会社名にしてみましょう。結合には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, ...)

 

left_join

データセットの項で説明したように航空会社名は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"

 


Sampo Suzuki CC BY-NC-SA 4.0 , Sampo Suzuki [2019-05-10(JST)]