「外れ値は 3σ ルールで除去」── 教科書では正解だが、実データの 8 割以上が裾の重い分布(log-normal、Pareto)で、3σ rule は正常なヘビーユーザーまで弾いてしまう。本記事では、より頑健な手法と、 シリーズとの棲み分けを実コード付きで扱います。
3σ rule の落とし穴を可視化する
まず実データに近い裾の重い分布を作って、3σ rule がどれだけ正常なデータを弾くかを見ます。
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport japanize_matplotlib # noqa: F401
np.random.seed(42)# 売上のような log-normal 分布(多くのデータが小さい、少数が極端に大きい)data = np.random.lognormal(mean=8, sigma=1.0, size=10000)print(f"範囲: {data.min():.0f} 〜 {data.max():.0f}")print(f"中央値: {np.median(data):.0f}")print(f"平均: {data.mean():.0f}") # 中央値 < 平均(裾が右に伸びる)
# 3σ rule で外れ値判定mu, sigma = data.mean(), data.std()upper_3sigma = mu + 3 * sigman_3sigma = (data > upper_3sigma).sum()print(f"3σ で除去される件数: {n_3sigma} / 10000 ({n_3sigma/100:.1f}%)")# → 200〜400 件除去される(正規分布なら 0.13% = 13 件のはずが、20-40倍多い)log-normal で 3σ ルール → 真の異常ではない上位の優良ユーザーまで除去される。マーケ施策で「ヘビーユーザーが消えた!」と気付いて、データ品質を疑う事故になりがち。
手法 ①:IQR ベース(Tukey's fence)
Q1 - 1.5×IQR / Q3 + 1.5×IQR を外れ値とする手法。外れ値の影響を受けないため、3σ より頑健。 EP.05 のボックスプロットの「」表示はこの式。
def detect_outliers_iqr(data, k=1.5): """IQR ベース。k=1.5 が標準、k=3.0 で保守的""" q1, q3 = np.percentile(data, [25, 75]) iqr = q3 - q1 lower = q1 - k * iqr upper = q3 + k * iqr return (data < lower) | (data > upper), (lower, upper)
mask, bounds = detect_outliers_iqr(data, k=1.5)print(f"IQR k=1.5: {mask.sum()} 件 / 範囲 [{bounds[0]:.0f}, {bounds[1]:.0f}]")
mask3, bounds3 = detect_outliers_iqr(data, k=3.0)print(f"IQR k=3.0: {mask3.sum()} 件 / 範囲 [{bounds3[0]:.0f}, {bounds3[1]:.0f}]")手法 ②:MAD(Median Absolute Deviation)
「中央値からの絶対偏差の中央値」で異常度を測る手法。外れ値が指標に影響しないため、IQR よりさらに頑健。Modified Z-score とも呼ばれる。
def detect_outliers_mad(data, threshold=3.5): """MAD ベース Modified Z-score。 0.6745 は正規分布における MAD と σ の関係係数。""" median = np.median(data) mad = np.median(np.abs(data - median)) if mad == 0: return np.zeros(len(data), dtype=bool) modified_z = 0.6745 * (data - median) / mad return np.abs(modified_z) > threshold
mask = detect_outliers_mad(data, threshold=3.5)print(f"MAD: {mask.sum()} 件除去")# 業界別の推奨閾値:# 金融: 3.5 (False positive 抑える)# 医療: 2.5 (見逃しを防ぐ)# 製造: 4.0 (アラート疲れを防ぐ)手法 ③:Winsorize(クリップ)
「除去」ではなく「境界値に丸める」手法。情報を失わないのが最大の利点。データ件数を維持できるため、その後の分析が安定する。
from scipy.stats.mstats import winsorize
# 上下 5% をクリップwinsorized = winsorize(data, limits=[0.05, 0.05])
# 自前実装の場合def winsorize_manual(data, lower_pct=5, upper_pct=95): lower = np.percentile(data, lower_pct) upper = np.percentile(data, upper_pct) return np.clip(data, lower, upper)
clipped = winsorize_manual(data, 5, 95)print(f"元: 平均 {data.mean():.0f} / 標準偏差 {data.std():.0f}")print(f"Winsorize: 平均 {clipped.mean():.0f} / 標準偏差 {clipped.std():.0f}")# 平均と分散が縮むが、件数は維持される手法 ④:パーセンタイル直クリップ
最もシンプル。「上位 1% / 下位 1% を除去」というドメインに依存しないルール。サンプル数が大きいときは IQR / MAD と同等の効果。
手法 ⑤:ドメイン知識ベース
業務上ありえない値を機械的に除去。「マイナス売上」「年齢 200 歳」「来店回数 -5 回」など。統計手法より優先される。
def clean_orders(df: pd.DataFrame) -> pd.DataFrame: """注文データのドメインルール""" # 1. ありえない値を除去 df = df[df["amount"] >= 0] # マイナス金額不可 df = df[df["amount"] <= 10_000_000] # 1000 万超は要確認 df = df[df["order_date"] >= "2020-01-01"] # 開業前のデータ不可
# 2. ドメイン外を NaN に df.loc[df["age"].isin([0, 200]), "age"] = np.nan
# 3. その後で IQR / MAD を適用(残った中で) mask = detect_outliers_iqr(df["amount"].values, k=3.0) df = df[~mask] return df手法比較:同じデータで結果がどう違うか
methods = { "3σ rule": (data > data.mean() + 3*data.std()), "IQR (k=1.5)": detect_outliers_iqr(data, k=1.5)[0], "IQR (k=3.0)": detect_outliers_iqr(data, k=3.0)[0], "MAD (3.5)": detect_outliers_mad(data, 3.5), "Percentile 99%": data > np.percentile(data, 99),}
results = pd.DataFrame({ "method": list(methods.keys()), "removed_count": [m.sum() for m in methods.values()], "removed_pct": [m.sum() / len(data) * 100 for m in methods.values()],})print(results)# 出力例(同じデータでも手法によって 1〜5 倍違う)# method removed_count removed_pct# 0 3σ rule 287 2.87# 1 IQR (k=1.5) 312 3.12# 2 IQR (k=3.0) 61 0.61# 3 MAD (3.5) 97 0.97# 4 Percentile 99% 100 1.00可視化:ボックスプロットで全方法を見比べる
fig, axes = plt.subplots(1, 5, figsize=(15, 5))
for ax, (name, mask) in zip(axes, methods.items()): cleaned = data[~mask] ax.boxplot(cleaned, vert=True) ax.set_title(f"{name}\n残: {len(cleaned)} 件") ax.set_yscale("log") # log scale で見やすく
plt.tight_layout()plt.savefig("outlier_methods_comparison.png", dpi=120)推奨フロー
- 1まず分布を見る:histogram, ボックスプロット → 正規?歪み?
- 2ドメインルールで明らかな異常を除去:マイナス値、桁違い等
- 3残りに対して IQR (k=3) or MAD:削除を保守的に
- 4重要分析の前は Winsorize 推奨:件数を維持して分散安定
- 5「除去」と「Winsorize」を別カラムで保持:後から手法変更可
前処理段階か異常検知段階か
前処理段階の外れ値は「明らかなデータ品質エラー」(マイナス売上、入力ミス)。 シリーズの異常検知は「異常自体が分析対象」(不正検知、故障予測)。用途を混同しない。
ふくふくの進め方
「外れ値処理で精度が安定しない」というご相談には、分布診断 → 適切な手法選定 → ドメインルール組込みを 1 週間で。「除去」より「Winsorize」が無難なケースが多いです。統計的処理 + ビジネスルールのハイブリッド設計が、現場で破綻しない秘訣です。
次回予告
EP.04 は型変換。日付・数値・カテゴリの地雷集を扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。