データ・分析

データ品質を保つ — 欠損・重複・異常値をどう扱うか

分析の結論が間違う原因の多くは、手法ではなくデータそのものにあります。
集計の前に確認すべき3点を整理します。

1. 欠損

まず「どこがどれだけ欠けているか」を出します。

missing = df.isna().sum()
print((missing / len(df) * 100).round(1).sort_values(ascending=False))

対処は、欠損の理由によって変わります

理由 対処
入力必須でない項目 「未入力」として1カテゴリに
途中から追加された項目 追加前のデータを分析から除外
システム障害で欠けた 該当期間を明示して除外

安易に平均で埋めるのは危険です。分布が歪み、
「実際より均質なデータ」に見えてしまいます。

集計時は、母数を明示します。

回答率 72%(1,240件中 893件が回答)

2. 重複

dups = df[df.duplicated(subset=["order_id"], keep=False)]
print(f"重複: {len(dups)}件 / {dups['order_id'].nunique()}種")

重複の原因は主に3つです。

  • 二重登録(送信ボタンの連打)
  • 結合ミス(JOINで行が増えた)
  • 再送されたイベントの二重取り込み

2つ目が最も気づきにくいです。集計前後で件数が変わっていないかを確認します。

before = len(orders)
merged = orders.merge(customers, on="customer_id", how="left")
assert len(merged) == before, f"結合で行が増えた: {before}{len(merged)}"

この assert を入れておくだけで、多くの事故を防げます。

3. 異常値

外れ値をすべて除外するのは間違いです。本物の外れ値かもしれません。

q1, q3 = df["amount"].quantile([0.25, 0.75])
iqr = q3 - q1
outliers = df[(df["amount"] < q1 - 1.5*iqr) | (df["amount"] > q3 + 1.5*iqr)]

見つけたら、除外する前に中身を見ます

  • 明らかな入力ミス(金額に桁が1つ多い)→ 修正または除外
  • テストデータが混入 → 除外
  • 実際の大口取引 → 残す(これを消すと実態が分からなくなる)

判断できない場合は、除外した場合としない場合の両方を出します。

平均注文額: ¥8,400(全件) / ¥6,200(上位1%を除く)

検査を仕組みにする

毎回手で確認するのは続きません。取り込み時に自動で検査します。

CHECKS = [
    ("金額が負", lambda d: (d["amount"] < 0).sum()),
    ("日付が未来", lambda d: (d["created_at"] > pd.Timestamp.now()).sum()),
    ("必須項目が空", lambda d: d["customer_id"].isna().sum()),
]

for name, fn in CHECKS:
    n = fn(df)
    if n:
        logger.warning("データ品質: %s%d件", name, n)

異常があっても止めず、記録して先に進むのが実務的です。
止めると分析自体ができなくなります。

数字を出すときに添えること

集計期間: 2025-08-01 〜 2025-08-31
対象件数: 12,480件(重複除去後)
除外: テストアカウント 42件、金額が負の記録 3件
欠損: 顧客区分が未設定 891件(7.1%)

何を除外したかを明記すると、後から検証できます。
これが無い数字は、疑問が出たときに再現できません。

まとめ

  • 欠損は理由によって対処を変える。平均で埋めない
  • 結合の前後で件数が変わっていないか assert で守る
  • 異常値は中身を見てから判断する。本物の大口を消さない
  • 検査は自動化し、警告を出して先に進む
  • 集計結果には対象件数・除外・欠損率を必ず添える