データの tidy

(これは完全な Tidy data paper の非公式でコードの重いバージョンです。 詳細はそちらを参照してください)。

データの tidy

データ分析の80%はデータの整理・準備に費やされるとよく言われます。 しかも、それは最初の一歩だけではなく、分析の過程で新たな問題が明らかになったり、新しいデータが収集されたりするたびに、何度も繰り返さなければなりません。 この問題を把握するために、本稿ではデータクリーニングの中でも、小さいながらも重要な側面に焦点を当て、私はそれをデータの ** tidying** (整頓) と呼んでいます。 つまり、分析しやすいようにデータセットを構造化することです。

tidy data の原則は、データセット内のデータ値を整理する標準的な方法を提供します。 標準規格があれば、毎回ゼロから始めて車輪を再発明する必要がないため、最初のデータクリーニングが容易になります。 tidy data standard は、データの初期探索と分析を容易にし、連携するデータ分析ツールの開発を簡素化するために設計されています。 現在のツールは翻訳が必要な場合が多い。 あるツールからの出力を別のツールに入力できるようにするために、時間をかけて咀嚼しなければなりません。 整頓されたデータセットと整頓されたツールは、手を取り合ってデータ分析を容易にし、興味のないデータのロジスティックスではなく、興味深い分野の問題に集中することができます。

tidy データの定義

Happy families are all alike; every unhappy family is unhappy in its own way — Leo Tolstoy

家族のように、整頓されたデータセットはみな似ているが、混乱したデータセットはどれもそれなりに混乱している。 tidy データセットは、データセットの構造(物理的なレイアウト)とセマンティクス(意味)を結びつける標準的な方法を提供すします この節では、データセットの構造とセマンティクスを記述するための標準的な語彙を提供し、それらの定義を使って tidy データを定義します。

データ構造

ほとんどの統計データは、で構成されるデータフレームです。 列にはほとんどの場合ラベルが貼られ、行には時々ラベルが貼られます。 次のコードは、想像上の教室に関するいくつかのデータを、一般的に見られる形式で提供しています。 表は3つの列と4つの行を持ち、行と列の両方にラベルが付けられています。

classroom <- read.csv("classroom.csv", stringsAsFactors = FALSE)
classroom
#>     name quiz1 quiz2 test1
#> 1  Billy  <NA>     D     C
#> 2   Suzy     F  <NA>  <NA>
#> 3 Lionel     B     C     B
#> 4  Jenny     A     A     B

同じ基礎データを構造化するには様々な方法があります。 次の表は、上記と同じデータを示していますが、行と列が入れ替わっています。

read.csv("classroom2.csv", stringsAsFactors = FALSE)
#>   assessment Billy  Suzy Lionel Jenny
#> 1      quiz1  <NA> FALSE      B     A
#> 2      quiz2     D    NA      C     A
#> 3      test1     C    NA      B     B

データは同じですが、レイアウトが異なります。この2つのテーブルがなぜ同じデータを表しているのかを説明するには、行と列の語彙が不足しています。見た目だけでなく、テーブルに表示される値の基本的なセマンティクス(意味)を説明する方法が必要です。

データセマンティクス

データセットはの集まりで、通常は数字(定量的な場合)または文字列(定性的な場合)です。値は2つの方法で整理されます。 すべての値は 変数観測 に属する。 変数には、同じ基本属性(高さ、温度、時間など)を単位ごとに測定するすべての値が含まれます。 観測値は、属性を超えて同じ単位(人、日、レースなど)で測定されたすべての値を含みます。

教室のデータを整理すると、次のようになります。(関数がどのように機能するかは後ほど説明します)

library(tidyr)
library(dplyr)
classroom2 <- classroom %>% 
  pivot_longer(quiz1:test1, names_to = "assessment", values_to = "grade") %>% 
  arrange(name, assessment)
classroom2
#> # A tibble: 12 × 3
#>    name   assessment grade
#>    <chr>  <chr>      <chr>
#>  1 Billy  quiz1      <NA> 
#>  2 Billy  quiz2      D    
#>  3 Billy  test1      C    
#>  4 Jenny  quiz1      A    
#>  5 Jenny  quiz2      A    
#>  6 Jenny  test1      B    
#>  7 Lionel quiz1      B    
#>  8 Lionel quiz2      C    
#>  9 Lionel test1      B    
#> 10 Suzy   quiz1      F    
#> # … with 2 more rows

これにより、値、変数、観測値がより明確になります。データセットには、3つの変数を表す36個の値と12個の観測値が含まれています。 変数は

  1. name, 4つの可能な値(Billy, Suzy, Lionel, Jenny)を持つ。

  2. assessment, 3つの値を持つ (quiz1, quiz2, test1).

  3. grade: 欠損値をどのように考えるかによって、5つまたは6つの値があります (A, B, C, D, F, NA)。

整理されたデータフレームは、観測の定義を明示的に教えてくれます。 この教室では、nameassessmentのすべての組み合わせが、1つの測定された観測値です。 また、データセットは欠損値についても教えてくれますが、これには意味があります。 Billy は最初の小テストを欠席したが、成績を挽回しようとした。 Suzy は最初の小テストに失敗したので、そのクラスをやめることにしました。 Billy の最終成績を計算するために、この欠損値をFに置き換えるかもしれません(あるいは、小テストを受ける2回目のチャンスを得るかもしれません)。 しかし、テスト1のクラス平均を知りたいのであれば、新しい値を入力するよりも、Suzy の構造的な欠損値を削除する方が適切です。

あるデータセットにおいて、何がオブザベーションで何が変数なのかを把握するのは通常簡単ですが、一般的に変数やオブザベーションを正確に定義するのは意外と難しいものです。例えば、教室のデータの列が heightweight だったら、喜んで変数と呼んでいたでしょう。 もし、列が「高さ」と「幅」だったら、高さと幅を「次元」変数の値と考えるかもしれないので、あまり明確ではないでしょう。 列が home phonework phone であれば、これらを2つの変数として扱うことができますが、不正行為を検出する環境では、1つの電話番号を複数の人が使用することは不正行為を示唆する可能性があるため、phone numbernumber type という変数が必要になるかもしれません。 一般的な経験則としては、行間よりも変数間の関数的関係(例えば、zxyの線形結合であり、densityweightvolumeの比である)を記述する方が簡単であり、列のグループ間よりもオブザベーションのグループ間の比較(例えば,グループaの平均とグループbの平均)を行う方が簡単である.

ある分析では、複数の観測レベルが存在する可能性があります。例えば、新しいアレルギー薬の試験では、3つの観測タイプがあるかもしれません: 各人から収集した人口統計学的データ(agesexrace)、各日に各人から収集した医学的データ(number of sneezesredness of eyes)、各日に収集した気象学的データ(temperaturepollen count)です。

変数は分析中に変更される可能性があります。 多くの場合、生データの変数は非常に細かいものであり、説明的な利益が少ないにもかかわらず、モデル化の複雑さが増してしまうことがあります。 例えば、多くの調査では、基本的な特性をよりよく把握するために、同じ質問のバリエーションを尋ねます。 分析の初期段階では、変数は質問に対応しています。 後半の段階では、複数の質問を平均化して算出された特性に焦点を移します。 これにより、階層モデルを必要とせず、データが離散的ではなく連続的であるとみなすことができるため、分析が大幅に簡素化されます。

tidy data

tidy data とは、データセットの意味をその構造にマッピングする標準的な方法です。 データセットは、行、列、表が観測値、変数、型とどのようにマッチしているかによって、雑然としたり整然としたりする。 整頓されたデータ では

  1. すべての列が変数である。

  2. すべての行はオブザベーションである。

  3. すべてのセルは1つの値である。

これはコッドの第3正規形であるが、制約は統計的な言語で表現され、リレーショナルデータベースでよく見られる多数の連結されたデータセットではなく、1つのデータセットに焦点が当てられている。 雑然としたデータとは、データの他の配置のことです。

整頓されたデータは、データセットを構成する標準的な方法を提供するため、分析者やコンピュータが必要な変数を簡単に抽出することができます。 教室のデータの異なるバージョンを比較してみてください。 混乱したバージョンでは、異なる変数を抽出するために異なる戦略を使用する必要があります。 これでは、分析が遅くなり、エラーが発生しやすくなります。 多くのデータ分析操作が、変数のすべての値を含むことを考えれば(すべての集約関数)、これらの値をシンプルで標準的な方法で抽出することがいかに重要かがわかるだろう。 tidy data は、Rのようなベクトル化されたプログラミング言語に特に適しています。 なぜなら、レイアウトによって、同じ観測結果からの異なる変数の値が常にペアになるからです。

変数とオブザベーションの順序は分析に影響しませんが、順序が良いと生の値をスキャンするのが容易になります。 変数を整理する1つの方法は、分析における役割によるものです。 データ収集の設計によって値が固定されているのか、それとも実験の過程で測定されているのか? 固定変数は、実験計画を記述し、事前に知られています。 コンピュータ科学者はしばしば固定変数を次元と呼び、統計学者は通常、ランダム変数の添え字で表します。 測定された変数は、研究で実際に測定するものです。固定変数が最初に来て、次に測定された変数が続き、それぞれ関連する変数が連続するように並べます。 そして、最初の変数で行を並べ、2番目以降の(固定)変数との関係を断ち切ることができる。これは、本稿のすべての表形式の表示で採用されている規則である。

雑然としたデータセットを tidy する

実際のデータセットは、整頓されたデータの3つの教訓を、考えられるほぼすべての方法で破ることができますし、しばしばそうなります。 たまに、すぐに分析を始められるようなデータセットが手に入ることもありますが、それは例外であって、原則ではありません。 このセクションでは、整頓されていないデータの5つの最も一般的な問題と、その解決策を説明します。

驚くべきことに、上記で明示的に説明されていないタイプのゴチャゴチャしたデータセットを含め、ほとんどのゴチャゴチャしたデータセットは、ピボット(長く、広く)とセパレートという小さなツールセットで整頓することができる。 以下のセクションでは、私が遭遇した実際のデータセットを使って各問題を説明し、それらを整頓する方法を示しています。

列ヘッダーは変数名ではなく値である

混乱したデータセットの一般的なタイプは、プレゼンテーション用に作成された表形式のデータで、変数が行と列の両方を構成し、列のヘッダーは変数名ではなく値になっています。 私はこの配置を厄介だと思っていますが、場合によっては非常に役に立つこともあります。 完全にクロスしたデザインを効率的に保存することができますし、必要な操作が行列操作として表現できる場合には、非常に効率的な計算を行うことができます。

次のコードは、この形式の典型的なデータセットのサブセットを示しています。このデータセットは、アメリカにおける所得と宗教の関係を調べたものです。 Pew Research Centerというアメリカのシンクタンクが作成した報告書からのものです。 Pew Research Centerは、宗教からインターネットまで、さまざまなトピックに対する態度についてのデータを収集しており、この形式のデータセットを含む報告書を多数作成しています。

relig_income
#> # A tibble: 18 × 11
#>    religion      `<$10k` $10-2…¹ $20-3…² $30-4…³ $40-5…⁴ $50-7…⁵ $75-1…⁶ $100-…⁷
#>    <chr>           <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
#>  1 Agnostic           27      34      60      81      76     137     122     109
#>  2 Atheist            12      27      37      52      35      70      73      59
#>  3 Buddhist           27      21      30      34      33      58      62      39
#>  4 Catholic          418     617     732     670     638    1116     949     792
#>  5 Don’t know/…       15      14      15      11      10      35      21      17
#>  6 Evangelical …     575     869    1064     982     881    1486     949     723
#>  7 Hindu               1       9       7       9      11      34      47      48
#>  8 Historically…     228     244     236     238     197     223     131      81
#>  9 Jehovah's Wi…      20      27      24      24      21      30      15      11
#> 10 Jewish             19      19      25      25      30      95      69      87
#> # … with 8 more rows, 2 more variables: `>150k` <dbl>,
#> #   `Don't know/refused` <dbl>, and abbreviated variable names ¹​`$10-20k`,
#> #   ²​`$20-30k`, ³​`$30-40k`, ⁴​`$40-50k`, ⁵​`$50-75k`, ⁶​`$75-100k`, ⁷​`$100-150k`

このデータセットには、religion, income, frequency という3つの変数があります。 これを整理するには、変数以外の列を2列のキーと値のペアに ピボット する必要があります。 この作業はよく、広いデータセットを長く(あるいは高く)すると表現されます。

変数をピボットする際には、新たに作成するキーバリュー列の名前を指定する必要があります。 ピボットする列(religionを除くすべての列)を定義した後、キーとなる列の名前が必要になります。 これは、列の見出しの値で定義される変数の名前です。 ここでは incomeとしています。2番目の引数は値の列の名前、frequency です。

relig_income %>% 
  pivot_longer(-religion, names_to = "income", values_to = "frequency")
#> # A tibble: 180 × 3
#>    religion income             frequency
#>    <chr>    <chr>                  <dbl>
#>  1 Agnostic <$10k                     27
#>  2 Agnostic $10-20k                   34
#>  3 Agnostic $20-30k                   60
#>  4 Agnostic $30-40k                   81
#>  5 Agnostic $40-50k                   76
#>  6 Agnostic $50-75k                  137
#>  7 Agnostic $75-100k                 122
#>  8 Agnostic $100-150k                109
#>  9 Agnostic >150k                     84
#> 10 Agnostic Don't know/refused        96
#> # … with 170 more rows

この形式は、各列が変数を表し、各行が観測値(この場合は、「宗教」と「所得」の組み合わせに対応する人口統計学的単位)を表すので、整然としています。

この形式は、時間的に一定の間隔で観測値を記録する場合にも使用されます。例えば,下記のビルボードのデータセットは,ある曲がビルボードのトップ100に初めて入った日を記録しています。 このデータセットには、artist, track, date.entered, rank, week という変数があります。 トップ100に入った後の各週のランクは、wk1 から wk75 までの75列に記録されています。 この保存形式は整然としたものではありませんが、データ入力には便利です。 そうしないと、各週の各曲がそれぞれの列を必要とし、タイトルやアーティストなどの曲のメタデータを繰り返す必要があるため、重複を減らすことができます。 これについては、multiple types で詳しく説明します。

billboard
#> # A tibble: 317 × 79
#>    artist track date.ent…¹   wk1   wk2   wk3   wk4   wk5   wk6   wk7   wk8   wk9
#>    <chr>  <chr> <date>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1 2 Pac  Baby… 2000-02-26    87    82    72    77    87    94    99    NA    NA
#>  2 2Ge+h… The … 2000-09-02    91    87    92    NA    NA    NA    NA    NA    NA
#>  3 3 Doo… Kryp… 2000-04-08    81    70    68    67    66    57    54    53    51
#>  4 3 Doo… Loser 2000-10-21    76    76    72    69    67    65    55    59    62
#>  5 504 B… Wobb… 2000-04-15    57    34    25    17    17    31    36    49    53
#>  6 98^0   Give… 2000-08-19    51    39    34    26    26    19     2     2     3
#>  7 A*Tee… Danc… 2000-07-08    97    97    96    95   100    NA    NA    NA    NA
#>  8 Aaliy… I Do… 2000-01-29    84    62    51    41    38    35    35    38    38
#>  9 Aaliy… Try … 2000-03-18    59    53    38    28    21    18    16    14    12
#> 10 Adams… Open… 2000-08-26    76    76    74    69    68    67    61    58    57
#> # … with 307 more rows, 67 more variables: wk10 <dbl>, wk11 <dbl>, wk12 <dbl>,
#> #   wk13 <dbl>, wk14 <dbl>, wk15 <dbl>, wk16 <dbl>, wk17 <dbl>, wk18 <dbl>,
#> #   wk19 <dbl>, wk20 <dbl>, wk21 <dbl>, wk22 <dbl>, wk23 <dbl>, wk24 <dbl>,
#> #   wk25 <dbl>, wk26 <dbl>, wk27 <dbl>, wk28 <dbl>, wk29 <dbl>, wk30 <dbl>,
#> #   wk31 <dbl>, wk32 <dbl>, wk33 <dbl>, wk34 <dbl>, wk35 <dbl>, wk36 <dbl>,
#> #   wk37 <dbl>, wk38 <dbl>, wk39 <dbl>, wk40 <dbl>, wk41 <dbl>, wk42 <dbl>,
#> #   wk43 <dbl>, wk44 <dbl>, wk45 <dbl>, wk46 <dbl>, wk47 <dbl>, wk48 <dbl>, …

このデータセットを整理するために,まずpivot_longer()を使ってデータセットを長くします。 列を wk1 から wk76 に変換し、名前を表す新しい列 week と、値を表す新しい値 rank を作ります。

billboard2 <- billboard %>% 
  pivot_longer(
    wk1:wk76, 
    names_to = "week", 
    values_to = "rank", 
    values_drop_na = TRUE
  )
billboard2
#> # A tibble: 5,307 × 5
#>    artist  track                   date.entered week   rank
#>    <chr>   <chr>                   <date>       <chr> <dbl>
#>  1 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk1      87
#>  2 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk2      82
#>  3 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk3      72
#>  4 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk4      77
#>  5 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk5      87
#>  6 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk6      94
#>  7 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk7      99
#>  8 2Ge+her The Hardest Part Of ... 2000-09-02   wk1      91
#>  9 2Ge+her The Hardest Part Of ... 2000-09-02   wk2      87
#> 10 2Ge+her The Hardest Part Of ... 2000-09-02   wk3      92
#> # … with 5,297 more rows

ここでは、values_drop_na = TRUEを使って、ランク列から欠損値を削除しています。 このデータでは、欠損値は曲がチャートに入っていなかった週を表しているので、安全に取り除くことができます。

このケースでは、ちょっとしたクリーニングをして、week変数を数字に変換し、チャートの各週に対応する日付を算出するのもいいでしょう。

billboard3 <- billboard2 %>%
  mutate(
    week = as.integer(gsub("wk", "", week)),
    date = as.Date(date.entered) + 7 * (week - 1),
    date.entered = NULL
  )
billboard3
#> # A tibble: 5,307 × 5
#>    artist  track                    week  rank date      
#>    <chr>   <chr>                   <int> <dbl> <date>    
#>  1 2 Pac   Baby Don't Cry (Keep...     1    87 2000-02-26
#>  2 2 Pac   Baby Don't Cry (Keep...     2    82 2000-03-04
#>  3 2 Pac   Baby Don't Cry (Keep...     3    72 2000-03-11
#>  4 2 Pac   Baby Don't Cry (Keep...     4    77 2000-03-18
#>  5 2 Pac   Baby Don't Cry (Keep...     5    87 2000-03-25
#>  6 2 Pac   Baby Don't Cry (Keep...     6    94 2000-04-01
#>  7 2 Pac   Baby Don't Cry (Keep...     7    99 2000-04-08
#>  8 2Ge+her The Hardest Part Of ...     1    91 2000-09-02
#>  9 2Ge+her The Hardest Part Of ...     2    87 2000-09-09
#> 10 2Ge+her The Hardest Part Of ...     3    92 2000-09-16
#> # … with 5,297 more rows

最後に、データをソートするのは常に良いアイデアです。 アーティスト別、トラック別、週別などが考えられます。

billboard3 %>% arrange(artist, track, week)
#> # A tibble: 5,307 × 5
#>    artist  track                    week  rank date      
#>    <chr>   <chr>                   <int> <dbl> <date>    
#>  1 2 Pac   Baby Don't Cry (Keep...     1    87 2000-02-26
#>  2 2 Pac   Baby Don't Cry (Keep...     2    82 2000-03-04
#>  3 2 Pac   Baby Don't Cry (Keep...     3    72 2000-03-11
#>  4 2 Pac   Baby Don't Cry (Keep...     4    77 2000-03-18
#>  5 2 Pac   Baby Don't Cry (Keep...     5    87 2000-03-25
#>  6 2 Pac   Baby Don't Cry (Keep...     6    94 2000-04-01
#>  7 2 Pac   Baby Don't Cry (Keep...     7    99 2000-04-08
#>  8 2Ge+her The Hardest Part Of ...     1    91 2000-09-02
#>  9 2Ge+her The Hardest Part Of ...     2    87 2000-09-09
#> 10 2Ge+her The Hardest Part Of ...     3    92 2000-09-16
#> # … with 5,297 more rows

または、日付とランクで。

billboard3 %>% arrange(date, rank)
#> # A tibble: 5,307 × 5
#>    artist   track   week  rank date      
#>    <chr>    <chr>  <int> <dbl> <date>    
#>  1 Lonestar Amazed     1    81 1999-06-05
#>  2 Lonestar Amazed     2    54 1999-06-12
#>  3 Lonestar Amazed     3    44 1999-06-19
#>  4 Lonestar Amazed     4    39 1999-06-26
#>  5 Lonestar Amazed     5    38 1999-07-03
#>  6 Lonestar Amazed     6    33 1999-07-10
#>  7 Lonestar Amazed     7    29 1999-07-17
#>  8 Amber    Sexual     1    99 1999-07-17
#>  9 Lonestar Amazed     8    29 1999-07-24
#> 10 Amber    Sexual     2    99 1999-07-24
#> # … with 5,297 more rows

複数の変数が1つの列に格納される

列をピボットした後、キーとなる列が複数の基礎となる変数名の組み合わせになることがあります。 これは、以下の tb (tuberculosis) データセットで起こります。 このデータセットは世界保健機関(World Health Organisation)からのもので、確認された結核の症例数を「国」、「年」、「人口グループ」ごとに記録しています。 人口統計グループは、「性別」(男性、女性)と「年齢」(0~14歳、15~25歳、25~34歳、35~44歳、45~54歳、55~64歳、不明)で分類されています。

tb <- as_tibble(read.csv("tb.csv", stringsAsFactors = FALSE))
tb
#> # A tibble: 5,769 × 22
#>    iso2   year   m04  m514  m014 m1524 m2534 m3544 m4554 m5564   m65    mu   f04
#>    <chr> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
#>  1 AD     1989    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  2 AD     1990    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  3 AD     1991    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  4 AD     1992    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  5 AD     1993    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  6 AD     1994    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#>  7 AD     1996    NA    NA     0     0     0     4     1     0     0    NA    NA
#>  8 AD     1997    NA    NA     0     0     1     2     2     1     6    NA    NA
#>  9 AD     1998    NA    NA     0     0     0     1     0     0     0    NA    NA
#> 10 AD     1999    NA    NA     0     0     0     1     1     0     0    NA    NA
#> # … with 5,759 more rows, and 9 more variables: f514 <int>, f014 <int>,
#> #   f1524 <int>, f2534 <int>, f3544 <int>, f4554 <int>, f5564 <int>, f65 <int>,
#> #   fu <int>

まず、pivot_longer() を使って、変数以外の列を集めます。

tb2 <- tb %>% 
  pivot_longer(
    !c(iso2, year), 
    names_to = "demo", 
    values_to = "n", 
    values_drop_na = TRUE
  )
tb2
#> # A tibble: 35,750 × 4
#>    iso2   year demo      n
#>    <chr> <int> <chr> <int>
#>  1 AD     1996 m014      0
#>  2 AD     1996 m1524     0
#>  3 AD     1996 m2534     0
#>  4 AD     1996 m3544     4
#>  5 AD     1996 m4554     1
#>  6 AD     1996 m5564     0
#>  7 AD     1996 m65       0
#>  8 AD     1996 f014      0
#>  9 AD     1996 f1524     1
#> 10 AD     1996 f2534     1
#> # … with 35,740 more rows

この形式のカラムヘッダは,英数字以外の文字(例:., -, _, :)で区切られていたり、このデータセットのように固定幅の形式になっていたりします。 separate() を使うと、複合変数を個々の変数に簡単に分割することができます。 分割するための正規表現(デフォルトでは英数字以外の列で分割されます)、または文字位置のベクトルを渡すことができます。 この例では、最初の文字の後で分割したいと思います。

tb3 <- tb2 %>% 
  separate(demo, c("sex", "age"), 1)
tb3
#> # A tibble: 35,750 × 5
#>    iso2   year sex   age       n
#>    <chr> <int> <chr> <chr> <int>
#>  1 AD     1996 m     014       0
#>  2 AD     1996 m     1524      0
#>  3 AD     1996 m     2534      0
#>  4 AD     1996 m     3544      4
#>  5 AD     1996 m     4554      1
#>  6 AD     1996 m     5564      0
#>  7 AD     1996 m     65        0
#>  8 AD     1996 f     014       0
#>  9 AD     1996 f     1524      1
#> 10 AD     1996 f     2534      1
#> # … with 35,740 more rows

このような形で値を保存することで、元のデータの問題点を解決することができます。 数ではなく率を比較したいので、母集団を知る必要があります。 元の形式では、人口変数を追加する簡単な方法がありません。 別のテーブルに保存しなければならないので、母集団とカウントを正しく一致させるのは難しいのです。 tidy 形式では、人口と率の変数を追加するのは、単なる追加列なので簡単です。

この場合、names_to に複数のカラム名を与え、さらに names_pattern にグループ化された正規表現を与えることで、ワンステップで変換を行うこともできます。

tb %>% pivot_longer(
  !c(iso2, year), 
  names_to = c("sex", "age"), 
  names_pattern = "(.)(.+)",
  values_to = "n", 
  values_drop_na = TRUE
)
#> # A tibble: 35,750 × 5
#>    iso2   year sex   age       n
#>    <chr> <int> <chr> <chr> <int>
#>  1 AD     1996 m     014       0
#>  2 AD     1996 m     1524      0
#>  3 AD     1996 m     2534      0
#>  4 AD     1996 m     3544      4
#>  5 AD     1996 m     4554      1
#>  6 AD     1996 m     5564      0
#>  7 AD     1996 m     65        0
#>  8 AD     1996 f     014       0
#>  9 AD     1996 f     1524      1
#> 10 AD     1996 f     2534      1
#> # … with 35,740 more rows

変数が行と列の両方に格納されている

変数が行と列の両方に格納されている場合、最も複雑な形でデータが混乱します。 以下のコードは、Global Historical Climatology Network から、2010年の5ヶ月間、メキシコの1つのウェザーステーション(MX17004)の日ごとの気象データを読み込みます。

weather <- as_tibble(read.csv("weather.csv", stringsAsFactors = FALSE))
weather
#> # A tibble: 22 × 35
#>    id       year month element    d1    d2    d3    d4    d5    d6    d7    d8
#>    <chr>   <int> <int> <chr>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1 MX17004  2010     1 tmax       NA  NA    NA      NA  NA      NA    NA    NA
#>  2 MX17004  2010     1 tmin       NA  NA    NA      NA  NA      NA    NA    NA
#>  3 MX17004  2010     2 tmax       NA  27.3  24.1    NA  NA      NA    NA    NA
#>  4 MX17004  2010     2 tmin       NA  14.4  14.4    NA  NA      NA    NA    NA
#>  5 MX17004  2010     3 tmax       NA  NA    NA      NA  32.1    NA    NA    NA
#>  6 MX17004  2010     3 tmin       NA  NA    NA      NA  14.2    NA    NA    NA
#>  7 MX17004  2010     4 tmax       NA  NA    NA      NA  NA      NA    NA    NA
#>  8 MX17004  2010     4 tmin       NA  NA    NA      NA  NA      NA    NA    NA
#>  9 MX17004  2010     5 tmax       NA  NA    NA      NA  NA      NA    NA    NA
#> 10 MX17004  2010     5 tmin       NA  NA    NA      NA  NA      NA    NA    NA
#> # … with 12 more rows, and 23 more variables: d9 <lgl>, d10 <dbl>, d11 <dbl>,
#> #   d12 <lgl>, d13 <dbl>, d14 <dbl>, d15 <dbl>, d16 <dbl>, d17 <dbl>,
#> #   d18 <lgl>, d19 <lgl>, d20 <lgl>, d21 <lgl>, d22 <lgl>, d23 <dbl>,
#> #   d24 <lgl>, d25 <dbl>, d26 <dbl>, d27 <dbl>, d28 <dbl>, d29 <dbl>,
#> #   d30 <dbl>, d31 <dbl>

これは、個々の列(id, year, month)に変数を持ち、列(day, d1-d31)と行(tmin, tmax)に分散しています(最低気温と最高気温)。 31日に満たない月は、その月の最終日が構造的に欠落しています。

このデータセットを整理するために、まず pivot_longer を使って日の列を集めます。

weather2 <- weather %>% 
  pivot_longer(
    d1:d31, 
    names_to = "day", 
    values_to = "value", 
    values_drop_na = TRUE
  ) 
weather2
#> # A tibble: 66 × 6
#>    id       year month element day   value
#>    <chr>   <int> <int> <chr>   <chr> <dbl>
#>  1 MX17004  2010     1 tmax    d30    27.8
#>  2 MX17004  2010     1 tmin    d30    14.5
#>  3 MX17004  2010     2 tmax    d2     27.3
#>  4 MX17004  2010     2 tmax    d3     24.1
#>  5 MX17004  2010     2 tmax    d11    29.7
#>  6 MX17004  2010     2 tmax    d23    29.9
#>  7 MX17004  2010     2 tmin    d2     14.4
#>  8 MX17004  2010     2 tmin    d3     14.4
#>  9 MX17004  2010     2 tmin    d11    13.4
#> 10 MX17004  2010     2 tmin    d23    10.7
#> # … with 56 more rows

表示のために、私は欠損値を削除して、明示的ではなく暗黙的にしました。 これは、各月の日数がわかっているので、明示的な欠損値を簡単に再構成できるからです。

また、ちょっとしたクリーニングも行います。

weather3 <- weather2 %>% 
  mutate(day = as.integer(gsub("d", "", day))) %>%
  select(id, year, month, day, element, value)
weather3
#> # A tibble: 66 × 6
#>    id       year month   day element value
#>    <chr>   <int> <int> <int> <chr>   <dbl>
#>  1 MX17004  2010     1    30 tmax     27.8
#>  2 MX17004  2010     1    30 tmin     14.5
#>  3 MX17004  2010     2     2 tmax     27.3
#>  4 MX17004  2010     2     3 tmax     24.1
#>  5 MX17004  2010     2    11 tmax     29.7
#>  6 MX17004  2010     2    23 tmax     29.9
#>  7 MX17004  2010     2     2 tmin     14.4
#>  8 MX17004  2010     2     3 tmin     14.4
#>  9 MX17004  2010     2    11 tmin     13.4
#> 10 MX17004  2010     2    23 tmin     10.7
#> # … with 56 more rows

このデータセットはほとんど整頓されていますが、element列は変数ではなく、変数の名前を格納しています。 この例では、他の気象変数である prcp (降水量)と snow (降雪量)は表示されていません)。 これを解決するには,データを広げる必要があります。 pivot_wider()pivot_longer() の逆で、elementvalue を複数の列に渡ってピボットします。

weather3 %>% 
  pivot_wider(names_from = element, values_from = value)
#> # A tibble: 33 × 6
#>    id       year month   day  tmax  tmin
#>    <chr>   <int> <int> <int> <dbl> <dbl>
#>  1 MX17004  2010     1    30  27.8  14.5
#>  2 MX17004  2010     2     2  27.3  14.4
#>  3 MX17004  2010     2     3  24.1  14.4
#>  4 MX17004  2010     2    11  29.7  13.4
#>  5 MX17004  2010     2    23  29.9  10.7
#>  6 MX17004  2010     3     5  32.1  14.2
#>  7 MX17004  2010     3    10  34.5  16.8
#>  8 MX17004  2010     3    16  31.1  17.6
#>  9 MX17004  2010     4    27  36.3  16.7
#> 10 MX17004  2010     5    27  33.2  18.2
#> # … with 23 more rows

このフォームは整然としています。 各列には1つの変数があり、各行は1日を表しています。

1つのテーブルに複数の型

データセットには、複数のレベル、異なる型の観測単位で収集された値が含まれることがよくあります。 整頓の際には、各タイプの観測単位をそれぞれのテーブルに格納する必要があります。 これは、データベースの正規化の考え方と密接に関連しており、各ファクトが1つの場所でのみ表現されます。 そうしないと不整合が生じる可能性があるため、重要です。

ビルボードのデータセットには、実際には、曲と各週での順位という2種類の観測単位が含まれています。 このことは、曲に関する事実が重複していることからもわかります。例えば、artist は何度も繰り返されます。

このデータセットを2つの部分に分解する必要があります。 すなわち、 artistsong name を格納する曲データセットと、各 week における songrank を与えるランキングデータセットです。 まず、 “song” のデータセットを抽出します。

song <- billboard3 %>% 
  distinct(artist, track) %>%
  mutate(song_id = row_number())
song
#> # A tibble: 317 × 3
#>    artist         track                   song_id
#>    <chr>          <chr>                     <int>
#>  1 2 Pac          Baby Don't Cry (Keep...       1
#>  2 2Ge+her        The Hardest Part Of ...       2
#>  3 3 Doors Down   Kryptonite                    3
#>  4 3 Doors Down   Loser                         4
#>  5 504 Boyz       Wobble Wobble                 5
#>  6 98^0           Give Me Just One Nig...       6
#>  7 A*Teens        Dancing Queen                 7
#>  8 Aaliyah        I Don't Wanna                 8
#>  9 Aaliyah        Try Again                     9
#> 10 Adams, Yolanda Open My Heart                10
#> # … with 307 more rows

そのデータを使って、繰り返される曲の情報を曲の詳細(一意の曲ID)へのポインタに置き換えることで、rank データセットを作成します。

rank <- billboard3 %>%
  left_join(song, c("artist", "track")) %>%
  select(song_id, date, week, rank)
rank
#> # A tibble: 5,307 × 4
#>    song_id date        week  rank
#>      <int> <date>     <int> <dbl>
#>  1       1 2000-02-26     1    87
#>  2       1 2000-03-04     2    82
#>  3       1 2000-03-11     3    72
#>  4       1 2000-03-18     4    77
#>  5       1 2000-03-25     5    87
#>  6       1 2000-04-01     6    94
#>  7       1 2000-04-08     7    99
#>  8       2 2000-09-02     1    91
#>  9       2 2000-09-09     2    87
#> 10       2 2000-09-16     3    92
#> # … with 5,297 more rows

また、week データセットを用意して、その週の背景情報を記録することもできます。曲の総売り上げ数や、似たような「人口統計」情報などです。

正規化は、データを整理して不整合をなくすのに便利です。 しかし、リレーショナルデータを直接扱えるデータ分析ツールはほとんどないので、 分析には通常、非正規化やデータセットを1つのテーブルに戻す作業も必要になります。

1つの型が複数のテーブルにある

1種類の観測ユニットに関するデータ値が、複数のテーブルやファイルに分散していることもよくあります。これらのテーブルやファイルは、他の変数によって分割されていることが多く、それぞれが1つの年、人、場所を表しています。個々のレコードのフォーマットが一貫していれば、この問題は簡単に解決できます。

  1. ファイルをテーブルのリストに読み込む。

  2. 各テーブルに、元のファイル名を記録する新しい列を追加します。 (ファイル名はしばしば重要な変数の値である)。

  3. すべてのテーブルを1つのテーブルにまとめる。

以下のコードでは,ディレクトリ(data/)内のファイル名が正規表現(.csvで終わる)にマッチするベクトルを生成します。 次に,ベクトルの各要素にファイル名を付けます。 これは、次のステップで名前を保存するためで,最終的なデータフレームの各行にソースのラベルが付けられるようにします。 最後に、map_dfr() が各パスをループし、csvファイルを読み込んで、結果を1つのデータフレームにまとめます.

library(purrr)
paths <- dir("data", pattern = "\\.csv$", full.names = TRUE)
names(paths) <- basename(paths)
map_dfr(paths, read.csv, stringsAsFactors = FALSE, .id = "filename")

一つのテーブルができたら、必要に応じて追加の整理を行うことができます。 この種のクリーニングの例としては、https://github.com/hadley/data-baby-namesにあるように、 米国社会保障庁から提供された129の年ごとの赤ちゃんの名前のテーブルを取り込んで、1つのファイルにまとめています。

さらに複雑なのは、データセットの構造が時間とともに変化する場合である。 例えば、データセットに異なる変数が含まれていたり、同じ変数でも名前が違っていたり、ファイル形式が違っていたり、欠損値の扱いが違っていたりします。 このような場合には、各ファイルを個別に(運が良ければ小グループで)整頓し、整頓した後に結合する必要があるかもしれません。 この種の整頓の例をhttps://github.com/hadley/data-fuel-economyに示します。 これは、1978年から2008年までの5万台以上の自動車のepa燃費データを整頓したものです。 生データはオンラインで入手できますが、各年が別々のファイルに保存されており、4つの主要なフォーマットと多くのマイナーなバリエーションがあるため、このデータセットを整頓するのはかなり困難です。