6 Rを用いたデータの前処理
6.2 前処理とは何か
統計分析と言うと、難しい統計モデルを組んだり、華やかなグラフを作成したりする場面を想像するかもしれません。しかし、データ分析の作業時間のうちのかなりの部分が「データの前処理」という、一見地味な作業に費やされていると言われています。
データの前処理とは、集めてきたままの「生」のデータを、分析しやすいようにきれいに整え、使いやすい形に加工することです。一般的なデータ分析は、以下のような流れで進みます。前処理は、データを手に入れてから本格的な分析を始めるまでの間の段階に位置します。
- データの取得:実験や調査、Webサイトからの収集、既存のデータベースなどからデータを手に入れます。
- データの前処理:データの統合や、欠損値や表記揺れの修正、データ形式の整形などを行います。
- 可視化や統計解析:統計分析のメインとなる作業です。
- レポート・共有:分析結果を他の人に分かりやすく伝え、意思決定などに役立てます。
前処理がなぜ重要なのでしょうか。その理由は、「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をインストールし、使える状態にしましょう。
また、スクリプトの冒頭に次のように記述してtidyverseを使えるようにします。
6.3.2 パイプ演算子%>%について
tidyverseを特徴づける最も重要な要素の一つがパイプ演算子です。これは%>%と書きます。
最近のRではデフォルトの機能としてパイプ演算子が使えるようになりました。Rデフォルトのパイプ演算子は|>という記号で表記します。使い方は基本的に同じです。
パイプ演算子の最大の利点は、複数の関数を繋げて、処理の流れを一つの連続した命令として書けることです。「Aをして、次にBをして、さらにCをする」という手順を、そのままの順番でスクリプトとして書くことができるようになります。
この「繋げる」機能を実現しているのが、「左側のデータや計算結果を、右側の関数の『第1引数』に自動的に渡す」というシンプルなルールです。
例えば、mean(numbers)というコードは、numbersというデータをmean 関数の第1引数に渡して平均を計算します。これをパイプ演算子を使って書くとnumbers %>% mean() となります。numbersがmean()のカッコの中に自動で挿入されるので、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()から外側に向かって解読する必要があり、読みにくいです。
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倍した値を代入しています。
この作業をパイプを使って書くと以下のようになります。
この場合は、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の場合はmutateもfilterもパイプの後に位置するのでdata変数を省略しますが、書き方2の場合はパイプの前に使用しているmutate関数ではdata変数を省略せずに書く必要があります。
書き方2ではパイプを使った一連の処理の最初の関数だけ引数の指定を通常通りに行う必要があり、それを忘れるとエラーが発生してしまいます(このミスは発生しやすい)。したがって、行数は1行多くなってしまうものの、一貫した書き方ができる書き方1の方式をいつも使うようにするとよいでしょう。
6.3.4 計算結果を変数に保存する
このようなパイプ処理の最終結果を変数に保存したい場合、アロー演算子 ->を使うことで処理の流れに沿ってプログラムを書くことができます。
あるいは、通常通りに=記号を使いたい場合は次のようにしても構いません。
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フォルダが作成されているはずです。
次に、このデータを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フォルダが作成されているはずです。
ではここから、この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 Male5人分すべてのデータが残っており、U004とU005について足りない部分のデータはNAになっています。
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 77tidyverse的にこれと同じ処理をする場合は、dplyrのmutate()とna_if()を使います。na_if(列, 条件の値)は、列の値が条件の値と一致した場合にNAに置き換える関数です。
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的に書く場合、dplyrのrecode()関数が便利です。recode(列, "元の値" = "新しい値")のように書きます。
不要な文字の削除
価格や数値データが文字列として入力されている際、"円"や,といった記号が含まれていると、後で数値に変換できず計算に使えません。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 3200tidyverse的に書く場合は次のようにします。
文字列の分割
一つの列に「都道府県-市区町村」のように複数の情報が結合されている場合、分析しやすいように別々の列に分割します。この処理には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的に書く場合は次のようにします。
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 1600tidyverse的に書く場合は、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 16006.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 926.8 確認問題
6.8.1 データ分析における品質管理
データ分析の分野には「Garbage In, Garbage Out(ゴミを入れたら、ゴミしか出てこない)」という有名な言葉があります。 この言葉が示唆する内容を踏まえ、データ分析における「前処理」の役割に関する記述として、最も適切なものを次の中から1つ選んでください。
- 質の悪いデータであっても、最新の高度な分析手法やAIを用いればデータの歪みを補正できるため、前処理を行わなくても信頼できる正しい結果を導き出すことができる。
- 前処理とは、分析結果として出力された数値の中から、予想と異なる「ゴミ(好ましくない結果)」を削除し、仮説と一致するデータだけを残す作業のことである。
- 分析結果の品質は入力するデータの品質に強く依存するため、どれほど高度な手法を使うか以前に、まずデータを正確で整理された状態に整えるための前処理が必要である。
- データ収集の段階でゴミ(不正確なデータ)が混じることは避けられないため、前処理を行ってデータを整えたとしても、最終的な分析結果の精度には影響を与えない。
解答
解答は3。
- Garbage In, Garbage Out」は、元となるデータの質が悪ければ(ゴミを入力すれば)、いくら分析手法やモデルが洗練されていても、そこから得られる結果は無意味なもの(ゴミ)になってしまうという教訓を表しています。
- したがって、質の高い分析結果を得るためには、分析を行う前の段階で、不正確なデータや表記揺れを修正し、データをクリーンな状態にする「前処理」が重要となります。
6.8.2 整然データの特徴
tidyverseなどのパッケージ群が最も効率的に動作するために推奨されているデータ形式を「整然データ(Tidy Data)」と呼びます。 整然データの特徴として正しい記述を次の中から1つ選んでください。
- 人間が目で見て理解しやすいように、1つの行に1人の全科目のテスト結果などが横に並んでおり、集計表のようにまとめられた形式。
- データサイズを最小限に抑えるため、変数名や観測値がすべて特殊な記号に変換され、圧縮・暗号化された形式。
- 「1つの変数は1つの列をなす」「1つの観測は1つの行をなす」「1つの値は1つのセルをなす」というルールを満たす形式。
- 欠損値(NA)や入力ミスが一切なく、すべてのセルに数値が埋まっており、これ以上前処理をする必要がない完璧な状態の形式。
解答
解答は3。
- 整然データ(Tidy Data)とは、データの「中身」の綺麗さ(欠損値がない等)ではなく、データの「構造」に関する定義です。
- 選択肢1のような「ワイド形式」は人間には見やすいですが、プログラムで処理する際には「整然データ(ロング形式)」の構造になっている方が扱いやすいため、分析の前処理の段階でこの形式に変換することが推奨されています。
6.8.3 パイプ演算子を使ったコード記述
以下のコードは、数値ベクトル numbers に対して「絶対値をとり、対数変換し、四捨五入して、最後に平均値を算出する」という処理を、関数を入れ子(ネスト)にして1行で書いたものです。
この一連の処理をパイプ演算子(%>%)を使って書き換えてください。
(パイプ演算子を使うには先に library(tidyverse) を実行しておく必要があります。)
解答
library(tidyverse)
# 方法1
result = numbers %>%
abs() %>%
log() %>%
round() %>%
mean()
# 方法2
numbers %>%
abs() %>%
log() %>%
round() %>%
mean() -> results
# このように書いても良い
result = numbers %>% abs() %>% log() %>% round() %>% mean()
- パイプ演算子 %>% を使うと、 データ %>% 処理1 %>% 処理2 %>% … という形式で、「処理を行う順番通り」に命令を記述できるため、可読性が大幅に向上します。
- 今回の例では、numbers を使って abs() が計算され、その結果に対して log() が計算され、その結果に対して round() が計算され、といった手順で処理が進んでいきます。
6.8.4 パイプ演算子と関数の第一引数
dplyr パッケージの mutate() 関数など、tidyverseに含まれる多くの関数は、本来は第一引数に処理対象のデータフレームを指定する決まりになっています。
一方、パイプ演算子を使ってdat %>% mutate(...)のように記述する場合、mutate 関数内の引数の書き方はどのように変化しますか?正しいものを次の中から1つ選んでください。
- パイプ演算子を使っても関数の使い方は変わらないため、
mutate(dat, ...)のように、第一引数にデータフレーム変数を省略せずに書く必要がある。 - パイプ演算子は「左側のデータを右側の関数の『第一引数』に自動的に渡す」仕組みであるため、
mutate(...)のカッコ内では第一引数(データフレーム)を省略して記述する。 - パイプ演算子を使う場合、データフレームは第一引数ではなく「最後の引数」として渡される仕様に変わるため、引数の最後にデータフレームを指定する。
- パイプ演算子はデータを送るだけなので、受け取る関数側では必ず .(ドット)という記号を第一引数に書いて、データの受け取り場所を明示しなければならない(例:
mutate(., ...))。
解答
解答は2。
- パイプ演算子 %>% の最大の役割は、「左側のデータ(前段の処理結果)を、右側の関数の第一引数として送り込む」ことです。そのため、パイプでつないだ後の関数内では、本来書くべき第一引数(データフレーム)を書かない(省略する)のが正しい記述法です。
- 逆に、パイプを使っているのに
dat %>% mutate(dat, ...)のように第一引数を書いてしまうと、データが二重に渡されることになってしまい、エラーや予期せぬ動作の原因となります。 - 選択肢4のように
.を使うやり方は仕様として存在し、ドットを使うことで第一引数以外の任意の位置にデータを渡すことが可能になります(例:df %>% lm(y ~ x, data = .))。ただし、「必ず書かなければならない」というのは誤りです。通常は省略して書きます。
6.8.5 read_csv におけるエンコーディング指定
readr パッケージの read_csv() 関数を使って、日本語が含まれるCSVファイルを読み込んだところ、文字が化けて意味不明な記号になってしまいました。
このファイルがShift-JIS形式(Windowsで一般的)で保存されていることがわかっている場合、文字化けを直すために read_csv() に追加すべき引数指定として正しいものはどれですか?
encoding = "Shift-JIS"fileEncoding = "Shift-JIS"locale = locale(encoding = "Shift-JIS")charset = "Shift-JIS"
解答
解答は3。
- Rの基本関数(read.csv)の場合は文字コードの指定に fileEncoding 引数を使用します(例:
fileEncoding = "Shift-JIS")。 - 一方で、tidyverseの read_csv() 関数には fileEncoding 引数は存在せず、文字コードや日付形式などの地域固有の設定を locale 引数でまとめて管理する仕組みになっています。
6.8.6 RDS形式を使用するメリット
あるデータフレームに対して、文字列型の列を「順序付きの因子型(ordered factor)」に変換するなどの前処理を行いました。 この作業を一旦中断し、後日またRで読み込んで続きの作業を行いたい場合、CSV形式(write_csv)ではなくRDS形式(write_rds)で保存すべき最大のメリットは何ですか?
- RDS形式はテキストファイルであるため、保存したデータをメモ帳やExcelで直接開いて中身を確認・編集することができる。
- RDS形式はR専用のデータ保存形式であり、因子型の順序情報や各列のデータ型などの属性情報を保持したままデータの保存よ読み込みができる。
- RDS形式はCSV形式に比べて保存容量が必ず大きくなるため、ディスクの空き容量が十分にある場合のみ推奨される。
- RDS形式はPythonやSQLなど他のプログラミング言語やデータベースとの互換性が高いため、チームでデータを共有する際に便利である。
解答
解答は2。
- CSVファイルは汎用性が高く便利ですが、データはすべて「テキスト(文字)」として保存されるため、Rで設定した「因子型の順序(Levels)」などの詳細な情報は失われてしまいます(読み込み直し時にただの文字列や数値に戻ってしまいます)。
- 一方、RDS形式はRのオブジェクトをそのままの状態で保存します。型情報や属性なども保存しておけるので、Rでの作業内容をそのまま保存し、再開することができます。
6.8.7 bind_rows 関数によるデータの積み上げ
以下の2つのデータフレームは、それぞれ4月と5月の売上データ(製品IDと売上個数)を記録したものです。
df_april = data.frame(id = 1:2, amount = c(100, 200))
df_may = data.frame(id = 3:4, amount = c(150, 250))この2つのデータフレームを縦方向に結合し、ひとつのデータフレーム df_total にまとめてください。 dplyr パッケージの bind_rows() 関数を使ってください。
解答
library(dplyr)
df_total = bind_rows(df_april, df_may)
# rbind 関数を使う場合はこれでよい
df_total = rbind(df_april, df_may)
# 結果の確認
print(df_total)
## id amount
## 1 1 100
## 2 2 200
## 3 3 150
## 4 4 250
- Rの基本関数である rbind() でも同じ処理はできますが、bind_rows() は処理が高速で、ID列を自動生成するオプション(.id)があるなど機能が豊富であるため、Tidyverseを利用する場合はこちらが推奨されます。
6.8.8 inner_join の実行結果
以下のような2つのデータフレームがあります。
df_A = data.frame(
user_id = c(1, 2, 3),
name = c("Taro", "Jiro", "Saburo")
)
df_B = data.frame(
user_id = c(2, 3, 4),
city = c("Osaka", "Tokyo", "Nagoya")
)この2つのデータに対して、以下のコードを実行しました。
この時、結果の result に残る user_id はどれになりますか?
- 「1, 2, 3, 4」(両方のデータにあるすべてのIDが残る)
- 「1, 2, 3」(df_A にあるIDがすべて残る)
- 「2, 3」(両方のデータに共通して存在するIDだけが残る)
- 「2, 3, 4」(df_BにあるIDがすべて残る)
解答
解答は3。
- inner_join(内部結合)は、指定したキー(ここでは user_id)が「両方のデータフレームに共通して存在する場合のみ」、行を残す結合方法です。
- 今回の例では、ID 2 と 3 は両方に存在しますが、1 は左のみ、4 は右のみに存在するため、inner_join の結果からは除外されます。
- なお、選択肢1は full_join() 関数、選択肢2は left_join() 関数を使った場合の挙動です。
6.8.9 left_join 関数による情報の付与
以下はある店舗の「売上データ(sales)」と「顧客データ(customers)」です。
# 売上データ(メインとなるデータ)
sales = data.frame(
order_id = c(1, 2, 3, 4),
customer_id = c("C01", "C02", "C01", "C03"),
amount = c(1000, 2500, 1200, 3000)
)
# 顧客データ(紐付けたい属性情報)
customers = data.frame(
customer_id = c("C01", "C02", "C03", "C04"),
gender = c("Male", "Female", "Male", "Female")
)sales データを基準(左側)とし、共通のキー変数 customer_id を使って、customers データにある gender(性別)情報を結合するコードを記述してください。
6.8.10 特定の値を NA に変換する
アンケートデータの集計を行っています。 データフレーム df の score 列には点数が記録されていますが、未回答の部分には「999」という数値が入力されてしまっています。
この score 列に含まれる「999」を、R言語で正しく欠損値として扱うための NA に置き換えるコードを記述してください。
6.8.11 不要な文字の削除と数値化
ある商品データのデータフレーム df を読み込みましたが、価格(price)の列が、「円」という単位や桁区切りのカンマが含まれた「文字列型」として読み込まれました。
この price 列から「円」と「,(カンマ)」の2つの文字を取り除き、計算可能な「数値型(numeric)」に変換するコードを記述してください。
解答
# str_replace_allを使う場合
df = df %>%
mutate(
price = str_replace_all(price, "円", ""),
price = str_replace_all(price, ",", ""),
price = as.numeric(price)
)
# 正規表現を使ってまとめて置換する場合
df = df %>%
mutate(
price = str_replace_all(price, "[円,]", ""),
price = as.numeric(price)
)
# Base Rの gsub 関数を使う場合
# gsub(パターン, 置換後, 対象データ) の順で指定する
df$price = gsub("円", "", df$price)
df$price = gsub(",", "", df$price)
df$price = as.numeric(df$price)
- stringr パッケージの
str_replace_all(文字列, "消したい文字", "置換後の文字")を使って不要な文字を空文字("")に置換して削除し、最後に as.numeric() 関数で数値型に変換することで、正しく計算できる状態にします。 - 正規表現の活用:
"[円,]"のように角括弧[]で文字を囲むと、「その中のいずれかの文字」という意味になります(文字クラス)。これを使えば、1行の処理で複数の不要な文字を一度に削除できます。 - Rにもともと備わっている gsub() 関数でも同様の置換が可能です。ただし、引数の順番が
gsub("消したい文字", "置換後の文字", 対象列)となり、str_replace_all とは「対象データの指定位置」が異なる(第3引数になる)点に注意が必要です。
6.8.12 separate 関数による列分割
ある顧客名簿データ df の address 列に、都道府県と市区町村がハイフン(-)でつながれた状態で記録されています。
この address 列を「prefecture」(都道府県)と「city」(市区町村)の2つの列に分割するコードを記述してください。
解答
df = df %>%
separate(address, into = c("prefecture", "city"), sep = "-")
print(df)
## id prefecture city
## 1 1 神奈川県 横浜市
## 2 2 埼玉県 さいたま市
- 1つの列に複数の情報が混ざっている場合は、tidyr パッケージの separate() 関数を使って分割します。 引数の指定方法は
separate(分割したい列, into = c("新しい列名1", "新しい列名2"), sep = "区切り文字")です。 - これにより、データをより扱いやすい「整然データ」の形式に近づけることができます。
6.8.13 mutate と case_when によるカテゴリ分け
あるテストの点数データ df があります。
このデータの score 列の値に対して以下の条件で判定をした結果を格納した新しい列 grade を作成するコードを記述してください。
- 80点以上の場合:「A」
- 60点以上80点未満の場合:「B」
- それ以外(60点未満)の場合:「C」
解答
df = df %>%
mutate(grade = case_when(
score >= 80 ~ "A",
score >= 60 ~ "B",
TRUE ~ "C"
))
print(df)
## id score grade
## 1 1 90 A
## 2 2 75 B
## 3 3 45 C
## 4 4 80 A
## 5 5 60 B
- 条件分岐によって新しい変数を作りたい場合、dplyr の case_when() 関数を使うと、複数の条件をスッキリと記述できます。書き方は
条件式 ~ "割り当てる値"です。 - 条件は上から順に判定されるため、
score >= 60 ~ "B"の行では自動的に「80点未満(上の条件に当てはまらなかったもの)」という条件が含まれることになります。 - 最後の
TRUE ~ "C"は、どの条件にも当てはまらなかった残りすべて(else)を意味します。
6.8.14 pivot_longer による整然データ化
以下のような「ワイド形式」のテスト結果データ df_wide があります。1行に1人の生徒の複数科目の点数が横並びで記録されています。
このデータを「科目(subject)」列と「点数(score)」列を持つ「ロング形式(整然データ)」に変換するコードを記述してください。
ヒント:pivot_longer() 関数を使ってください。
6.8.15 総合問題1
ある学校の「生徒名簿(students)」と、テストの「成績データ(results)」があります。 しかし、成績データは以下のような問題を抱えています。
- 値の汚れ: score 列に「点」という文字が含まれている(例: "80点")。
- 欠損値: 未受験の科目は -1 と記録されており、これは NA として扱う必要がある。
【課題】 これらのデータを処理し、以下の要件を満たすデータフレーム df_final を作成する一連のコードを記述してください。
- score 列の「点」を削除して数値型に変換し、-1 は NA に変換する。
- 生徒名簿(students)と結合して、生徒の名前(name)情報を付与する。
- 最終的に、生徒1人につき1行で、各科目の点数が列として横に並ぶワイド形式に変換する。
# 生徒名簿
students = data.frame(
student_id = 1:3,
name = c("Taro", "Jiro", "Saburo")
)
# 成績データ(汚れたロング形式)
results = data.frame(
student_id = c(1, 1, 2, 2, 3, 3),
subject = c("Math", "English", "Math", "English", "Math", "English"),
score = c("80点", "70点", "90", "-1", "60点", "85点")
)解答
df_final = results %>%
mutate(
score = str_replace_all(score, "点", ""),
score = as.numeric(score),
score = na_if(score, -1)
) %>%
left_join(students, by = "student_id") %>%
pivot_wider(
names_from = "subject",
values_from = "score"
)
print(df_final)
## # A tibble: 3 × 4
## student_id name Math English
## <dbl> <chr> <dbl> <dbl>
## 1 1 Taro 80 70
## 2 2 Jiro 90 NA
## 3 3 Saburo 60 85
- まず mutate でデータをきれいに掃除し(クリーニング)、次に left_join で必要な情報を集め(統合)、最後に pivot_wider で人間が見やすい形やレポート用の形に整える(構造変換)という流れをパイプ演算子を使うことでひとまとめに記述できます。
6.8.16 総合問題2
ある小売店の「4月のデータ(apr_data)」と「5月のデータ(may_data)」があります。 これらのデータは現状は以下のような状態になっています。
- ファイルが分割されている: 月ごとに別々のデータフレームになっている。
- 複合列がある: meta_info 列に「日付_店舗名」という形式(例: "20230401_Tokyo")で日付と店舗名の情報が混ざっている。
- 価格列の表記: price 列に「¥」やカンマが含まれている。
【課題】 これらのデータを処理し、以下の要件を満たすデータフレーム df_analysis を作成してください。
- 2つのデータを結合して1つにまとめる。
- meta_info 列を「date」と「store」の2列に分割する(区切り文字は_)。
- price 列を数値型に変換する。
- 売上金額(price × quantity)を計算し、その結果が 10,000 以上なら「High」、それ未満なら「Low」という値を持つ新しい列 sales_rank を作成する。
# 4月のデータ
apr_data = data.frame(
meta_info = c("20230401_Tokyo", "20230402_Osaka"),
price = c("¥1,000", "¥5,000"),
quantity = c(5, 3)
)
# 5月のデータ
may_data = data.frame(
meta_info = c("20230501_Tokyo", "20230505_Nagoya"),
price = c("¥2,000", "¥12,000"),
quantity = c(10, 1)
)解答
df_analysis = bind_rows(apr_data, may_data) %>%
separate(meta_info, into = c("date", "store"), sep = "_") %>%
mutate(
price = str_replace_all(price, "¥", ""),
price = str_replace_all(price, ",", ""),
price = as.numeric(price)
) %>%
mutate(
total_sales = price * quantity,
sales_rank = case_when(
total_sales >= 10000 ~ "High",
TRUE ~ "Low"
)
)
print(df_analysis)
## date store price quantity total_sales sales_rank
## 1 20230401 Tokyo 1000 5 5000 Low
## 2 20230402 Osaka 5000 3 15000 High
## 3 20230501 Tokyo 2000 10 20000 High
## 4 20230505 Nagoya 12000 1 12000 High
- パイプと dplyr パッケージの関数を使うことで一連の作業をひとまとめに書くことができます。