分析の結論が間違う原因の多くは、手法ではなくデータそのものにあります。
集計の前に確認すべき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で守る - 異常値は中身を見てから判断する。本物の大口を消さない
- 検査は自動化し、警告を出して先に進む
- 集計結果には対象件数・除外・欠損率を必ず添える