Skip to content

Pandas Data Cleaning:実務向けワークフロー

Updated on

汚れたデータは分析を初期から止めます。場当たり的な fillna や文字置換では、誤った型や静かな重複、外れ値によるバイアスが隠れたままになります。

PASの流れ:

  • 問題: 型混在・欠損・重複がジョインを壊し、統計を歪める。
  • あおり: その場しのぎの修正は問題を後回しにし、後で手戻りになる。
  • 解決: 反復可能なチェックリストで型を揃え、欠損を意図的に処理し、外れ値と重複をさばき、最後に検証する。

ワークフロー概要

ステップやること
1. プロファイル列/欠損/ユニークを見るdf.info(), df.describe(include="all")
2. カラム正規化名前/文字列を整理df.columns = df.columns.str.strip().str.lower()
3. 型を直すto_datetime, to_numeric, astype("category")df["date"] = pd.to_datetime(df["date"], errors="coerce")
4. 欠損処理ルールで削除/補完df["age"] = df["age"].fillna(df["age"].median())
5. 外れ値処理クリップ/フラグdf["rev_cap"] = df["revenue"].clip(upper=df["revenue"].quantile(0.99))
6. 重複排除完全/部分重複を除去df.drop_duplicates(subset=["id", "date"], keep="last")
7. 検証範囲・カテゴリを確認assert df["score"].between(0,100).all()

1) まず素早く把握

summary = {
    "rows": len(df),
    "columns": df.shape[1],
    "nulls": df.isna().sum(),
    "unique_counts": df.nunique(),
}
  • 早めに型混在を見つける (df.info()).
  • df.describe(percentiles=[0.01,0.99]) で分布の歪みを確認。

2) カラムとテキストを整える

df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")
df["country"] = df["country"].str.strip().str.title()
  • 標準化した名前はジョインやコードを簡単にする。
  • 変な文字が混じる場合は .str.normalize("NFKC") も有効。

3) データ型を修正

df["date"] = pd.to_datetime(df["date"], errors="coerce", utc=True)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df["segment"] = df["segment"].astype("category")
  • errors="coerce" で不正値を NaT/NaN として顕在化。
  • convert_dtypes() で一括して nullable・省メモリ型に。

4) 欠損値を処理

df["age"] = df["age"].fillna(df["age"].median())
df["city"] = df["city"].fillna("Unknown")
df = df.dropna(subset=["id"])  # 必須キー
  • 列ごとに方針: 数値→中央値/平均、カテゴリ→最頻/プレースホルダー。
  • シグナル保持のためフラグ追加: df["age_imputed"] = df["age"].isna()

5) 外れ値を扱う

upper = df["revenue"].quantile(0.99)
lower = df["revenue"].quantile(0.01)
df["revenue_capped"] = df["revenue"].clip(lower=lower, upper=upper)
  • 比率なら Z スコア: abs(z) < 3 など。
  • 金額系は削除よりクリップが安全(件数を保つ)。

6) 重複を安全に除去

df = df.drop_duplicates(subset=["id", "date"], keep="last")
  • 想定キーを確認: assert df.duplicated(["id"]).sum() == 0
  • あいまい照合が必要なら、先に正規化(例: メールを小文字化)。

7) エクスポート前に検証

assert df["score"].between(0, 100).all()
valid_segments = {"basic", "pro", "enterprise"}
assert df["segment"].isin(valid_segments).all()
  • テストでは pd.testing.assert_frame_equal で出力比較も可能。
  • パイプラインに軽い行数/欠損チェックを入れて回帰を防ぐ。

エンドツーエンドのミニパイプライン

def clean(df):
    df = df.copy()
    df.columns = df.columns.str.strip().str.lower()
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
    df["amount"] = df["amount"].fillna(df["amount"].median())
    df["segment"] = df["segment"].fillna("unknown").str.lower()
    df = df.dropna(subset=["id"])
    df = df.drop_duplicates(subset=["id", "date"], keep="last")
    assert df["amount"].ge(0).all()
    return df
  • まずコピーして入力を破壊しない。
  • df.pipe(clean) を使うとパイプラインが読みやすい。

関連ガイド


まとめ

  • 名前を標準化し、型を強制し、欠損を意図を持って扱う。
  • 外れ値は静かに削除するよりクリップやフラグで扱う。
  • エクスポート前にキーと値の範囲を検証し、品質を担保する。