6 Rを用いたデータの前処理

6.2 前処理とは何か

統計分析と言うと、難しい統計モデルを組んだり、華やかなグラフを作成したりする場面を想像するかもしれません。しかし、データ分析の作業時間のうちのかなりの部分が「データの前処理」という、一見地味な作業に費やされていると言われています。

データの前処理とは、集めてきたままの「生」のデータを、分析しやすいようにきれいに整え、使いやすい形に加工することです。一般的なデータ分析は、以下のような流れで進みます。前処理は、データを手に入れてから本格的な分析を始めるまでの間の段階に位置します。

  1. データの取得:実験や調査、Webサイトからの収集、既存のデータベースなどからデータを手に入れます。
  2. データの前処理:データの統合や、欠損値や表記揺れの修正、データ形式の整形などを行います。
  3. 可視化や統計解析:統計分析のメインとなる作業です。
  4. レポート・共有:分析結果を他の人に分かりやすく伝え、意思決定などに役立てます。

前処理がなぜ重要なのでしょうか。その理由は、「Garbage In, Garbage Out」(ゴミを入れたら、ゴミしか出てこない)という、データ分析の世界で有名な言葉によって表現されています。

どれほど高度で洗練された分析手法を使っても、元となるデータが不正確だったり、整理されていなかったりすれば、そこから得られる分析結果もまた信頼性のないものになってしまいます。

例えば、

  • アンケートの「未回答」が「0点」として入力されていたら?
  • 「男性」と「男」という表記揺れがそのままだったら?
  • 分析に必要なデータが複数のファイルに分かれたままだったら?

これでは、正しい集計や分析はできません。

質の高い分析を行い、信頼できる結論を導き出すためには、その土台となるデータをクリーンな状態に整える作業である「前処理」が不可欠と言えます。

6.3 tidyverseについて

前処理も含め、Rを使ってデータを操作する際に非常に便利で強力なツールとなるのがtidyverseです。tidyverseは、Rでのデータ分析をより簡単、高速、そして直感的に行うために開発されたパッケージの集合体です。これらを使いこなすことで、Rのスクリプトを短く読みやすい形で書くことができます。tidyverseはR界隈での使用頻度が極めて高いため、Rをある程度使いこなそうとするとtidyverseを学ぶことが必須になるという印象です。

6.3.1 tidyverse とは?

tidyverseは単一のパッケージではなく、Rでのプログラミング全体をカバーするさまざまな便利パッケージの集まりです。以下のような主要パッケージが含まれています。

  • readr: CSVファイルなどのデータを高速で読み込むためのパッケージ。
  • dplyr: データの絞り込み、並べ替え、新しい変数の追加など、データ操作に用いる。
  • tidyr: データを扱いやすい「整然とした(Tidy)」形式に整形するのに役立つ。
  • ggplot2: 美しく柔軟なグラフを作成するためのパッケージ。

これらのパッケージは、Tidy Data(整然データ)という共通のデータ形式に基づいて設計されているため、相互に連携して使うことが容易になっています。

まずは以下のコードを実行して、tidyverseをインストールし、使える状態にしましょう。

install.packages("tidyverse")

また、スクリプトの冒頭に次のように記述してtidyverseを使えるようにします。

library(tidyverse)

6.3.2 パイプ演算子%>%について

tidyverseを特徴づける最も重要な要素の一つがパイプ演算子です。これは%>%と書きます。

最近のRではデフォルトの機能としてパイプ演算子が使えるようになりました。Rデフォルトのパイプ演算子は|>という記号で表記します。使い方は基本的に同じです。

パイプ演算子の最大の利点は、複数の関数を繋げて、処理の流れを一つの連続した命令として書けることです。「Aをして、次にBをして、さらにCをする」という手順を、そのままの順番でスクリプトとして書くことができるようになります。

この「繋げる」機能を実現しているのが、「左側のデータや計算結果を、右側の関数の『第1引数』に自動的に渡す」というシンプルなルールです。

例えば、mean(numbers)というコードは、numbersというデータをmean 関数の第1引数に渡して平均を計算します。これをパイプ演算子を使って書くとnumbers %>% mean() となります。numbersmean()のカッコの中に自動で挿入されるので、mean関数の引数にnumbersを指定する必要はありません。

この仕組みのおかげで、ある関数の処理結果を次の関数へと連続的に渡していくことが可能になります。

具体例として、ある数値型ベクトルに対して「絶対値を取り、対数を計算し、四捨五入して、最後に平均値を求める」という一連の計算を考えます。

パイプを使わない場合

Rの基本機能だけでこの処理を書く方法はいくつか考えられます。
一つ目は、処理のたびに結果を新しい変数に保存していく方法です。

numbers = c(1.5, -10.2, 25.8, -0.7) # 計算対象の数値型ベクトル
# 処理のたびに結果を新しい変数に保存する。
step1_abs = abs(numbers)
step2_log = log(step1_abs)
step3_round = round(step2_log)
result = mean(step3_round)
print(result)
## [1] 1.25

この書き方は手順が明確ですが、一時的な変数がたくさんできてしまうし、行数も多いです。

行数を減らしたいのであれば、関数を入れ子(ネスト)にして1行で書くこともできます。

numbers = c(1.5, -10.2, 25.8, -0.7)
result = mean(round(log(abs(numbers))))
print(result)
## [1] 1.25

このコードは、abs()log()round()mean() の順に実行されますが、プログラムを読むときは一番内側のabs()から外側に向かって解読する必要があり、読みにくいです。

パイプを使う場合

同じ処理をパイプ演算子を使って書いてみましょう。

numbers = c(1.5, -10.2, 25.8, -0.7)
numbers %>%
  abs() %>%
  log() %>%
  round() %>% 
  mean()
## [1] 1.25

パイプ演算子を使った場合「絶対値を取り、対数を計算し、四捨五入して、最後に平均値を求める」という計算の流れと同じ順序で命令を書くことができます。プログラムが書きやすく、そして読みやすくなります。この読みやすさは、後からコードを見返したり、他の人と共有したりする際にも大きなメリットになります。

なお、上記の計算は次のようにすれば一行で書くこともできます(あまり推奨はされないですが)。

numbers %>% abs() %>% log() %>% round() %>% mean()
## [1] 1.25

6.3.3 補足:第一引数を書く時と書かない時

パイプを使ったプログラムを書く際に、引数の省略ができるのはパイプ演算子に後続する場合であり、パイプより手前の段階では引数の省略をしません。この違いが混乱やミスを起こすことがあるのでここで説明しておきます。

以下の例で使用しているdplyrパッケージのmutate関数は、データフレームに新たな列を追加したい場合に使う関数です。第一引数には処理対象のデータフレームを指定し、次に追加する列の列名 = (計算式)という形で、追加したい列の内容を指定します。

# 元データ
data = data.frame(value = c(1, 2, 3, 4))
print(data)
##   value
## 1     1
## 2     2
## 3     3
## 4     4
# 列を追加
df = mutate(data, score = value * 2)
print(df)
##   value score
## 1     1     2
## 2     2     4
## 3     3     6
## 4     4     8

上記の例では、scoreという名前で新たな列を作成し、そこに元のデータフレームのvalue列を2倍した値を代入しています。

この作業をパイプを使って書くと以下のようになります。

data = data.frame(value = c(1, 2, 3, 4))
df = data %>% 
  mutate(score = value * 2)

この場合は、mutate関数においてdata変数を書き入れていないことに注意してください。

同じようにmutate関数を使っていても、それをパイプの後に使うかそうでないかで引数の書き方が変わっています。

以下の例ではmutateの後にfilter(特定の条件を満たす行を抽出する関数)を使用しています。この作業は、少なくとも2通りの書き方があり得ます。

data = data.frame(value = c(1, 2, 3, 4))
# 書き方1
df1 = data %>% 
  mutate(score = value * 2) %>%
  dplyr::filter(score > 5)
# 書き方2
df2 = mutate(data, score = value * 2) %>%
  dplyr::filter(score > 5)
# df1とdf2は同じ内容
print(df1)
##   value score
## 1     3     6
## 2     4     8
print(df2)
##   value score
## 1     3     6
## 2     4     8

書き方1の場合はmutatefilterもパイプの後に位置するのでdata変数を省略しますが、書き方2の場合はパイプの前に使用しているmutate関数ではdata変数を省略せずに書く必要があります。

書き方2ではパイプを使った一連の処理の最初の関数だけ引数の指定を通常通りに行う必要があり、それを忘れるとエラーが発生してしまいます(このミスは発生しやすい)。したがって、行数は1行多くなってしまうものの、一貫した書き方ができる書き方1の方式をいつも使うようにするとよいでしょう。

6.3.4 計算結果を変数に保存する

このようなパイプ処理の最終結果を変数に保存したい場合、アロー演算子 ->を使うことで処理の流れに沿ってプログラムを書くことができます。

numbers %>%
  abs() %>%
  log() %>%
  round() %>%
  mean() -> result

print(result)
## [1] 1.25

あるいは、通常通りに=記号を使いたい場合は次のようにしても構いません。

result = numbers %>%
  abs() %>%
  log() %>%
  round() %>%
  mean()

# 一行で書く場合はこうする
result = numbers %>% abs() %>% log() %>% round() %>% mean()

print(result)
## [1] 1.25

以上のように、パイプ演算子を使うことで行いたい作業の順番のままに処理内容を記述することができ、プログラムの書きやすさや可読性が向上します。

6.4 データの入出力

データ分析は、まず対象となるデータをRの環境に読み込むことから始まります。また、加工した後のデータを保存することも必要です。

6.4.1 CSVファイルの読み書き:read_csv()write_csv()

データ分析で最も一般的に使われるファイル形式の一つがCSV(Comma Separated Values)ファイルです。tidyverseファミリーのreadrパッケージに含まれるread_csv()write_csv()は、CSVファイルの入出力を行う際に利用する関数です。

補足:readrには、TSV(タブ区切り)ファイルを扱うread_tsv()や、セミコロンなど任意の区切り文字を指定できるread_delim()といった関数もあり、入力したいファイルの形式に合わせて使い分けます。使い方はread_csv()write_csv()とほとんど同様です。

readrパッケージの利点

Rには元々read.csv()write.csv()という基本関数があり、これを使ってファイルの入出力ができます。では、なぜreadrパッケージ関数を使ってデータの読み込みを行うのでしょうか。それには、以下のようなさまざまな利点があるからです。

  • 圧倒的な速さ:read_csv()は、Rの基本関数に比べて10倍以上高速に動作することがあります。何十万行もあるような巨大なファイルを読み込む際にはその差は歴然とします。
  • 安定した動作:列の型(数値、文字列など)をより賢く、安定して推測してくれます。
  • 進捗表示:大きなファイルの読み込み中に進捗バーが表示されるため、読み込み処理がどのくらい進んでいるか分かりやすい。

6.4.2 データの読み込み:read_csv()

はじめにサンプルデータを作成しておきます。

# サンプルデータを作成
df = data.frame(
  ID = 1:3,
  Product = c("Apple", "Orange", "Grape"),
  Price = c(150, 120, 300)
)
# dataという名前のフォルダがなければ作成する
if (!dir.exists("data")) {dir.create("data")}
# データフレームをCSVファイルとして "data" フォルダに保存
readr::write_csv(df, "data/mycsvdata.csv")

もしdataフォルダがパソコン内のどこに作成されたのか分からない場合は、Consoleで以下の命令を実行してください。そこで表示された場所にdataフォルダが作成されているはずです。

# Rが現在作業している場所を調べる関数
getwd()

次に、このデータをread_csv()関数を使って読み込みます。read_csv()は、第一引数にファイルのパスを指定して使います。ファイルの形式に合わせて、いくつかの重要な引数を指定することで、より柔軟にデータを読み込めます。

# dataフォルダにあるmy_data.csvというファイルを読み込む
df = readr::read_csv("data/mycsvdata.csv")
print(df)
## # A tibble: 3 × 3
##      ID Product Price
##   <dbl> <chr>   <dbl>
## 1     1 Apple     150
## 2     2 Orange    120
## 3     3 Grape     300

よく使う read_csv() の引数:

  • file: 必須。読み込むCSVファイルのパス。
  • col_names: 1行目を列名として扱うかを決めます。TRUEなら1行目を列名として読み込みます (デフォルト)。FALSEを指定すると1行目からデータとして読み込み、列名は X1, X2, …と自動で割り振られます。ヘッダー行がないファイルを読むときに使います。
  • skip: ファイルの先頭から指定した行数をスキップします。ファイルの冒頭にタイトルや注釈が入っている場合に便利です。例えばskip = 3とすると、最初の3行を無視して4行目から読み込みます。
  • locale: 文字コード(エンコーディング)などを指定します。日本語の文字化け対策でよく使います。

なお、readr::read_csvを使って読み込んだデータはtibbleというデータ形式になります。データフレームと基本的に同じように扱うことができます。もしデータフレーム型に変換したい場合はas.data.frame()関数を使うと良いです。

# Web上のデータを読み込む例
df = readr::read_csv("https://htsuda.net/stats/dataset/job.csv")
# tibble型をデータフレーム型に変換
df = as.data.frame(df)
# 内容の確認
head(df, 3)
##   id gender education_level score
## 1  1   male          school  5.51
## 2  2   male          school  5.65
## 3  3   male          school  5.07
エンコーディング問題:日本語の文字化けを防ぐ

日本語を含むファイルをread_csv()で読み込んだ際に、コンソールに表示される列名やデータの中身が「文字化け」(例: … や意味不明な記号になる)を起こすことがあります。これは、エンコーディング(文字コード) の違いが原因です。

多くの場合は、Shift-JIS(またはcp932)というエンコーディング形式を指定することで解決できます。

# 文字化け対策:Shift-JISでエンコードされたファイルを読み込む
df = readr::read_csv(
  file = "data/my_data_jp.csv", 
  locale = readr::locale(encoding = "Shift-JIS")
)

もしShift-JISでも解決しない場合は、ファイルの元のエンコーディングを特定する必要がありますが、Windows環境で作成されたCSVファイルの多くは、この指定で読み込めるはずです。

6.4.3 データの書き出し:write_csv()

処理が完了したデータフレームを、再びCSVファイルとして保存するには write_csv() を使います。

第1引数にファイルに保存したいデータフレームを指定し、第2引数に書き出すCSVファイルのパスを指定します。

# 書き出すためのサンプルデータを作成
df = data.frame(
  ID = c("A01", "A02", "A03"),
  Score = c(88, 55, 95)
)
# outputという名前のフォルダがなければ作成する
if (!dir.exists("output")) {dir.create("output")}
# このデータフレームをoutputフォルダ内にprocessed_data.csvという名前で保存する
readr::write_csv(df, "output/processed_data.csv")

よく使う write_csv() の引数:

  • x: 必須の引数。ファイルに保存したいデータフレームを指定します。
  • file: 必須の引数。書き出すCSVファイルのパスを指定します。
  • na: Rの欠損値 NA を、ファイル上でどのような文字列として表現するかを指定します。デフォルトは""(空文字)ですが、na = "NA"とすれば、欠損値がNAという文字列で書き出されます。
  • append: 既存のファイルに追記するかどうかを決めます。FALSEなら同じ名前のファイルがあれば上書きします(デフォルト)。TRUEを指定すると同じ名前のファイルがあればその末尾にデータを追記します。

6.4.4 Rオブジェクトの保存と読み込み:read_rds()write_rds()

CSVファイルは汎用性が高い一方で、一度Rで処理したデータフレームを保存する際には列の型情報などが失われることがあります。

もし、Rでの作業を一度中断し、後で全く同じ状態から再開したい場合は、R専用の保存形式(RDS形式)を使うのが最も確実で高速です。データフレームだけでなく、分析モデルの結果など、CSVでは保存できない複雑なオブジェクトもそのまま保存できます。

オブジェクトの保存と読み込みの例

ここでは、データフレーム、ベクトル、そして簡単な統計モデルの結果という3種類のオブジェクトを作成し、それぞれをRDSファイルとして保存・復元してみましょう。

# --- 保存するオブジェクトをいくつか作成 ---
sample_df = data.frame(ID = 1:3, Name = c("Taro", "Hanako", "Jiro"))
important_cities = c("Tokyo", "Osaka", "Nagoya")
fit_model = lm(mpg ~ wt, data = mtcars) # 分析モデル

# outputという名前のフォルダがなければ作成する
if (!dir.exists("output")) {dir.create("output")}
# --- オブジェクトをそれぞれRDSファイルに保存 ---
readr::write_rds(sample_df, "output/my_dataframe.rds")
readr::write_rds(important_cities, "output/my_cities.rds")
readr::write_rds(fit_model, "output/my_model.rds")

# --- 保存したオブジェクトを読み込んで復元 ---
restored_df = readr::read_rds("output/my_dataframe.rds")
restored_model = readr::read_rds("output/my_model.rds")

# --- 復元されたか確認 ---
print(restored_df)
summary(restored_model) # モデル情報も完全に復元される

処理に時間のかかった結果をRDS形式で保存しておくと、次回以降の作業効率が大幅に向上します。

6.5 データの統合

分析用のデータは最初から一つのファイルにまとまっているとは限りません。アンケートの回答者ごと、あるいは店舗ごとなど、複数のファイルに分割されて保存されていることがよくあります。また、アンケートの全員分の回答データと各回答者の属性データ(年齢や性別など)が別々に保存されているといった場合もよくあります。

このセクションでは、そうしたバラバラのデータを一つに統合し、分析しやすい形に整える方法を説明します。

6.5.1 複数ファイルの統合(縦方向)

アンケートの回答が回答者ごとに別のCSVファイルとして保存されている、といった状況を考えます。データ分析をする際は、これらのファイル全てを読み込んで、全員分のデータをひとつのデータフレームにまとめることが重要です。

サンプルファイルの作成

まず、このシナリオを再現するためのサンプルファイル(架空のデータ)を作成します。以下のスクリプトは、3人分のアンケート回答データを作成し、それぞれを別々のCSVファイルとしてdata_surveyフォルダに保存します。

# 必要なパッケージを読み込む
library(readr)

# "data_survey" というフォルダがなければ作成する
if (!dir.exists("data_survey")) {
  dir.create("data_survey")
}

# サンプルデータを作成
user01 = data.frame(user_id = 1, Q1 = 5, Q2 = 4, Q3 = 5)
user02 = data.frame(user_id = 2, Q1 = 2, Q2 = 3, Q3 = 4)
user03 = data.frame(user_id = 3, Q1 = 4, Q2 = 4, Q3 = 3)

# 各データをCSVファイルとして書き出す
write_csv(user01, "data_survey/user01.csv")
write_csv(user02, "data_survey/user02.csv")
write_csv(user03, "data_survey/user03.csv")

これを実行すると、data_surveyという名前のフォルダが作成され、その中に3つのCSVファイルが作成されます。

もしdata_surveyフォルダがパソコン内のどこに作成されたのか分からない場合は、Consoleで以下の命令を実行してください。そこで表示された場所にdata_surveyフォルダが作成されているはずです。

getwd()

ではここから、この3つのCSVファイルをRに読み込み、その内容をひとつのデータフレームへと統合します。

統合方法1:Rの基本機能(forループ)を使う

forループを使って1ファイルずつ読み込み、リストに追加したのち、最後に結合します。

# 1. 統合したいファイルの一覧を取得
paths = list.files(path = "data_survey", pattern = ".csv$", full.names = TRUE)
# 2. 結果を格納するための、中身が空のデータフレームを用意する
data = data.frame()
# 3. forループで1つずつファイルを読み込み、rbind()で結合していく
for (path in paths) {
  temp_data = readr::read_csv(path)
  data = rbind(data, temp_data)
}
# 結果を確認
print(data)
## # A tibble: 3 × 4
##   user_id    Q1    Q2    Q3
##     <dbl> <dbl> <dbl> <dbl>
## 1       1     5     4     5
## 2       2     2     3     4
## 3       3     4     4     3

ここで使っているlist.files()は、指定したフォルダ内にあるファイルのリストを取得する便利な関数です。

list.files関数の主な引数:

  • path: 対象となるフォルダのパスを指定する。
  • pattern: どのような名前のファイルを探すかを指定する。例の".csv$"は、「.csv という文字列で終わる」という意味で、CSVファイルのみを抽出できる。
  • full.names: ファイルへの完全なパス(フルパス)を取得するかどうかを決めます。TRUEを指定すると、"data_survey/user01.csv"のように、フォルダ名を含んだ完全なパスを返します。ファイルを読み込む際には、Rがファイルの場所を正確に知る必要があるため、通常は TRUE を指定します。デフォルトでは FALSE が指定されるようになっており、"user01.csv"のように、ファイル名だけを返します。

list.files関数を使用する際に、path引数に指定する値が間違っているとCSVファイルのパスがうまく取得できません。先述したサンプルデータ作成スクリプトでCSVファイルを生成した場合は上記のpath = "data_survey"という指定でうまくいくはずですが、もし失敗する場合や、自分のパソコン内の任意のフォルダを読み込みたい場合は、そのフォルダのパス(絶対パス)を調べて、その値をpath引数の値として指定してください。

統合方法2:tidyverseの関数を使う

以下は、purrrパッケージのmap()dplyrパッケージのbind_rows()を組み合わせる、より簡潔な方法です。数行のコードで同じ処理が実現できますが、初学者にはプログラムの意味が把握しにくいかもしれません。

# 1. ファイル一覧を取得
paths = list.files(path = "data_survey", pattern = ".csv$", full.names = TRUE)
# 2. map()で全ファイルを一括で読み込み、bind_rows()で結合する
survey_all_tidy = purrr::map(paths, readr::read_csv) %>%
  dplyr::bind_rows()
# 結果を確認
print(survey_all_tidy)
## # A tibble: 3 × 4
##   user_id    Q1    Q2    Q3
##     <dbl> <dbl> <dbl> <dbl>
## 1       1     5     4     5
## 2       2     2     3     4
## 3       3     4     4     3

ここで使用しているpurrr::map()は、リストやベクトルのすべての要素に対して、同じ処理(関数)を順番に適用し、その結果をリストにして返してくれる関数です。forループを使って記述する処理をこの関数ひとつで実行できるというイメージです。

上記の例では、pathsに含まれるそれぞれの要素に対してreadr::read_csvを実行し、その結果(読み込まれた個別の参加者のデータがリストになったもの)をbind_rows()を使ってひとつのリストに統合しています。

6.5.2 複数ファイルの統合(横方向)

ここまでの統合作業は、同じ形式のデータ(データフレーム)が複数あり、それらを縦方向に結合するという例でした。

ここからは、異なる情報を持つデータフレーム同士を、共通の列(キー変数)を手がかりに横に結合する方法を説明します。具体例として、「アンケートの回答データ」と「回答者の属性データ」を、共通の user_id をキーとして結合するケースを考えます。

サンプルデータの準備

まず、結合する2つのデータフレームを用意します。answersには回答データが、attributesには回答者の属性情報(年齢と性別)が含まれています。

library(dplyr)
# アンケート回答データ
answers = data.frame(
  user_id = c("U001", "U002", "U003", "U004"),
  Q1 = c(5, 3, 4, 5),
  Q2 = c(4, 2, 5, 4)
)
# 回答者属性データ
attributes = data.frame(
  user_id = c("U001", "U002", "U003", "U005"),
  age = c(25, 41, 33, 52),
  gender = c("Male", "Female", "Female", "Male")
)

ここで用意したデータは部分的にデータの欠け(未記載)があります。データ全体でみれば、U001からU005までの5人の人物がいるのですが、アンケート回答データにはU005の人の回答データが記録されておらず、回答者属性データにはU004の人のデータがありません(このような不完全さは実際のデータではよくあることです)。

それでは、この2つのデータをひとつのデータフレームに統合しましょう。このような場面で使われる関数としてleft_join()inner_join()full_join()などがあります。

Join系関数の主な引数

これから紹介するjoin系の関数は、共通して以下のような引数を取ります。

  • x: 第1引数であり、結合の基準となる左側のデータフレーム。
  • y: 第2引数であり、結合する右側のデータフレーム。
  • by: 結合のキーとなる列名を指定します。by = "user_id"のように列名を文字列で指定します。もしキーとなる列名が両方のデータフレームで異なる場合は、by = c("id_A" = "id_B")のように指定します。

left_join関数を使ったデータの統合

left_join関数は、左側(x)のデータフレームを基準にして結合します。左側のデータフレームの行はすべて保持され、キー変数が一致する右側(y)のデータフレームの情報が追加されます。右側に一致するキーがない場合、その行の新しい列は NA(欠損値)になります。

「主となるデータに、補足的な情報を付け加えたい」という場合に最もよく使われます。

data = left_join(answers, attributes, by = "user_id")
print(data) # 結果を確認
##   user_id Q1 Q2 age gender
## 1    U001  5  4  25   Male
## 2    U002  3  2  41 Female
## 3    U003  4  5  33 Female
## 4    U004  5  4  NA   <NA>

左側(answers)のデータに含まれるU001からU004までの4人分のデータに対して、そこに右側(attributes)のデータから来た属性情報が追加されています。右側のデータだけに存在するU005のデータは統合後のデータには含まれません。

inner_join関数を使ったデータの統合

inner_join関数は、「両方のデータフレームにキー変数が存在する行」のみを保持します。どちらか一方にしか存在しないキーを持つ行は、結果から除外されます。

「両方のデータに共通して存在する情報だけを抽出したい」という場合に役立ちます。

data = inner_join(answers, attributes, by = "user_id")
print(data) # 結果を確認
##   user_id Q1 Q2 age gender
## 1    U001  5  4  25   Male
## 2    U002  3  2  41 Female
## 3    U003  4  5  33 Female

入力した両方のデータに共通して含まれるU001, U002, U003の3人分のデータだけが統合後のデータには含まれます。

full_join関数を使ったデータの統合

full_join関数は、両方のデータフレームに存在するすべての行を保持します。片方のデータフレームにしかキーが存在しない行も結果に含まれ、対応するデータがない箇所はNAで埋められます。

「どちらかのデータにでも情報が存在するものは、すべて残しておきたい」という場合に便利です。

data = full_join(answers, attributes, by = "user_id")
print(data) # 結果を確認
##   user_id Q1 Q2 age gender
## 1    U001  5  4  25   Male
## 2    U002  3  2  41 Female
## 3    U003  4  5  33 Female
## 4    U004  5  4  NA   <NA>
## 5    U005 NA NA  52   Male

5人分すべてのデータが残っており、U004とU005について足りない部分のデータはNAになっています。

まとめ:どのJoinを使えばいい?

どのJoin関数を使うかは、「どの行を残したいか」で決まります。

  • left_join: 主データが基準であり、そこに補足情報を追加したい。
    左側のデータフレームの行はすべて残ります。大抵の場合はこの関数を使えばOKです。
  • inner_join: 両方のデータに共通して存在する行だけが欲しい。
    この場合、統合後のデータに欠損値(NA)がない状態になります。
  • full_join: とにかくすべての情報を残したい。
    どちらかのデータフレームにでも存在する行はすべて残ります。全ての情報を保持したい場合に使います。

6.6 データの値の調整

データを読み込んだら、次はその「中身」を分析に適した形に整えていきます。不適切な値や形式のままでは、正しい分析はできません。以下のトピックを順に説明します。

  • 欠損値の正しい記録
  • 文字列処理
  • データ型の設定
  • 有効数字の統制

6.6.1 欠損値の正しい記録

分析データにおいて、値が存在しない欠損値はNAという特別な値で表現されるべきです。しかし、元のデータにおいてアンケートの「無回答」の箇所が -1 や 999 など、特定の値を使って記録されていることがよくあります。これらをNAに統一しておかないと、平均値などの計算が狂ってしまう原因になります。

# 4人分の得点データで、3人目のスコアは欠損値。
data = data.frame(id = 1:4, score = c(85, 92, 999, 77))
print(data)
##   id score
## 1  1    85
## 2  2    92
## 3  3   999
## 4  4    77
data$score[data$score == 999] = NA
print(data)
##   id score
## 1  1    85
## 2  2    92
## 3  3    NA
## 4  4    77

tidyverse的にこれと同じ処理をする場合は、dplyrmutate()na_if()を使います。na_if(列, 条件の値)は、列の値が条件の値と一致した場合にNAに置き換える関数です。

data = data.frame(id = 1:4, score = c(85, 92, 999, 77))
data %>%
  dplyr::mutate(score = dplyr::na_if(score, 999)) -> data
print(data)
##   id score
## 1  1    85
## 2  2    92
## 3  3    NA
## 4  4    77

6.6.2 文字列処理

文字を含むデータには、以下のような問題がある場合があります。

  • 表記揺れ
    「男性」と「男」という表記が混在しているようなケース。どちらかの表記に統一したい。
  • 不要な文字がある
    値段が"1000円"などと記録されているケース。"円"を消して1000という数値型データにしたい。
  • ひとつの列に複数の情報がまとめられている
    住所が"東京都-渋谷区"などと記録されているケース。都道府県と市区町村の2列に分割したい。

stringrパッケージの紹介

文字列を処理する際には、tidyverseファミリーのstringrパッケージが便利です。ここでは文字の検索と置換を行うstr_replace_all()を例に見てみましょう。

関数 str_replace_all(string, pattern, replacement)

  • string: 対象となる文字列データ
  • pattern: 探し出す文字列
  • replacement: 置換後の文字列

例えば、電話番号の区切り文字-/に置換したい場合、以下のように書くことができます。

numbers = c("090-1234-5678", "080-9876-5432")
numbers2 = str_replace_all(numbers, "-", "/")
print(numbers2)
## [1] "090/1234/5678" "080/9876/5432"

strinrパッケージにはこれ以外にも文字列処理に関連するさまざまな関数があります。こちらのページなどが参考になるでしょう。https://heavywatal.github.io/rstats/stringr.html

表記揺れの修正

以下の例では、gender列の"男"を"男性"に統一し、表記揺れをなくします。

data = data.frame(id = 1:3, gender = c("男性", "女性", "男")) # 表記揺れのあるデータ
data$gender[data$gender == "男"] = "男性"
print(data)
##   id gender
## 1  1   男性
## 2  2   女性
## 3  3   男性

tidyverse的に書く場合、dplyrrecode()関数が便利です。recode(列, "元の値" = "新しい値")のように書きます。

data = data.frame(id = 1:3, gender = c("男性", "女性", "男")) # 表記揺れのあるデータ
data = data %>%
  dplyr::mutate(gender = dplyr::recode(gender, "男" = "男性"))
print(data)
##   id gender
## 1  1   男性
## 2  2   女性
## 3  3   男性

不要な文字の削除

価格や数値データが文字列として入力されている際、"円",といった記号が含まれていると、後で数値に変換できず計算に使えません。stringr::str_replace_all()を使ってこれらの不要な文字を削除しましょう。

data = data.frame(price = c("1,500円", "800円", "3,200円"))
data$price = str_replace_all(data$price, "円", "") # 円を消す
data$price = str_replace_all(data$price, ",", "") # ,を消す
data$price = as.numeric(data$price) # 文字列型から数値型への変換
print(data)
##   price
## 1  1500
## 2   800
## 3  3200

tidyverse的に書く場合は次のようにします。

data = data.frame(price = c("1,500円", "800円", "3,200円"))
data = data %>%
  mutate(
    price = str_replace_all(price, "円", ""),
    price = str_replace_all(price, ",", "")
  )
print(data)
##   price
## 1  1500
## 2   800
## 3  3200

文字列の分割

一つの列に「都道府県-市区町村」のように複数の情報が結合されている場合、分析しやすいように別々の列に分割します。この処理にはtidyrパッケージのseparate()関数が便利です。

separate(data, col, into, sep)

  • data: 対象のデータフレーム
  • col: 分割したい列名
  • into: 新しい列名のベクトル(例: c("prefecture", "city")
  • sep: 区切り文字(例: "-"
data = data.frame(location = c("東京都-渋谷区", "大阪府-大阪市", "北海道-札幌市"))
data = tidyr::separate(
  data, col = location, into = c("prefecture", "city"), sep = "-")
print(data)
##   prefecture   city
## 1     東京都 渋谷区
## 2     大阪府 大阪市
## 3     北海道 札幌市

tidyverse的に書く場合は次のようにします。

data = data.frame(location = c("東京都-渋谷区", "大阪府-大阪市", "北海道-札幌市"))
data = tidyr::separate(data, location, into = c("prefecture", "city"), sep = "-")
print(data)
##   prefecture   city
## 1     東京都 渋谷区
## 2     大阪府 大阪市
## 3     北海道 札幌市

6.6.3 データ型の設定

数値であるべき列が文字列として扱われている場合、as.numeric()で数値型に変換します。

data = data.frame(price = c("1500", "800", "3200"))
data$price = as.numeric(data$price) # 文字列型から数値型への変換
print(data)
##   price
## 1  1500
## 2   800
## 3  3200

また、満足度のように順序に意味があるデータは、順序付きの因子(factor)型に変換します。

data = data.frame(
  satisfaction = c("満足", "不満", "普通", "満足")
)
data$satisfaction = factor(
  data$satisfaction,
  levels = c("不満", "普通", "満足"), # ここで順序を定義する
  ordered = TRUE
)
print(data$satisfaction)
## [1] 満足 不満 普通 満足
## Levels: 不満 < 普通 < 満足

6.6.4 有効数字の統制

計算結果などで生じた、小数点以下の桁数が多すぎる数値をround()で丸め、データをすっきりと見やすくします。

data = data.frame(ratio = c(1/3, 2/3, 1/6))
print(data)
##       ratio
## 1 0.3333333
## 2 0.6666667
## 3 0.1666667
data$ratio = round(data$ratio, digits = 2) # 小数点以下2桁まで残して四捨五入
print(data)
##   ratio
## 1  0.33
## 2  0.67
## 3  0.17

6.7 データの構造を整える

データの値そのものをきれいにするだけでなく、分析の目的に合わせてデータの「構造」を整えることも非常に重要です。既存の列から新しい意味を持つ列を作成したり、整然データの考え方に基づいてデータの表現形式を整えることで、集計や可視化が格段に行いやすくなります。

6.7.1 新たな変数の作成

元のデータにある列を組み合わせたり、条件に応じて値を割り振ったりすることで、分析に役立つ新しい変数(列)を作成します。

具体例として、商品データに「単価」と「数量」の列があるとします。この2つを掛け合わせて、「合計金額」という新しい列を作成してみましょう。

# 単価と数量のデータ
data = data.frame(
  product = c("Apple", "Orange", "Banana"),
  price = c(150, 120, 80),
  quantity = c(10, 15, 20)
)
# 合計金額の列を追加
data$total = data$price * data$quantity
print(data)
##   product price quantity total
## 1   Apple   150       10  1500
## 2  Orange   120       15  1800
## 3  Banana    80       20  1600

tidyverse的に書く場合は、dplyrのmutate()関数を使います。mutate()は、データフレームに新しい列を追加したり、既存の列を上書きしたりするための関数です。

# 単価と数量のデータ
data = data.frame(
  product = c("Apple", "Orange", "Banana"),
  price = c(150, 120, 80),
  quantity = c(10, 15, 20)
)
# 合計金額の列を追加
data = dplyr::mutate(data, total_price = price * quantity)
print(data)
##   product price quantity total_price
## 1   Apple   150       10        1500
## 2  Orange   120       15        1800
## 3  Banana    80       20        1600

6.7.2 条件分岐による変数作成(ビニング)

たとえば、年齢データを使って、「20代」「30代」「40代以上」といった年代のグループ分けをすることができます。このように、連続的な数値をいくつかの区間(ビン)に区切ってカテゴリ変数にすることをビニングと呼びます。

このような作業に使える関数としてcut()関数があります。breaks引数で区切り位置を、labels引数でそれぞれの区間に付ける名前を指定します。なお、Infは「無限大」を意味し、「〜以上」のような上限のない区間を指定する際に便利です。

# ユーザーの年齢データ
data = data.frame(
  user_id = 1:6, age = c(24, 31, 45, 18, 28, 72)
)
# cut()で年齢を4つのカテゴリに分ける
data$age_group = cut(
  data$age,
  breaks = c(0, 19, 29, 39, 49, Inf), # 区切り: 0-19, 20-29, 30-39, 40-49, 50-Inf
  labels = c("10代以下", "20代", "30代", "40代", "50代以上"), # 各区間のラベル名
  right = TRUE # 区間を「(a, b]」(aより大きくb以下)とするか。デフォルトはTRUE
)
print(data)
##   user_id age age_group
## 1       1  24      20代
## 2       2  31      30代
## 3       3  45      40代
## 4       4  18  10代以下
## 5       5  28      20代
## 6       6  72  50代以上

dplyrのcase_when()関数もビニングを行う際に便利です。不等号記号を使って条件を記述できるので、cut()よりもわかりやすく条件指定を書くことができます。

data = data.frame(
  user_id = 1:6, age = c(24, 31, 45, 18, 28, 72)
)
data = dplyr::mutate(
  data,
  age_group = case_when(
    age < 20 ~ "10代以下",
    age < 30 ~ "20代",
    age < 40 ~ "30代",
    age < 50 ~ "40代",
    TRUE ~ "50代以上"))
print(data)
##   user_id age age_group
## 1       1  24      20代
## 2       2  31      30代
## 3       3  45      40代
## 4       4  18  10代以下
## 5       5  28      20代
## 6       6  72  50代以上

6.7.3 データの記録形式と整然データ

データの値そのものをきれいにするだけでなく、分析の目的に合わせてデータの「構造」を整えることも非常に重要です。このセクションでは整然データという考え方と、そのためのデータ形式の変換方法を説明します。

整然データ(Tidy Data)という考え方

データ分析をスムーズに進めるためには、データが「整理整頓された状態」であることが理想です。その理想的な状態を定義したのが、整然データ(Tidy Data)というコンセプトです。

整然データには、以下の3つのシンプルなルールがあります。

  • 各変数は、それぞれ独立した列をなす。
    (例:「生徒ID」「科目」「点数」の列がある)
  • 各観測は、それぞれ独立した行をなす。
    (例:1行には「A君の国語の点数」という1つの事実だけが記録されている)
  • 各値は、それぞれ独立したセルをなす。
    (例:1つのセルに「80点」という1つの値だけが入っている)

一言でいえば、「1つの行が1つの観測、1つの列が1つの変数を表す、シンプルで機械が読みやすい形式」のことです。

なぜ整然データが重要なのかといえば、dplyr や ggplot2 をはじめとする tidyverse のパッケージ群は、データが整然データ形式であることを前提に設計されているからです。データが整然としていれば、tidyverse の関数を使って、データの集計、加工、そして可視化(グラフ作成)などの作業を即座に実行することができます。

ワイド形式とロング形式

整然データの考え方を理解するために、代表的な2つのデータ形式、ワイド形式とロング形式を見ていきましょう。

  • ワイド形式 (Wide Format): 人間が見やすい形式。各観測対象(例:一人の生徒)が1行となり、測定項目(例:英語の点数、数学の点数)が横に列として広がります。
  • ロング形式 (Long Format): Rや多くの分析ツールが扱いやすい、整然データの形式。1行が「1観測対象の1測定項目」を表します。

具体例として、「4人の生徒の、4科目のテスト結果」という情報を、両方の形式で見てみましょう。

# ワイド形式のデータフレームを作成
scores_wide = data.frame(
  student = 1:4,
  Science = c(88, 92, 75, 85),
  Math = c(78, 88, 82, 90),
  English = c(72, 95, 80, 78),
  Art = c(90, 85, 88, 92)
)
print("--- ワイド形式 ---")
## [1] "--- ワイド形式 ---"
print(scores_wide)
##   student Science Math English Art
## 1       1      88   78      72  90
## 2       2      92   88      95  85
## 3       3      75   82      80  88
## 4       4      85   90      78  92

この形式の場合、ある生徒の4科目分の結果が1行にまとめられており、人が見て理解しやすい状態になっています。

次はロング形式の例です。

# ロング形式のデータフレームを作成
scores_long = data.frame(
  student = rep(1:4, each = 4),
  subject = rep(c("Science", "Math", "English", "Art"), times = 4),
  score = c(88, 78, 72, 90, 92, 88, 95, 85, 75, 82, 80, 88, 85, 90, 78, 92)
)
print("--- ロング形式 ---")
## [1] "--- ロング形式 ---"
print(scores_long)
##    student subject score
## 1        1 Science    88
## 2        1    Math    78
## 3        1 English    72
## 4        1     Art    90
## 5        2 Science    92
## 6        2    Math    88
## 7        2 English    95
## 8        2     Art    85
## 9        3 Science    75
## 10       3    Math    82
## 11       3 English    80
## 12       3     Art    88
## 13       4 Science    85
## 14       4    Math    90
## 15       4 English    78
## 16       4     Art    92

ロング形式では、ひとつの行には「1つの観測」(ある学生のある科目の点)だけが記載されています。

以上のように、ワイド形式は横方向にワイドな広がりを持ち、ロング形式は縦方向にロングな形になります。一見するとワイド形式の方が分かりやすいのに、なぜわざわざロング形式を使うのでしょうか? それは、ロング形式がプログラムにとって非常に都合の良い形だからです。

科目ごとに集計したり、科目ごとにグラフの色分けをしたりといった処理をする際に、ロング形式では subject 列にその情報が明示的に表現されています。もし歴史の点もあとで追加するなどして科目の数が増えた場合、ロング形式では subject 列に新たな値が増えるだけですが、ワイド形式では列の数を増やすことになり、データの構造(列の数)が変化してしまいます。ロング形式はデータを一貫したやり方で保持することができ、それはプログラミング言語の内部的な処理にとって都合がよいのです。

Rで統計解析をしたりグラフを作成する際は、基本的に、データをロング形式(整然データ)で用意する必要があります。元データがはじめからロング形式になっていれば問題ありませんが、元データがワイド形式で記録されている場合は、ワイド形式からロング形式への変換作業が必要です。

実際の変換作業はtidyrパッケージの関数を使うだけで簡単に実行できます。

ワイド形式からロング形式への変換

tidyrのpivot_longer()を使います。主な引数:

  • cols: 縦にしたい列を指定します。
  • names_to: 新しい「項目名」の列の名前を決めます。colsで指定した元の列名(例: "Science", "Math")が、この新しい列の値になります。
  • values_to: 新しい「値」の列の名前を決めます。
# ワイド形式の元データ
data = data.frame(
  student = 1:4,
  Science = c(88, 92, 75, 85),
  Math = c(78, 88, 82, 90),
  English = c(72, 95, 80, 78),
  Art = c(90, 85, 88, 92)
)
print(data)
##   student Science Math English Art
## 1       1      88   78      72  90
## 2       2      92   88      95  85
## 3       3      75   82      80  88
## 4       4      85   90      78  92
# pivot_longerを使ってロング形式にする
data_long = data %>%
  tidyr::pivot_longer(
    cols = c(Science, Math, English, Art), # ロング形式にしたい列
    names_to = "subject", # データのカテゴリを表現する新しい列の列名
    values_to = "score" # 値を入れる新しい列の列名
  )
print(data_long)
## # A tibble: 16 × 3
##    student subject score
##      <int> <chr>   <dbl>
##  1       1 Science    88
##  2       1 Math       78
##  3       1 English    72
##  4       1 Art        90
##  5       2 Science    92
##  6       2 Math       88
##  7       2 English    95
##  8       2 Art        85
##  9       3 Science    75
## 10       3 Math       82
## 11       3 English    80
## 12       3 Art        88
## 13       4 Science    85
## 14       4 Math       90
## 15       4 English    78
## 16       4 Art        92

ロング形式からワイド形式への変換

分析が終わった後などに、人間が見やすいワイド形式に戻したい場合もあります。

tidyrのpivot_wider()を使います。主な引数

  • names_from: 新しい列名として使いたい値が入っている列を指定します。この列のユニークな値(例: "Science", "Math")が、新しい列名になります。
  • values_from: 新しく作られる列のセルに入る値が格納されている列を指定します。
# ロング形式の元データ
data = data.frame(
  student = rep(1:4, each = 4),
  subject = rep(c("Science", "Math", "English", "Art"), times = 4),
  score = c(88, 78, 72, 90, 92, 88, 95, 85, 75, 82, 80, 88, 85, 90, 78, 92)
)
print(data)
##    student subject score
## 1        1 Science    88
## 2        1    Math    78
## 3        1 English    72
## 4        1     Art    90
## 5        2 Science    92
## 6        2    Math    88
## 7        2 English    95
## 8        2     Art    85
## 9        3 Science    75
## 10       3    Math    82
## 11       3 English    80
## 12       3     Art    88
## 13       4 Science    85
## 14       4    Math    90
## 15       4 English    78
## 16       4     Art    92
# pivot_widerを使ってワイド形式にする
data_wide = data %>%
  tidyr::pivot_wider(
    names_from = "subject", # ここで指定した列が横に広がる
    values_from = "score"   # 値を表現している列の列名
  )
print(data_wide)
## # A tibble: 4 × 5
##   student Science  Math English   Art
##     <int>   <dbl> <dbl>   <dbl> <dbl>
## 1       1      88    78      72    90
## 2       2      92    88      95    85
## 3       3      75    82      80    88
## 4       4      85    90      78    92