データサイエンス
Tidyverseで始めるデータクレンジング - 汚いデータを綺麗にする方法
データ分析の8割は「データクレンジング(データ前処理)」だと言われます。実際のデータは、欠損値、重複、表記ゆれ、外れ値など、様々な「汚れ」を含んでいます。これらを綺麗にしないと、正確な分析はできません。
この記事では、R言語のTidyverseを使ったデータクレンジングの実践方法を、人事データを例に解説します。欠損値の処理、重複の削除、型変換、外れ値の除去など、実務で必ず使う手法を網羅します。
データクレンジングとは
データ分析の流れ
- データ収集:ExcelファイルやDBから取得
- データクレンジング:汚れを除去(全体の70〜80%)
- 探索的データ分析(EDA):可視化して傾向を探る
- モデリング:統計分析や機械学習
- レポーティング:結果を共有
よくあるデータの問題
人事部から受け取った社員データの例:
employee_id,name,age,department,salary,join_date
001,山田太郎,32,営業部,5000000,2020-04-01
002,佐藤花子,28,開発部,4500000,2021-06-15
003,田中一郎,999,人事部,4800000,2019-10-01 # 年齢が明らかに異常
004,山田太郎,32,営業部,5000000,2020-04-01 # 001と重複
005,鈴木次郎,,営業部,4200000,2022-01-10 # 年齢が欠損
006,高橋三郎,45,開発,5500000,2018-05-20 # 「開発部」ではなく「開発」
007,伊藤四郎,38,営業部,,2020-08-01 # 給与が欠損
008,渡辺五郎,29,人事部,4900000,2021/03/15 # 日付の形式が異なる
このデータには、以下の問題があります:
- 重複データ:001と004が同じ
- 欠損値:005の年齢、007の給与
- 外れ値:003の年齢999歳
- 表記ゆれ:「開発部」と「開発」
- 日付形式の不統一:2020-04-01と2021/03/15
準備:Tidyverseのインストール
# Tidyverseのインストール
install.packages("tidyverse")
# ライブラリ読み込み
library(tidyverse)
ステップ1:データの読み込みと確認
CSVファイルの読み込み
# readr パッケージの read_csv() を使用
employee_data <- read_csv("employee_data.csv")
# データの最初の6行を表示
head(employee_data)
# データの構造を確認
glimpse(employee_data)
# 出力例:
# Rows: 100
# Columns: 6
# $ employee_id "001", "002", "003", ...
# $ name "山田太郎", "佐藤花子", ...
# $ age 32, 28, 999, ...
# $ department "営業部", "開発部", ...
# $ salary 5000000, 4500000, ...
# $ join_date "2020-04-01", "2021-06-15", ...
基本統計量の確認
# 各列の統計量
summary(employee_data)
# 欠損値の数
employee_data %>%
summarise(across(everything(), ~sum(is.na(.))))
# 出力例:
# employee_id name age department salary join_date
# 0 0 5 2 12 0
ステップ2:重複データの削除
完全一致する重複
# 重複行を削除(全ての列が一致)
employee_data <- employee_data %>%
distinct()
# 削除前後の行数を確認
nrow(employee_data) # 95行(5行の重複を削除)
特定列での重複
# employee_id が重複している行を削除(最初の行を残す)
employee_data <- employee_data %>%
distinct(employee_id, .keep_all = TRUE)
# どの行が重複していたかを確認
employee_data %>%
group_by(employee_id) %>%
filter(n() > 1)
ステップ3:欠損値の処理
欠損値の確認
# 列ごとの欠損値数と割合
employee_data %>%
summarise(across(everything(), list(
missing = ~sum(is.na(.)),
pct = ~mean(is.na(.)) * 100
)))
# 出力例:
# age_missing age_pct salary_missing salary_pct
# 5 5.26% 12 12.6%
欠損値の削除
# 欠損値を含む行を全て削除
employee_data_complete <- employee_data %>%
drop_na()
# 特定の列の欠損値のみ削除
employee_data_clean <- employee_data %>%
drop_na(age, salary) # age または salary が欠損している行を削除
欠損値の補完
# 平均値で補完
employee_data <- employee_data %>%
mutate(salary = if_else(is.na(salary), mean(salary, na.rm = TRUE), salary))
# 中央値で補完(外れ値の影響を受けにくい)
employee_data <- employee_data %>%
mutate(age = if_else(is.na(age), median(age, na.rm = TRUE), age))
# 前の値で補完(時系列データの場合)
employee_data <- employee_data %>%
arrange(join_date) %>%
fill(department, .direction = "down")
# 部署ごとの平均値で補完
employee_data <- employee_data %>%
group_by(department) %>%
mutate(salary = if_else(is.na(salary), mean(salary, na.rm = TRUE), salary)) %>%
ungroup()
ステップ4:外れ値の処理
外れ値の検出(箱ひげ図)
# 年齢の箱ひげ図
ggplot(employee_data, aes(y = age)) +
geom_boxplot() +
labs(title = "年齢の分布")
# IQR(四分位範囲)で外れ値を検出
Q1 <- quantile(employee_data$age, 0.25, na.rm = TRUE)
Q3 <- quantile(employee_data$age, 0.75, na.rm = TRUE)
IQR <- Q3 - Q1
# 外れ値の範囲
lower_bound <- Q1 - 1.5 * IQR
upper_bound <- Q3 + 1.5 * IQR
# 外れ値を含む行を確認
outliers <- employee_data %>%
filter(age < lower_bound | age > upper_bound)
print(outliers)
# 年齢999歳のデータが検出される
外れ値の処理
# 方法1:外れ値を削除
employee_data_clean <- employee_data %>%
filter(age >= lower_bound & age <= upper_bound)
# 方法2:外れ値を上下限値で置き換え(Winsorization)
employee_data <- employee_data %>%
mutate(age = case_when(
age < lower_bound ~ lower_bound,
age > upper_bound ~ upper_bound,
TRUE ~ age
))
# 方法3:外れ値をNAに置き換えて、後で補完
employee_data <- employee_data %>%
mutate(age = if_else(age < 18 | age > 70, NA_real_, age))
ステップ5:データ型の変換
文字列→数値
# employee_id を数値型に変換
employee_data <- employee_data %>%
mutate(employee_id = as.numeric(employee_id))
# カンマ区切りの数値文字列を数値に変換
employee_data <- employee_data %>%
mutate(salary = as.numeric(str_remove_all(salary, ",")))
# 例: "5,000,000" → 5000000
文字列→日付
# lubridateパッケージを使用
library(lubridate)
# 様々な日付形式を統一
employee_data <- employee_data %>%
mutate(join_date = ymd(join_date))
# 日付の要素を抽出
employee_data <- employee_data %>%
mutate(
join_year = year(join_date),
join_month = month(join_date),
join_day = day(join_date)
)
文字列→カテゴリ変数(factor)
# department を factor 型に変換
employee_data <- employee_data %>%
mutate(department = factor(department))
# 順序付きfactor(評価など)
employee_data <- employee_data %>%
mutate(performance = factor(
performance,
levels = c("C", "B", "A", "S"),
ordered = TRUE
))
ステップ6:表記ゆれの修正
部署名の統一
# 「開発」を「開発部」に統一
employee_data <- employee_data %>%
mutate(department = case_when(
department == "開発" ~ "開発部",
department == "営業" ~ "営業部",
department == "人事" ~ "人事部",
TRUE ~ department
))
# または str_replace() を使用
employee_data <- employee_data %>%
mutate(department = str_replace(department, "^(\\w+)$", "\\1部"))
空白の削除
# 前後の空白を削除
employee_data <- employee_data %>%
mutate(across(where(is.character), str_trim))
# 全角スペースも削除
employee_data <- employee_data %>%
mutate(across(where(is.character), ~str_replace_all(., "[ ]+", "")))
大文字・小文字の統一
# 全て小文字に変換
employee_data <- employee_data %>%
mutate(email = str_to_lower(email))
# 姓名を統一(姓は大文字、名は小文字)
employee_data <- employee_data %>%
mutate(name = str_to_title(name))
ステップ7:データ変換と集約
新しい列の追加
# 勤続年数を計算
employee_data <- employee_data %>%
mutate(tenure_years = as.numeric(difftime(Sys.Date(), join_date, units = "days")) / 365.25)
# 年齢層を作成
employee_data <- employee_data %>%
mutate(age_group = case_when(
age < 30 ~ "20代",
age < 40 ~ "30代",
age < 50 ~ "40代",
TRUE ~ "50代以上"
))
# 給与を万円単位に変換
employee_data <- employee_data %>%
mutate(salary_man = salary / 10000)
条件によるフィルタリング
# 30歳以上かつ営業部の社員
filtered_data <- employee_data %>%
filter(age >= 30 & department == "営業部")
# 給与が上位10%の社員
top10_salary <- employee_data %>%
filter(salary >= quantile(salary, 0.9, na.rm = TRUE))
データの集約
# 部署別の平均給与
dept_salary <- employee_data %>%
group_by(department) %>%
summarise(
count = n(),
avg_salary = mean(salary, na.rm = TRUE),
median_salary = median(salary, na.rm = TRUE),
sd_salary = sd(salary, na.rm = TRUE)
)
実践例:人事データの完全クレンジング
# 1. データ読み込み
employee_data <- read_csv("employee_data.csv")
# 2. パイプラインで一気にクレンジング
employee_cleaned <- employee_data %>%
# 重複削除
distinct(employee_id, .keep_all = TRUE) %>%
# 型変換
mutate(
employee_id = as.numeric(employee_id),
join_date = ymd(join_date),
department = factor(department)
) %>%
# 表記ゆれ修正
mutate(across(where(is.character), str_trim)) %>%
mutate(department = case_when(
department %in% c("開発", "dev") ~ "開発部",
department %in% c("営業", "sales") ~ "営業部",
TRUE ~ department
)) %>%
# 外れ値除外(年齢18〜70歳)
filter(age >= 18 & age <= 70) %>%
# 欠損値補完
group_by(department) %>%
mutate(salary = if_else(is.na(salary), median(salary, na.rm = TRUE), salary)) %>%
ungroup() %>%
mutate(age = if_else(is.na(age), median(age, na.rm = TRUE), age)) %>%
# 新しい列追加
mutate(
tenure_years = as.numeric(difftime(Sys.Date(), join_date, units = "days")) / 365.25,
age_group = case_when(
age < 30 ~ "20代",
age < 40 ~ "30代",
age < 50 ~ "40代",
TRUE ~ "50代以上"
)
) %>%
# 必要な列のみ選択
select(employee_id, name, age, age_group, department, salary, join_date, tenure_years)
# 3. 確認
glimpse(employee_cleaned)
summary(employee_cleaned)
# 4. クリーンなデータを保存
write_csv(employee_cleaned, "employee_cleaned.csv")
データクレンジングのベストプラクティス
1. 元データは残す
# 悪い例:元データを上書き
employee_data <- employee_data %>% filter(age >= 18)
# 良い例:新しいオブジェクトに保存
employee_cleaned <- employee_data %>% filter(age >= 18)
2. 処理を記録する
# Rスクリプトに全ての処理を記録
# 再現可能な形で保存
# クレンジング前のデータ概要
cat("Before cleaning:\n")
print(summary(employee_data))
# クレンジング
employee_cleaned <- employee_data %>%
# ... クレンジング処理
# クレンジング後のデータ概要
cat("\nAfter cleaning:\n")
print(summary(employee_cleaned))
3. データ品質レポートを作成
# skimr パッケージで詳細なレポート
library(skimr)
skim(employee_data) # クレンジング前
skim(employee_cleaned) # クレンジング後
# 出力:
# データ型、欠損値、統計量、ヒストグラムなどを自動表示
まとめ
データクレンジングの流れ
- データ確認:構造、欠損値、統計量
- 重複削除:distinct()
- 欠損値処理:drop_na(), 補完
- 外れ値処理:IQRで検出、削除または置換
- 型変換:as.numeric(), ymd(), factor()
- 表記ゆれ修正:case_when(), str_replace()
- データ変換:新しい列追加、集約
よく使う関数まとめ
| 処理 | 関数 |
|---|---|
| 重複削除 | distinct() |
| 欠損値削除 | drop_na() |
| 欠損値補完 | if_else(), fill() |
| 条件分岐 | case_when() |
| 文字列操作 | str_trim(), str_replace() |
| 日付変換 | ymd(), year(), month() |
データクレンジングは地味ですが、分析の精度を左右する最重要工程です。Tidyverseを使えば、効率的かつ再現可能な形でクレンジングできます。ぜひ、自分のデータで試してみてください!