ふくふくHukuhuku Inc.
EP.03Prep 12分公開: 2026-05-10

外れ値の検出と除去:3σは思ったより使えない

「3σ から外れたら除去」は正規分布前提。実データの多くは歪んだ分布で、IQR・MAD・Winsorize の方が安全。判定式と Python 実装、可視化まで。

#outlier#前処理
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

外れ値は 3σ ルールで除去」── 教科書では正解だが、実データの 8 割以上が裾の重い分布(log-normal、Pareto)で、3σ rule は正常なヘビーユーザーまで弾いてしまう。本記事では、より頑健な手法と、 シリーズとの棲み分けを実コード付きで扱います。

3σ rule の落とし穴を可視化する

まず実データに近い裾の重い分布を作って、3σ rule がどれだけ正常なデータを弾くかを見ます。

log-normal 分布で 3σ の挙動を確認
Python
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 のボックスプロットの「」表示はこの式。

IQR で外れ値検出
Python
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 とも呼ばれる。

MAD で外れ値検出
Python
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(クリップ)

「除去」ではなく「境界値に丸める」手法。情報を失わないのが最大の利点。データ件数を維持できるため、その後の分析が安定する。

Winsorize(5%/95% でクリップ)
Python
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 回」など。統計手法より優先される。

ドメインルールの組合せ
Python
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

手法比較:同じデータで結果がどう違うか

5 手法を一気に比較
Python
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

可視化:ボックスプロットで全方法を見比べる

5 手法の効果を視覚化
Python
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. 1まず分布を見る:histogram, ボックスプロット → 正規?歪み?
  2. 2ドメインルールで明らかな異常を除去:マイナス値、桁違い等
  3. 3残りに対して IQR (k=3) or MAD:削除を保守的に
  4. 4重要分析の前は Winsorize 推奨:件数を維持して分散安定
  5. 5「除去」と「Winsorize」を別カラムで保持:後から手法変更可

前処理段階か異常検知段階か

棲み分け

前処理段階の外れ値は「明らかなデータ品質エラー」(マイナス売上、入力ミス)。 シリーズの異常検知は「異常自体が分析対象」(不正検知、故障予測)。用途を混同しない

ふくふくの進め方

外れ値処理で精度が安定しない」というご相談には、分布診断 → 適切な手法選定 → ドメインルール組込みを 1 週間で。「除去」より「Winsorize」が無難なケースが多いです。統計的処理 + ビジネスルールのハイブリッド設計が、現場で破綻しない秘訣です。

次回予告

EP.04 は型変換。日付・数値・カテゴリの地雷集を扱います。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp