MechaToraのブログ

記事のアイキャッチ画像
データサイエンス

Tidyverseで始めるデータクレンジング - 汚いデータを綺麗にする方法

データ分析の8割は「データクレンジング(データ前処理)」だと言われます。実際のデータは、欠損値、重複、表記ゆれ、外れ値など、様々な「汚れ」を含んでいます。これらを綺麗にしないと、正確な分析はできません。

この記事では、R言語のTidyverseを使ったデータクレンジングの実践方法を、人事データを例に解説します。欠損値の処理、重複の削除、型変換、外れ値の除去など、実務で必ず使う手法を網羅します。

データクレンジングとは

データ分析の流れ

  1. データ収集:ExcelファイルやDBから取得
  2. データクレンジング:汚れを除去(全体の70〜80%)
  3. 探索的データ分析(EDA):可視化して傾向を探る
  4. モデリング:統計分析や機械学習
  5. レポーティング:結果を共有

よくあるデータの問題

人事部から受け取った社員データの例:

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  # 日付の形式が異なる

このデータには、以下の問題があります:

準備: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)  # クレンジング後

# 出力:
# データ型、欠損値、統計量、ヒストグラムなどを自動表示

まとめ

データクレンジングの流れ

  1. データ確認:構造、欠損値、統計量
  2. 重複削除:distinct()
  3. 欠損値処理:drop_na(), 補完
  4. 外れ値処理:IQRで検出、削除または置換
  5. 型変換:as.numeric(), ymd(), factor()
  6. 表記ゆれ修正:case_when(), str_replace()
  7. データ変換:新しい列追加、集約

よく使う関数まとめ

処理 関数
重複削除 distinct()
欠損値削除 drop_na()
欠損値補完 if_else(), fill()
条件分岐 case_when()
文字列操作 str_trim(), str_replace()
日付変換 ymd(), year(), month()

データクレンジングは地味ですが、分析の精度を左右する最重要工程です。Tidyverseを使えば、効率的かつ再現可能な形でクレンジングできます。ぜひ、自分のデータで試してみてください!