Day 050 — 評価指標の深掘り(MAE / RMSE / R² / MAPE)

2026-05-31 緑 / Phase 2 コーディング 評価指標

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

🎉
Day 050 到達! 線形回帰で予測できるようになりました。今日は「予測の良さ」を測る 評価指標 を完全マスターします。

モデルを作ったら「どれくらい良いのか?」を数値で測る必要があります。回帰モデルでは予測値と実際の値の「ズレ」を複数の角度から評価します。

なぜ複数の指標が必要なのか?

1つの指標だけでは「外れ値に弱い」「スケールに依存する」などの弱点が見えません。複数の指標をセットで見ることで、モデルの特性を正確に把握できます。

指標計算方法特徴単位
MAE|誤差| の平均直感的・外れ値に強い目的変数と同じ
RMSE誤差² の平均の √外れ値に敏感・大誤差をペナルティ目的変数と同じ
説明できた変動の割合相対評価・0〜1の範囲無次元
MAPE|誤差/実値| の平均 × 100%表示・スケール不変%
RMSLElog誤差の RMSE右スキュー分布に適用無次元

🃏 評価指標カード

MAE
Mean Absolute Error
予測誤差の絶対値を平均。外れ値の影響を受けにくく、「平均で何円外れているか」を直接表す
単位: 目的変数と同じ
RMSE
Root Mean Squared Error
誤差を二乗してから平均し √ を取る。大きな誤差に厳しいペナルティ。常に MAE 以上になる
単位: 目的変数と同じ
決定係数 (R-squared)
データの変動のうちモデルが説明できた割合。0〜1で 1 に近いほど良い。負になるとモデルが平均値より悪い
単位: 無次元(比率)
MAPE
Mean Absolute Percentage Error
誤差を実際の値で割って % 表示。スケールが異なる問題を比較可能。実値がゼロだと計算不能
単位: %
RMSLE
Root Mean Squared Log Error
log変換後の RMSE。大きな値のズレに寛容、小さな値のズレに厳格。House Prices コンペで使用
単位: 無次元

🗂️ データスキーマ(住宅価格データ 500件)

列名説明真の係数
areafloat64面積(㎡)15〜150+5.0
roomsint64部屋数 1〜6+8.0
agefloat64築年数(年)0〜60−1.5
pricefloat64住宅価格(万円)50〜800目的変数
💡
ノイズ σ=30 万円を含む。訓練後の RMSE がノイズ(30万円)に近ければ、モデルが真の構造を学習できた証拠です。

📝 問題

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

np.random.seed(42)
n = 500

area     = np.random.normal(60, 20, n).clip(15, 150).round(0)
rooms    = np.random.randint(1, 7, n).astype(float)
age      = np.random.normal(20, 12, n).clip(0, 60).round(0)
price = (
    5.0 * area + 8.0 * rooms - 1.5 * age + np.random.normal(0, 30, n)
).clip(50, 800).round(0)

df = pd.DataFrame({"area": area, "rooms": rooms, "age": age, "price": price})

問1 — 全指標の計算と比較

  • train_test_split で訓練80% / テスト20%に分割(random_state=42
  • 3特徴量(area, rooms, age)で LinearRegression を学習・予測する
  • テストデータで MAE, RMSE, R², MAPE をすべて計算・表示する
  • MAPEは 手動実装も行い sklearn の結果と一致することを確認する

問2 — 外れ値の影響を観察する

  • テストデータに人工的な外れ値を5件追加(price を実際の値の3倍に変換)
  • 外れ値追加前後で MAE と RMSE を比較し、どちらが外れ値に敏感かを数値で示す

問3 — 指標の関係を理解する

  1. 「RMSE ≥ MAE は常に成り立つ」 → seed 0〜9 の10ケースで全確認
  2. 「特徴量数が増えると R² は上がり RMSE は下がる(傾向)」 → 1〜3列で比較表を作成
  3. 「MAPE はスケール変換しても変わらない」 → price ÷ 100 した場合と比較

問4 — RMSLE の実装

  • 以下の式を自分で実装する: RMSLE = sqrt(mean((log(y_pred+1) - log(y_true+1))²))
  • sklearn.metrics.mean_squared_log_error の結果と一致することを確認する
  • 「なぜ House Prices コンペで RMSLE が使われるのか」を一文で説明する

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問1: from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, mean_absolute_percentage_error
  • 問1のMAPE手動実装: np.mean(np.abs((y_true - y_pred) / y_true)) * 100
  • 問2: y_test_out = y_test.copy() でコピーしてから変更する
  • 問3-1: for seed in range(10): でループ
  • 問4: np.log1p(x)log(x+1) と同義。負の予測値には np.clip(y_pred, 0, None)
ヒント2 — アプローチ
X = df[["area", "rooms", "age"]]
y = df["price"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

mae    = mean_absolute_error(y_test, y_pred)
rmse   = np.sqrt(mean_squared_error(y_test, y_pred))
r2     = r2_score(y_test, y_pred)
mape   = np.mean(np.abs((y_test - y_pred) / y_test)) * 100
ヒント3 — コード骨格(ほぼ答え)
# 問2: 外れ値追加
y_test_out = y_test.copy()
outlier_idx = y_test_out.index[:5]
y_test_out.loc[outlier_idx] = y_test_out.loc[outlier_idx] * 3

mae_out  = mean_absolute_error(y_test_out, y_pred)
rmse_out = np.sqrt(mean_squared_error(y_test_out, y_pred))
print(f"MAE  変化: {mae:.1f} → {mae_out:.1f} (×{mae_out/mae:.2f})")
print(f"RMSE 変化: {rmse:.1f} → {rmse_out:.1f} (×{rmse_out/rmse:.2f})")

# 問4: RMSLE
y_pred_clipped = np.clip(y_pred, 0, None)
rmsle_manual  = np.sqrt(np.mean((np.log1p(y_pred_clipped) - np.log1p(y_test.values))**2))
from sklearn.metrics import mean_squared_log_error
rmsle_sklearn = np.sqrt(mean_squared_log_error(y_test, y_pred_clipped))

模範解答

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
    mean_absolute_error, mean_squared_error, r2_score,
    mean_absolute_percentage_error, mean_squared_log_error
)

np.random.seed(42)
n = 500
area     = np.random.normal(60, 20, n).clip(15, 150).round(0)
rooms    = np.random.randint(1, 7, n).astype(float)
age      = np.random.normal(20, 12, n).clip(0, 60).round(0)
price    = (5.0*area + 8.0*rooms - 1.5*age + np.random.normal(0,30,n)).clip(50,800).round(0)
df = pd.DataFrame({"area": area, "rooms": rooms, "age": age, "price": price})

X = df[["area", "rooms", "age"]]
y = df["price"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# ── 問1 ──
mae       = mean_absolute_error(y_test, y_pred)
rmse      = np.sqrt(mean_squared_error(y_test, y_pred))
r2        = r2_score(y_test, y_pred)
mape_sk   = mean_absolute_percentage_error(y_test, y_pred) * 100
mape_man  = np.mean(np.abs((y_test - y_pred) / y_test)) * 100

print(f"MAE : {mae:.2f} 万円")
print(f"RMSE: {rmse:.2f} 万円")
print(f"R²  : {r2:.4f}")
print(f"MAPE(sklearn): {mape_sk:.2f}%  MAPE(手動): {mape_man:.2f}%")

# ── 問2 ──
y_test_out = y_test.copy()
y_test_out.loc[y_test_out.index[:5]] *= 3
mae_out  = mean_absolute_error(y_test_out, y_pred)
rmse_out = np.sqrt(mean_squared_error(y_test_out, y_pred))
print(f"MAE  変化: {mae:.1f} → {mae_out:.1f} (×{mae_out/mae:.2f})")
print(f"RMSE 変化: {rmse:.1f} → {rmse_out:.1f} (×{rmse_out/rmse:.2f})")

# ── 問3-1 ──
for seed in range(10):
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=seed)
    m = LinearRegression().fit(Xtr, ytr)
    yp = m.predict(Xte)
    _mae, _rmse = mean_absolute_error(yte,yp), np.sqrt(mean_squared_error(yte,yp))
    print(f"seed={seed}: MAE={_mae:.2f}, RMSE={_rmse:.2f}, RMSE>=MAE={_rmse>=_mae}")

# ── 問3-2 ──
for feats in [["area"], ["area","rooms"], ["area","rooms","age"]]:
    Xf = df[feats]
    Xtr, Xte, ytr, yte = train_test_split(Xf, y, test_size=0.2, random_state=42)
    m = LinearRegression().fit(Xtr, ytr)
    yp = m.predict(Xte)
    print(f"{feats}: R²={r2_score(yte,yp):.4f}, RMSE={np.sqrt(mean_squared_error(yte,yp)):.2f}")

# ── 問3-3 ──
mape_orig  = mean_absolute_percentage_error(y_test, y_pred) * 100
mape_scale = mean_absolute_percentage_error(y_test/100, y_pred/100) * 100
print(f"元スケール MAPE: {mape_orig:.4f}%  ÷100スケール MAPE: {mape_scale:.4f}%")

# ── 問4 ──
ypc = np.clip(y_pred, 0, None)
rmsle_manual  = np.sqrt(np.mean((np.log1p(ypc) - np.log1p(y_test.values))**2))
rmsle_sklearn = np.sqrt(mean_squared_log_error(y_test, ypc))
print(f"RMSLE 手動={rmsle_manual:.6f}  sklearn={rmsle_sklearn:.6f}")
print(f"House Prices 理由: 価格の分布が右スキューのため、相対誤差を均等評価するlog変換が適切")
📌
期待される実行結果(seed=42):
MAE ≈ 22〜25 万円 | RMSE ≈ 28〜33 万円(ノイズ30万円に近い) | R² ≈ 0.96〜0.97
外れ値追加後: RMSE の増加率 > MAE の増加率(RMSE の方が敏感)
MAPE は ÷100 しても同じ値になる(スケール不変)

外れ値の影響(MAE vs RMSE)

テストデータ 5件の price を3倍にしたとき、各指標がどれだけ変化するか。

MAE
外れ値前
≈23 万円
外れ値後
≈36 万円(×1.5)
RMSE
外れ値前
≈29 万円
外れ値後
≈65 万円(×2.2+)
⚠️
RMSE の方が外れ値に 2〜3倍 敏感に反応します。「二乗」という操作が大きな誤差を指数的に拡大するためです。外れ値が多いデータでは MAE の方が安定した評価ができます。

📊 評価指標の比較チャート

特徴量数と R² / RMSE の関係

area のみ +rooms +age 0.83 0.88 0.96 55 43 29 R²(↑高いほど良い) RMSE 万円(↓低いほど良い)

MAPE のスケール不変性

price (万円) スケール MAPE = 8.5% price ÷ 100 スケール MAPE = 8.5%(同じ!)
💡
MAPE は分子(誤差)と分母(実値)を同じ値で割るため、スケールが消えます。100円を1円と比較する場合でも同じ % になります。

🪜 Step-by-Step 解説

1 sklearn の指標関数と手動実装の対応

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# MAE: sklearn vs 手動
mae_sk  = mean_absolute_error(y_true, y_pred)
mae_man = np.mean(np.abs(y_true - y_pred))

# RMSE: sklearn vs 手動
rmse_sk  = np.sqrt(mean_squared_error(y_true, y_pred))
rmse_man = np.sqrt(np.mean((y_true - y_pred)**2))

# MAPE: sklearn vs 手動(× 100 で % に)
from sklearn.metrics import mean_absolute_percentage_error
mape_sk  = mean_absolute_percentage_error(y_true, y_pred) * 100
mape_man = np.mean(np.abs((y_true - y_pred) / y_true)) * 100

sklearn の mean_absolute_percentage_error は 0〜1 の範囲で返ってくるため、× 100 して % に変換します。

2 外れ値を人工的に追加する方法

# 元のデータをコピーしてから変更(元データを保護)
y_test_out = y_test.copy()

# index の最初の5件を3倍にする
outlier_idx = y_test_out.index[:5]
y_test_out.loc[outlier_idx] = y_test_out.loc[outlier_idx] * 3

# 変更後で評価
mae_out  = mean_absolute_error(y_test_out, y_pred)
rmse_out = np.sqrt(mean_squared_error(y_test_out, y_pred))

.copy() を忘れると元の y_test も変更されます。pandas はビューとコピーを自動で判断するため、明示的に .copy() を呼ぶ習慣を付けましょう。

3 RMSE ≥ MAE の数学的証明(直感版)

# シミュレーションで確認
errors = np.array([10, 10, 10, 10])      # 均一な誤差
mae  = np.mean(np.abs(errors))           # → 10
rmse = np.sqrt(np.mean(errors**2))       # → 10  ← 均一ならMAE=RMSE

errors_var = np.array([2, 5, 10, 30])   # ばらつきある誤差
mae_v  = np.mean(np.abs(errors_var))    # → 11.75
rmse_v = np.sqrt(np.mean(errors_var**2))# → 16.2 > 11.75

誤差が均一なら MAE = RMSE。ばらつき(特に大きな外れ値)があると RMSE > MAE になります。

4 RMSLE の実装と使い所

# RMSLE の手動実装
y_pred_clipped = np.clip(y_pred, 0, None)  # 負の予測を防ぐ
rmsle = np.sqrt(np.mean(
    (np.log1p(y_pred_clipped) - np.log1p(y_test.values))**2
))

# 対数変換してから RMSE を最小化する学習パターン(Kaggle頻出)
y_train_log = np.log1p(y_train)
model_log = LinearRegression().fit(X_train, y_train_log)
y_pred_log = model_log.predict(X_test)
y_pred_original = np.expm1(y_pred_log)  # log1p の逆変換

np.log1p(x)log(x+1) と同じ。+1 する理由は 0 のときに log(0)=-∞ になるのを防ぐためです。

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

MAE と RMSE の違いを「点数」で理解する

シナリオ誤差の内訳MAERMSE解釈
均一な誤差全員 ±10 点1010同じ(外れ値なし)
1人だけ大外れ99人 ±0点 / 1人 -100点110RMSE が大きく反応
全員バラバラ1,5,10,30 点の誤差11.7516.2ばらつきで差が出る

R² の直感的な意味

# R² の計算式
# SS_res = 残差の二乗和(モデルの誤差)
# SS_tot = 全分散の二乗和(データのバラつき)
# R² = 1 - SS_res / SS_tot

# → 0の基準: モデル = "全部の平均値を予測" と同じ精度
# → R²=0.96: データのバラつきの96%をモデルが説明できた

RMSLE が right-skewed データに強い理由

住宅価格は「200万円の物件〜3億円の物件」と幅が広い。普通の RMSE だと 3億円の物件の誤差が支配的になる。log 変換すると大きな値の差が縮まり、小さな値の差が相対的に重視されます。

実価格予測価格誤差(万円)RMSE への影響RMSLE への影響
100万円200万円100万円100² = 10000log(201)-log(101) ≈ 0.69
10000万円10100万円100万円100² = 10000(同じ)log(10101)-log(10001) ≈ 0.01(小さい)

🏆 Kaggleでの実践的な使い方

コンペ公式メトリクス理由実装パターン
House PricesRMSLE価格レンジが広い・右スキューlog1p 変換して RMSE 最適化
Ames HousingRMSE (log)外れ値を重視したい同上
M5 ForecastingWRMSSE重み付き・階層的カスタム実装
医療費予測MAE外れ値の影響を抑えたい標準実装
# House Prices 対応パターン(RMSLE を最小化)
import numpy as np
from sklearn.linear_model import LinearRegression

# log変換してから学習
y_train_log = np.log1p(y_train)
model = LinearRegression()
model.fit(X_train, y_train_log)

# 予測して逆変換
y_pred_log   = model.predict(X_test)
y_pred_final = np.expm1(y_pred_log)  # np.expm1 = exp(x)-1(np.log1p の逆)

# RMSLE を評価
from sklearn.metrics import mean_squared_log_error
rmsle = np.sqrt(mean_squared_log_error(y_test, y_pred_final.clip(0)))
print(f"RMSLE: {rmsle:.4f}")

# Kaggle 標準: cross_val_score でCV評価
from sklearn.model_selection import cross_val_score
cv_scores = cross_val_score(
    LinearRegression(), X, np.log1p(y),
    cv=5, scoring="neg_root_mean_squared_error"
)
print(f"CV RMSE (log): {-cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
MAPE を 0〜1 で解釈する sklearn の返り値は小数 × 100 して % に変換する
mean_squared_error を RMSE と呼ぶ 名前の混同 np.sqrt(mean_squared_error(...)) が RMSE
R² が高い = テストでも良い 訓練データで評価している 必ず test か CV で評価する
RMSLE で負の予測値をそのまま使う clip を忘れる np.clip(y_pred, 0, None) で保護する
MAPE を使えばスケールの異なるモデルを比較できる 部分的に正しい 正しいが、実値が 0 に近い場合は MAPE が爆発するので注意

🚀 次のステップ

  • 発展: cross_val_score を使って、単一のテスト分割より信頼性の高い CV 評価を実施する。scoring="neg_mean_absolute_error" など各指標に対応したスコアリング文字列を習得する
  • 次回予告 (Day 051): 交差検証の基礎(K-Fold CV・Stratified K-Fold・CV の正しい使い方)— Kaggle上位勢が全員使う「データ分割戦略」の核心へ

📋 自己評価(解いた後に記入)

自分の回答・気づき・メモ: