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)を使うとパイプラインが読みやすい。
関連ガイド
まとめ
- 名前を標準化し、型を強制し、欠損を意図を持って扱う。
- 外れ値は静かに削除するよりクリップやフラグで扱う。
- エクスポート前にキーと値の範囲を検証し、品質を担保する。