Day 016 — 分散・標準偏差・外れ値(実装編)

2026-04-25 白 / Phase 1 コーディング 分散・標準偏差・外れ値

📚 背景知識(読んでから問題へ)

標準偏差のおさらい(ひとことで言うと)

「データが平均からどれだけバラバラか」を数値で表したもの。

  • 標準偏差が小さい → みんな平均に近い(均一)
  • 標準偏差が大きい → バラバラ(ばらつきが大きい)

外れ値の定義(IQR法)

外れ値を検出する一般的な方法が「IQR法」です。

IQR(四分位範囲)= Q3(75パーセンタイル) - Q1(25パーセンタイル)

  • 下限: Q1 - 1.5 × IQR より小さい値
  • 上限: Q3 + 1.5 × IQR より大きい値

この範囲を外れた値を「外れ値」とみなします。

Z-スコア法(別の外れ値検出法)

「平均から何標準偏差分離れているか」で表す値。

Z-スコア = (データ値 - 平均) / 標準偏差

一般的に |Z-スコア| > 3 のデータを外れ値とみなすことが多い。


📝 問題

以下のデータ(ある商品の日次売上個数)について、Pythonで次の処理を実装してください。

import numpy as np
import pandas as pd

sales = [120, 135, 128, 122, 140, 131, 119, 125, 500, 130, 127, 133, 121, 3, 129]

タスク1: sales の基本統計量(平均・中央値・標準偏差)を計算して出力する

タスク2: IQR法を使って外れ値のインデックスと値を出力する

タスク3: Z-スコア法(閾値: |Z| > 2.5)を使って外れ値のインデックスと値を出力する

タスク4: 外れ値を除いたデータの平均と標準偏差を計算し、元のデータと比較する(IQR法の結果を使う)


🔍 ヒント(段階的開示)

ヒント1(方向性)

numpy と pandas の基本関数だけで解けます。for ループは最小限に。

ヒント2(アプローチ)
  • np.mean(), np.median(), np.std()
  • np.percentile(data, 25) で Q1、np.percentile(data, 75) で Q3
  • np.abs() で絶対値
  • boolean indexing でフィルタリング: data[mask]
ヒント3(コード骨格)
# IQR法
Q1 = np.percentile(sales_arr, 25)
Q3 = np.percentile(sales_arr, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outlier_mask = (sales_arr < lower) | (sales_arr > upper)
# outlier_mask が True の場所が外れ値

# Z-スコア法
z_scores = (sales_arr - np.mean(sales_arr)) / np.std(sales_arr)
outlier_mask_z = np.abs(z_scores) > 2.5

模範解答

import numpy as np
import pandas as pd

sales = [120, 135, 128, 122, 140, 131, 119, 125, 500, 130, 127, 133, 121, 3, 129]
sales_arr = np.array(sales)

# タスク1: 基本統計量
print("=== 基本統計量 ===")
print(f"平均:     {np.mean(sales_arr):.2f}")
print(f"中央値:   {np.median(sales_arr):.2f}")
print(f"標準偏差: {np.std(sales_arr):.2f}")

# タスク2: IQR法
print("\n=== IQR法による外れ値 ===")
Q1 = np.percentile(sales_arr, 25)
Q3 = np.percentile(sales_arr, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
print(f"Q1={Q1}, Q3={Q3}, IQR={IQR}")
print(f"下限: {lower:.2f}, 上限: {upper:.2f}")

outlier_mask_iqr = (sales_arr < lower) | (sales_arr > upper)
outlier_indices_iqr = np.where(outlier_mask_iqr)[0]
print(f"外れ値インデックス: {outlier_indices_iqr}")
print(f"外れ値の値: {sales_arr[outlier_mask_iqr]}")

# タスク3: Z-スコア法
print("\n=== Z-スコア法による外れ値(|Z| > 2.5)===")
z_scores = (sales_arr - np.mean(sales_arr)) / np.std(sales_arr)
outlier_mask_z = np.abs(z_scores) > 2.5
outlier_indices_z = np.where(outlier_mask_z)[0]
print(f"外れ値インデックス: {outlier_indices_z}")
print(f"外れ値の値: {sales_arr[outlier_mask_z]}")
print(f"Z-スコア: {z_scores[outlier_mask_z].round(2)}")

# タスク4: 外れ値除去後の比較
print("\n=== 外れ値除去前後の比較 ===")
clean_data = sales_arr[~outlier_mask_iqr]
print(f"元データ  → 平均: {np.mean(sales_arr):.2f}, 標準偏差: {np.std(sales_arr):.2f}")
print(f"除去後    → 平均: {np.mean(clean_data):.2f}, 標準偏差: {np.std(clean_data):.2f}")
print(f"除去した値: {sales_arr[outlier_mask_iqr]}")

期待される出力:

▶ 出力を見る
=== 基本統計量 ===
平均:     152.87
中央値:   129.00
標準偏差: 119.22

=== IQR法による外れ値 ===
Q1=122.0, Q3=133.0, IQR=11.0
下限: 105.50, 上限: 149.50
外れ値インデックス: [ 8 13]
外れ値の値: [500   3]

=== Z-スコア法による外れ値(|Z| > 2.5)===
外れ値インデックス: [ 8 13]
外れ値の値: [500   3]
Z-スコア: [ 2.91 -1.26]  ← 3は Z=約-1.26 で2.5未満のため IQR のみ検出

=== 外れ値除去前後の比較 ===
元データ  → 平均: 152.87, 標準偏差: 119.22
除去後    → 平均: 127.31, 標準偏差: 5.74
除去した値: [500   3]

🪜 Step-by-Step 解説

1
numpy配列への変換
sales_arr = np.array(sales)

リストのまま計算もできますが、numpy配列にしておくと boolean indexing など高速な操作が使えます。

2
基本統計量
np.mean(sales_arr)    # 算術平均
np.median(sales_arr)  # 中央値(ソートして真ん中の値)
np.std(sales_arr)     # 標準偏差(平均からの平均的な距離)

平均(152.87)と中央値(129.00)の差が大きいことに注目。これ自体が「外れ値がある」サインです。

3
IQR法の計算
Q1 = np.percentile(sales_arr, 25)  # 下から25%の位置の値
Q3 = np.percentile(sales_arr, 75)  # 下から75%の位置の値
IQR = Q3 - Q1                      # 中央50%の幅

IQR = 11 → データの中央50%は11の範囲に収まっている → 500と3は明らかに逸脱。

4
Boolean Mask によるフィルタリング
outlier_mask = (sales_arr < lower) | (sales_arr > upper)
# True/False の配列: [False, False, ..., True, ..., True, ...]
clean_data = sales_arr[~outlier_mask]  # ~ は否定(Falseの部分だけ取り出す)
5
Z-スコア法との比較

今回のデータでは 3 のZ-スコアは約 -1.26 で 2.5 を下回るため、Z-スコア法では検出されない(IQR法の方が敏感)。外れ値検出には複数の手法を試すのが実践的。


📐 数学・統計の補足(文系向け)

IQRとは何か: データを4等分したとき、上から25%〜75%の部分の幅。「真ん中の50%がどれだけ広がっているか」。

なぜ 1.5 倍か: 統計学者ジョン・テューキーが経験則として提案した値。正規分布なら全体の0.7%が外れ値として検出される。

Z-スコアが直感的な理由: 「500 は平均から 2.91 標準偏差分離れている」= 「普通のバラツキの約3倍離れた値」→ 異常と見なせる。


🏆 Kaggleでの実践的な使い方

外れ値の扱いはKaggleコンペで重要な特徴量エンジニアリングの一つ。

# Kaggleでよくやるパターン: 外れ値をクリッピング(切り捨てではなく上限に置き換え)
sales_clipped = np.clip(sales_arr, lower, upper)
# 500 → 149.5、3 → 105.5 に置き換え

# または対数変換で外れ値の影響を軽減
import numpy as np
sales_log = np.log1p(sales_arr)  # log(1+x) で0以下を安全に変換

コンペでは「外れ値を削除するか、クリッピングするか、対数変換するか」を試して交差検証で評価するのが定石。


⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
平均だけ見て外れ値を判断直感的でわかりやすい平均は外れ値の影響を強く受ける。中央値やIQRで補完する
外れ値は必ず削除すべき「データを綺麗にする」という思い込みドメイン知識で判断。売上500個は本当のバーゲンセール日かも
np.std() で標準偏差が違うサンプル標準偏差(ddof=1)との違いデフォルトは母標準偏差(ddof=0)。サンプルは np.std(x, ddof=1)

🚀 次のステップ

  • 発展: df['col'].clip(lower=Q1-1.5IQR, upper=Q3+1.5IQR) を使った DataFrame での外れ値処理
  • 次回予告: 確率の基礎(確率とは何か)→ Phase 1 テーマ3

🎯 自己評価

自分の回答

気づき・メモ