U8. tidyr: ワイド⇔ロング変換

ユニット概要

分類: 幹(全員必須)

依存関係: U0 → U1 → U2 → U4 → U5 → U6 → U7 → U8

学習目標:

  • 整然データ(Tidy Data)の概念を理解する
  • ワイド型とロング型の違いを理解し、使い分けられる
  • pivot_longer()でワイド型をロング型に変換できる
  • pivot_wider()でロング型をワイド型に変換できる
  • データの形状を自由に変えることで、可視化や分析を柔軟に行える

事前知識: 整然データ(Tidy Data)とは

データ分析における「データの持ち方」の重要性

ここまでみてきたデータは行列の2次元に、ケース×変数の形で格納されていました。この形式は、人間が見て管理するときにわかりやすい形式をしていますが、計算機にとっては必ずしもそうではありません

たとえば「神エクセル」と揶揄されることがあるように、稀に表計算ソフトを方眼紙ソフトあるいは原稿用紙ソフトと勘違いしたかのような使い方がなされる場合があります。人間にとってはわかりやすい(見て把握しやすい)かもしれませんが、計算機にとって構造が把握できないため、データ解析に不向きです。巷には、こうした分析しにくい電子データがまだまだたくさん存在します。

機械判読可能なデータの原則

これをうけて2020年12月、総務省により機械判読可能なデータの表記方法の統一ルールが策定されました。それには次のようなチェック項目が含まれています:

  • ファイル形式はExcelかCSVとなっているか
  • 1セル1データとなっているか
  • 数値データは数値属性とし、文字列を含まないこと
  • セルの結合をしていないか
  • スペースや改行等で体裁を整えていないか
  • 項目名を省略していないか
  • 数式を使用している場合は、数値データに修正しているか
  • オブジェクトを使用していないか
  • データの単位を記載しているか
  • 機種依存文字を使用していないか
  • データが分断されていないか
  • 1シートに複数の表が掲載されていないか

データの入力の基本は、1行に1ケースの情報が入っている、過不足のない1つのデータセットを作ることといえるでしょう。

整然データ(Tidy Data)の4原則

同様に、計算機にとって分析しやすいデータの形について、Hadley Wickhamが提唱したのが整然データ(Tidy Data)という考え方です。整然データとは、次の4つの特徴を持ったデータ形式のことを指します:

  1. 個々の変数(variable)が1つの列(column)をなす
  2. 個々の観測(observation)が1つの行(row)をなす
  3. 個々の観測の構成単位の類型(type of observational unit)が1つの表(table)をなす
  4. 個々の値(value)が1つのセル(cell)をなす

この形式のデータであれば、計算機が変数と値の対応構造を把握しやすく、分析しやすいデータになります。データハンドリングの目的は、混乱している雑多なデータを、利用しやすい整然データの形に整えることであると言っても過言ではありません

ロング型とワイド型の使い分け

さて、ここでよく考えてみると、変数名も一つの変数だと考えることに気づきます。

ワイド型とロング型

同じ情報でも、データの「持ち方」によって扱いやすさが変わります。

ワイド型(横持ち):

午前 午後 夕方 深夜
東京
大阪
福岡

人間が見やすい形式ですが、「大阪・夕方の天気」を参照するには行と列の両方のラベルが必要です。

ロング型(縦持ち):

地域 時間帯 天候
東京 午前
東京 午後
東京 夕方
大阪 午前
大阪 午後
大阪 夕方

このデータが表す情報は同じですが、大阪・夕方の条件を絞り込むことは行選択だけでよく、計算機にとって使いやすいのです。この形式をロング型データ、あるいは「縦持ち」データといいます。これに対して前者の形式をワイド型データ、あるいは「横持ち」データといいます。

ロング型データの利点

ロング型データにする利点のひとつは、欠測値の扱いです。ワイド型データで欠測値が含まれる場合、その行あるいは列全体を削除するのは無駄が多く、かと言って行・列両方を特定するのは技術的にも面倒です。これに対しロング型データの場合は、当該行を絞り込んで削除するだけで良いのです。

また、ggplot2で色分けやfacet分割をする際はロング型が便利です。aes(color = 変数名)のように、色分けの基準となる変数を1つの列として持つ必要があるため、ロング型の方が扱いやすくなります。

tidyverse(正確にはtidyr)には、このようなロング型データ、ワイド型データの変換関数(pivot_longer()pivot_wider())が用意されています。


ランクC: 基礎知識を確認しよう

8-C-1

整然データ(Tidy Data)では、1つの変数が1つの列に対応する。

これは整然データの第1の特徴です。変数が複数の列に分散していると、分析が困難になります。


8-C-2

ロング型データはワイド型データより常に優れている。

どちらが優れているかは用途によります。人間が見て理解する場合や、クロス集計表を作る場合はワイド型が便利です。一方、プログラムで処理する場合や、ggplot2で可視化する場合はロング型が扱いやすいです。


8-C-3

次のワイド型データを見てください。これは整然データ(Tidy Data)の条件を満たしていますか?

午前 午後 夕方
東京
大阪

このデータは整然データの条件を満たしていません。なぜなら、「時間帯」という変数が複数の列(午前・午後・夕方)に分散しているからです。各観測(例:東京・午前・晴)が1行になっていないため、整然データではありません。


8-C-4

ワイド型データをロング型に変換するtidyr関数は次のうちどれですか?

pivot_longer()は、複数の列を「name(変数名)」と「value(値)」の2列に集約する関数です。spread()gather()は古い関数で、現在はpivot_wider()pivot_longer()の使用が推奨されています。


8-C-5

次のコードを見てください。

iris %>% pivot_longer(-Species)

ここで-Speciesは何を意味していますか?

select()関数と同様、-(マイナス)は「除外」を意味します。つまりSpecies列は軸として残し、それ以外の列(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)をロング化します。


8-C-6

pivot_wider()で、元の変数名がどの列に入っているかを指定するパラメータは次のうちどれですか?

pivot_wider(names_from = 列名, values_from = 列名)という形で使います。names_fromで指定した列の値が新しい列名になり、values_fromで指定した列の値がセルの中身になります。


8-C-7

ggplot2で、種別ごとに色を変えた散布図を描きたい場合、データはワイド型とロング型のどちらが扱いやすいですか?

ggplot2ではaes(color = 変数名)のように、色分けの基準となる変数を1つの列として持つ必要があります。したがってロング型の方が扱いやすいです。ワイド型の場合、複数の列を指定することになり、コードが複雑になります。


8-C-8

次のデータを見てください。

# A tibble: 3 × 4
     ID  国語  数学  英語
  <int> <dbl> <dbl> <dbl>
1     1    80    75    90
2     2    70    85    80
3     3    90    65    70

このデータをpivot_longer(-ID)でロング化すると、結果は何行になりますか?

元のデータは3行です。ID列を軸として、3つの科目列(国語・数学・英語)をロング化するので、3行 × 3科目 = 9行になります。


ランクB: 実践スキルを磨こう

8-B-1

irisデータセットをロング型に変換してください。Species列を軸として、それ以外の列(4つの測定値)をnamevalueの2列にまとめてください。

pivot_longer()関数を使います。第一引数は「ロング化したい列」を指定しますが、-Speciesとすれば「Species以外」を意味します。

iris %>%
  pivot_longer(-Species)

結果は600行(150個体 × 4測定値)のロング型データになります。name列に測定値の名前(Sepal.Length等)、value列に実測値が入ります。


8-B-2

8-B-1で作ったロング型データを、元のワイド型に戻してください。ただし、IDがないため正しく戻らない場合があることに注意してください。

pivot_wider()を使います。names_fromvalues_fromのパラメータが必要です。また、個体を識別するIDがない場合、同じSpeciesの個体が混ざってしまう可能性があります。

元のirisデータには個体IDがありません。そのため、pivot_longerでロング化した後にpivot_widerで戻すと、Speciesごとに値が集約されてしまいます。これを防ぐには、最初にrowid_to_column("ID")で行番号を追加する必要があります。

正しい方法(IDを付与する):

iris %>%
  rowid_to_column("ID") %>%
  pivot_longer(-c(ID, Species)) %>%
  pivot_wider(id_cols = c(ID, Species), names_from = name, values_from = value)

失敗する方法(参考):

# これだとSpeciesごとに集約されてしまう
iris %>%
  pivot_longer(-Species) %>%
  pivot_wider(names_from = name, values_from = value)

個体を識別するIDがないと、pivot_wider()は同じSpeciesの値をまとめてしまいます。この問題から、ワイド⇔ロング変換には個体IDが必要であることを学んでください。


8-B-3

BaseballDecade.csvを読み込み、年度(Year)と球団(team)ごとの平均年俸(salary)を集計して、ワイド型の表を作成してください。行が年度、列が球団名になるようにしてください。

手順: 1. read_csv()でデータを読み込む 2. group_by(Year, team)でグループ化 3. summarise(mean_salary = mean(salary, na.rm = TRUE))で平均を計算 4. pivot_wider(names_from = team, values_from = mean_salary)でワイド化

library(tidyverse)

df <- read_csv("../data/BaseballDecade.csv")

df_wide <- df %>%
  group_by(Year, team) %>%
  summarise(mean_salary = mean(salary, na.rm = TRUE), .groups = "drop") %>%
  pivot_wider(names_from = team, values_from = mean_salary)

df_wide

結果は年度が行、球団名が列のワイド型の表になります。この形式は人間が見やすいですが、ggplot2で可視化する場合はロング型の方が便利です。


8-B-4

8-B-3で作成したワイド型の表を、再びロング型に戻してください。年度(Year)を軸として、球団名と平均年俸の2列にしてください。

df_long <- df_wide %>%
  pivot_longer(-Year, names_to = "team", values_to = "mean_salary")

df_long

pivot_longer()names_toで新しい列名(球団名)を、values_toで値の列名(平均年俸)を指定できます。デフォルトではnamevalueですが、明示的に指定することで意味が分かりやすくなります。


8-B-5

irisデータをロング型にし、name列(測定値の種類)ごとに分けた箱ひげ図を描いてください。facet_wrap(~name)を使って、4つの測定値を別々のパネルに表示してください。

ロング型にすることで、name列(測定値の種類)でfacet分割できます。scales = "free_y"を指定すると、各パネルのY軸スケールが独立します。

iris %>%
  pivot_longer(-Species) %>%
  ggplot(aes(x = Species, y = value, fill = Species)) +
  geom_boxplot() +
  facet_wrap(~name, scales = "free_y") +
  labs(title = "アヤメ3種の測定値比較", y = "測定値 (cm)")

ロング型にすることで、複数の測定値を一度にfacet表示できます。ワイド型のままだと、各測定値ごとに個別のggplotコードを書く必要があり、非効率です。これがロング型の威力です。


8-B-6

BaseballDecade.csvから、年度(Year)と球団(team)ごとに、平均年俸(mean)と標準偏差(sd)の両方を計算し、ワイド型の表にまとめてください。列名は「球団名_mean」「球団名_sd」のようになるはずです。

summarise()で複数の統計量を計算した後、pivot_longer()で一旦ロング化し、names_sepnames_toオプションを使って整形してからpivot_wider()でワイド化する方法があります。またはpivot_wider()names_glueオプションを活用します。

df <- read_csv("../data/BaseballDecade.csv")

# 方法1: pivot_wider()のnames_gluを使う方法
df_wide1 <- df %>%
  group_by(Year, team) %>%
  summarise(
    mean_salary = mean(salary, na.rm = TRUE),
    sd_salary = sd(salary, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  pivot_longer(-c(Year, team), names_to = "stat", values_to = "value") %>%
  pivot_wider(
    id_cols = Year,
    names_from = c(team, stat),
    values_from = value,
    names_sep = "_"
  )

df_wide1

この問題は少し複雑です。一旦ロング化してから、teamstat(mean/sd)の組み合わせで列名を作ります。names_sep = "_"で「Giants_mean_salary」のような列名になります。

複数の統計量をワイド型にまとめる操作は、集計表を作る際によく使われます。


ランクA: AI協働に挑戦しよう

8-A-1

課題: BaseballDecade.csvに含まれるワイド型データ(年度×球団の平均年俸)を使って、球団ごとの年俸推移を折れ線グラフで可視化したいと考えています。

あなたがAI(ChatGPT、Claude、GitHub Copilotなど)に指示を出して、この可視化を実現してください。AIとの対話を通じて:

  1. どのようなデータ変換が必要か(ワイド→ロングなど)
  2. どのようなggplot2のコードが適切か

を明確にし、実際に動作するコードを完成させてください。

提出物: AIとの対話のスクリーンショット、または完成したコードとグラフ。


8-A-2

課題: irisデータセットには、3種のアヤメについて4つの測定値(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)が記録されています。

種ごとに測定値の分布を比較できる図を描きたいと思います。どのような図が適切か考え、AIに相談しながら実現してください。

提出物: AIとの対話ログ、完成した図、どのような意図でその図を選んだかの説明。


8-A-3

課題: BaseballDecade.csvから、年度×球団ごとの複数統計量(平均年俸・標準偏差・最大値・最小値)をワイド型の表にまとめたいと思います。その後、年度ごとの球団間ばらつき(標準偏差など)を可視化したいと考えています。

AIに相談しながらこれを実現してください。データ整形の手順が複雑になるため、どのように問題を分解してAIに伝えるかが重要です。

提出物: - AIとの対話の記録(どのように問題を分解して伝えたか) - 完成したワイド型の表(一部でOK) - 可視化した図 - うまくいかなかった点、学んだ点


まとめ

このユニットでは、ワイド型とロング型の変換を学びました。データの「持ち方」を自由に変えられるようになると:

  • 可視化の自由度が上がる
  • 集計やフィルタリングが簡単になる
  • 欠測値の扱いが楽になる

といったメリットがあります。tidyrのpivot_longer()pivot_wider()は、データ分析の基礎スキルです。繰り返し練習して、自分の手に馴染ませましょう。

次のユニット: U9. 変数の型の変換と統一では、カテゴリ変数のワンホットエンコーディングや標準化について学びます。


進捗: あなたは今 8-C-8 まで完了しました!(と仮定)次は 8-B-1 に進みましょう。