📚 背景知識(読んでから問題へ)
🎯
Day 052 — 交差検証でモデル評価の設計を学んだ次のステップ。Kaggleで最もよく使われる分類アルゴリズムの基礎、ロジスティック回帰を実装します。
「ある人がローンを返済できるか?」「このメールはスパムか?」—— こうした Yes/No の2択を確率として予測するのがロジスティック回帰です。
⚠️
名前に「回帰」が付いていますが、実際は分類アルゴリズムです。出力が 0〜1 の「確率」であることが特徴です。
| 手法 | 出力 | 用途 |
|---|---|---|
| 線形回帰 | 連続値(-∞ ~ +∞) | 家賃予測・売上予測 |
| ロジスティック回帰 | 確率(0 ~ 1) | 生存予測・スパム判定・離脱予測 |
📈 シグモイド関数(S字曲線の直感的理解)
線形回帰は -∞ ~ +∞ の値を出しますが、確率は 0 ~ 1 の範囲でなければなりません。シグモイド関数はどんな値でも 0〜1 に押し込む「変換装置」です。
シグモイド曲線
決定境界(p=0.5)
| z の値 | シグモイド出力 | 解釈 |
|---|---|---|
z ≫ 0(大きい正) | ≈ 1.0 | クラス1の確率が非常に高い |
z = 0 | = 0.5 | 五分五分(決定境界) |
z ≪ 0(大きい負) | ≈ 0.0 | クラス0の確率が非常に高い |
🗂️ データスキーマ(Titanic Dataset)
| 特徴量 | 型 | 説明 | 処理 |
|---|---|---|---|
pclass | int | 客室クラス(1=一等, 2=二等, 3=三等) | そのまま使用 |
age | float | 年齢(欠損あり:約20%) | 中央値で補完 |
fare | float | 運賃 | そのまま使用 |
sibsp | int | 兄弟・配偶者の数 | そのまま使用 |
parch | int | 親・子の数 | そのまま使用 |
sex | str | 性別(male/female) | male=1, female=0 |
embarked | str | 乗船港(C/Q/S) | get_dummies(drop_first=True) |
survived | int | 目的変数(1=生存, 0=死亡) | y として使用 |
📊
Titanic データの生存率は約 38%(不均衡データ)。Accuracy だけでなく ROC-AUC や Recall も確認することが重要です。
📝 問題
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
accuracy_score, classification_report,
roc_auc_score, roc_curve, confusion_matrix
)
import matplotlib.pyplot as plt
# Titanic データの読み込み(seabornから)
import seaborn as sns
df = sns.load_dataset("titanic")
問1 — データの前処理
survivedを目的変数yにする- 特徴量
Xはpclass,age,fare,sibsp,parch,sex,embarkedを使う sexを数値化(male=1, female=0)embarkedをダミー変数化(pd.get_dummies,drop_first=True)ageの欠損値を中央値で埋めるembarkedの欠損値を最頻値で埋める
問2 — モデルの学習と確率予測
train_test_splitで 80:20 に分割(stratify=y,random_state=42)Pipeline([("scaler", StandardScaler()), ("model", LogisticRegression(max_iter=500))])を使って学習- テストデータで
predict_probaを使い、クラス1(生存)の確率を取得 classification_reportを出力する
問3 — ROC-AUC の計算と曲線の描画
roc_auc_scoreで AUC を計算するroc_curveで FPR, TPR を取得し、matplotlib でROC曲線を描画する- グラフに AUC の値をタイトルまたはラベルに含める
問4 — 係数の解釈
pipeline.named_steps["model"].coef_[0]で係数を取得する- 特徴量名と係数をデータフレームにまとめ、係数の絶対値で降順に並べて表示する
- 最も生存に正の影響を与える特徴量と、負の影響を与える特徴量を1つずつ答えよ
問5 — 閾値の変更による影響
- デフォルト閾値(0.5)の Accuracy と Recall(生存クラス)を計算する
- 閾値を 0.3 に変更したとき(
pred = (proba >= 0.3).astype(int))の Accuracy と Recall を計算する - なぜ閾値を下げると Recall が上がるのかを言葉で説明せよ
🔍 ヒント(段階的開示)
ヒント1 — 方向性
- 問1:
df["sex"] = df["sex"].map({"male": 1, "female": 0})、欠損は.fillna(df["age"].median()) - 問2:
pipeline.predict_proba(X_test)[:, 1]でクラス1の確率を取得 - 問3:
from sklearn.metrics import roc_curve, roc_auc_score - 問4:
pd.Series(coef, index=feature_names).abs().sort_values(ascending=False) - 問5:
(proba >= threshold).astype(int)で閾値を変えた予測値を作る
ヒント2 — アプローチ
# 前処理の骨格
df["sex"] = df["sex"].map({"male": 1, "female": 0})
df["age"] = df["age"].fillna(df["age"].median())
df["embarked"] = df["embarked"].fillna(df["embarked"].mode()[0])
embarked_dummies = pd.get_dummies(df["embarked"], prefix="embarked", drop_first=True)
feature_cols = ["pclass", "age", "fare", "sibsp", "parch", "sex"]
X = pd.concat([df[feature_cols], embarked_dummies], axis=1)
y = df["survived"]
ヒント3 — コード骨格(ほぼ答え)
# ROC曲線の描画
fpr, tpr, thresholds = roc_curve(y_test, proba)
auc_score = roc_auc_score(y_test, proba)
plt.figure(figsize=(7, 5))
plt.plot(fpr, tpr, color="royalblue", lw=2, label=f"ROC curve (AUC = {auc_score:.4f})")
plt.plot([0, 1], [0, 1], "k--", lw=1, label="Random classifier")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve — Titanic Survival Prediction")
plt.legend()
plt.tight_layout()
plt.show()
✅ 模範解答
import pandas as pd
import numpy as np
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
accuracy_score, classification_report,
roc_auc_score, roc_curve, recall_score
)
import matplotlib.pyplot as plt
# ── データ読み込み ──
df = sns.load_dataset("titanic")
# ── 問1: 前処理 ──
df["sex"] = df["sex"].map({"male": 1, "female": 0})
df["age"] = df["age"].fillna(df["age"].median())
df["embarked"] = df["embarked"].fillna(df["embarked"].mode()[0])
embarked_dummies = pd.get_dummies(df["embarked"], prefix="embarked", drop_first=True)
feature_cols = ["pclass", "age", "fare", "sibsp", "parch", "sex"]
X = pd.concat([df[feature_cols], embarked_dummies], axis=1)
y = df["survived"]
print(f"データ形状: {X.shape}")
print(f"生存率: {y.mean():.3f}")
# ── 問2: 学習と予測 ──
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=500, random_state=42))
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
proba = pipeline.predict_proba(X_test)[:, 1]
print("\n[Classification Report]")
print(classification_report(y_test, y_pred, target_names=["死亡(0)", "生存(1)"]))
# ── 問3: ROC-AUC ──
auc_score = roc_auc_score(y_test, proba)
print(f"ROC-AUC: {auc_score:.4f}")
fpr, tpr, _ = roc_curve(y_test, proba)
plt.figure(figsize=(7, 5))
plt.plot(fpr, tpr, color="royalblue", lw=2, label=f"ROC curve (AUC = {auc_score:.4f})")
plt.plot([0, 1], [0, 1], "k--", lw=1, label="Random classifier")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve — Titanic Survival Prediction")
plt.legend()
plt.tight_layout()
plt.show()
# ── 問4: 係数の解釈 ──
coef = pipeline.named_steps["model"].coef_[0]
feature_names = X.columns.tolist()
coef_df = pd.DataFrame({
"feature": feature_names,
"coefficient": coef
}).sort_values("coefficient", key=abs, ascending=False)
print("\n[係数ランキング(絶対値降順)]")
print(coef_df.to_string(index=False))
print(f"\n生存に最も正の影響: {coef_df[coef_df['coefficient'] > 0].iloc[0]['feature']}")
print(f"生存に最も負の影響: {coef_df[coef_df['coefficient'] < 0].iloc[0]['feature']}")
# ── 問5: 閾値の変更 ──
for threshold in [0.5, 0.3]:
pred = (proba >= threshold).astype(int)
acc = accuracy_score(y_test, pred)
rec = recall_score(y_test, pred)
print(f"\n閾値={threshold:.1f}: Accuracy={acc:.4f}, Recall(生存)={rec:.4f}")
print("\n【解説】閾値を下げる = より積極的に「生存」と予測する")
print("→ 本来生存だったのに「死亡」と間違えるケース(偽陰性)が減り、Recallが上がる")
📌
期待される結果の傾向:
ROC-AUC ≈ 0.85 前後(Titanic の LogisticRegression の典型的な性能)
係数の絶対値が最大 →
閾値 0.3 では Recall が大幅に上がるが Accuracy・Precision は下がる
ROC-AUC ≈ 0.85 前後(Titanic の LogisticRegression の典型的な性能)
係数の絶対値が最大 →
sex(女性が生存しやすいという歴史的事実を反映)閾値 0.3 では Recall が大幅に上がるが Accuracy・Precision は下がる
📊 係数チャート(Titanic 典型値)
StandardScaler 適用後の係数。絶対値が大きいほど予測への影響が大きい。
sex
−1.35
pclass
−0.83
age
−0.45
fare
+0.68
sibsp
−0.30
embarked_Q
+0.22
💡
sex の係数が最大 — Titanic では「女性・子供を先に」という原則があり、女性(female=0, male=1 → 係数が負)の生存率が圧倒的に高い。歴史的事実がモデルの係数に反映されています。
📈 ROC曲線の概念図
横軸: FPR(偽陽性率)、縦軸: TPR(真陽性率=Recall)。曲線が左上に近いほど良いモデル。
LogisticRegression (Titanic)
Random classifier
⚖️ 閾値(Threshold)の変更と Precision-Recall トレードオフ
デフォルト: 閾値 = 0.5
≈ 80%
Accuracy
≈ 72%
Recall(生存クラス)
バランスの取れた予測。「死亡と予測して実は生存」というケースが多め。
カスタム: 閾値 = 0.3
≈ 74%
Accuracy(低下)
≈ 88%
Recall(生存クラス)大幅向上
「生存をより多く検出」。救命活動など「見逃しを減らしたい」場面に適切。
🪜 Step-by-Step 解説
1 カテゴリ変数の数値化
# sex: male=1, female=0(二値なので map が簡単)
df["sex"] = df["sex"].map({"male": 1, "female": 0})
# embarked: C/Q/S の3値 → one-hot encoding
# drop_first=True で多重共線性を防ぐ(C を基準とする)
embarked_dummies = pd.get_dummies(df["embarked"], prefix="embarked", drop_first=True)
# → embarked_Q, embarked_S の2列になる(embarked_C が落ちる)
drop_first=True を必ず使う理由:3つのカラムを作ると「3つ目 = 1 - 1つ目 - 2つ目」という関係が成立し、情報が重複(多重共線性)するためです。
2 predict と predict_proba の違い
y_pred = pipeline.predict(X_test) # クラス(0 or 1)
y_proba = pipeline.predict_proba(X_test) # 各クラスの確率 [[0.3, 0.7], ...]
proba1 = pipeline.predict_proba(X_test)[:, 1] # クラス1の確率のみ取得
確率を持っておくと、閾値変更・ROC-AUC 計算・スタッキングなど幅広く使えます。
3 ROC-AUC の意味
from sklearn.metrics import roc_auc_score, roc_curve
auc = roc_auc_score(y_test, proba)
# AUC = ランダムに選んだ正例と負例を比較したとき、正例の方が
# 高いスコアを持つ確率
# → AUC=1.0 が完璧、AUC=0.5 がランダムと同等
fpr, tpr, thresholds = roc_curve(y_test, proba)
# fpr: 各閾値での偽陽性率
# tpr: 各閾値での真陽性率(= Recall)
# thresholds: 対応する閾値リスト
4 係数の解釈(オッズ比)
import numpy as np
coef = pipeline.named_steps["model"].coef_[0]
feature_names = X.columns.tolist()
# exp(coef) = オッズ比
odds_ratio = np.exp(coef)
# sex の係数が -1.35 なら → exp(-1.35) ≈ 0.26
# つまり「male(sex=1)であると、生存のオッズが 0.26 倍(=74%減少)」
5 閾値(Threshold)の調整
from sklearn.metrics import accuracy_score, recall_score
for threshold in [0.5, 0.3]:
pred = (proba >= threshold).astype(int)
acc = accuracy_score(y_test, pred)
rec = recall_score(y_test, pred)
print(f"閾値={threshold:.1f}: Accuracy={acc:.4f}, Recall={rec:.4f}")
# Recall = TP / (TP + FN)
# FN = 「実際は生存だが死亡と予測」
# → 閾値を下げるとFNが減りRecallが上がる
# → ただし偽陽性(死亡なのに生存と予測)が増えPrecisionは下がる
🔢 数学・統計の補足(文系向け)
シグモイド関数を身近な例えで
体重計の数値をどんなに大きくても「100点満点」に変換する装置を想像してください。シグモイド関数は「どんな数値入力でも 0〜1 の確率に変換する装置」です。
| z の値 | σ(z) | 日本語の解釈 |
|---|---|---|
| z = 2 | ≈ 0.88 | 88%の確率でクラス1 |
| z = 0 | = 0.50 | 五分五分(決定境界) |
| z = -2 | ≈ 0.12 | 12%の確率でクラス1 |
オッズ比(Odds Ratio)
| 係数 β | exp(β)(オッズ比) | 解釈 |
|---|---|---|
β = 0.5 | ≈ 1.65 | 1特徴量増えるたびにオッズが1.65倍 |
β = 0 | = 1.0 | 影響なし |
β = -1.35 | ≈ 0.26 | オッズが0.26倍(74%減少) |
正則化パラメータ C
| C の値 | 正則化の強さ | 特徴 | 使い所 |
|---|---|---|---|
C = 0.01 | 強い | 係数が小さく保たれる → 過学習防止 | 特徴量が多い・ノイズが多い |
C = 1.0 | 標準(デフォルト) | バランスが良い | まず試す値 |
C = 100 | 弱い | 係数が自由になる → 過学習しやすい | データが豊富・ノイズが少ない |
🏆 Kaggleでの実践的な使い方
| 用途 | 説明 | コンペ例 |
|---|---|---|
| ベースラインモデル | シンプルで解釈性が高い → まず LogisticRegression で確認 | Titanic, Credit Risk |
| 確率キャリブレーション | predict_proba の出力が本当に確率か確認 | 保険コンペ |
| スタッキングの1層目 | 弱い予測器として GBDTの入力に使う | ほぼ全てのコンペ |
| 解釈・説明 | 係数でビジネス向けの説明が可能 | 金融・医療系コンペ |
# Kaggle 実践: cross_val_score で AUC を評価
from sklearn.model_selection import StratifiedKFold, cross_val_score
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=500, C=1.0))
])
auc_scores = cross_val_score(pipeline, X, y, cv=cv, scoring="roc_auc", n_jobs=-1)
print(f"CV AUC: {auc_scores.mean():.4f} ± {auc_scores.std():.4f}")
# C パラメータ(正則化の強さの逆数)を変えて最適化することもできる
# C の比較(Optuna の前哨戦)
for c in [0.01, 0.1, 1.0, 10.0, 100.0]:
pipe = Pipeline([("scaler", StandardScaler()),
("model", LogisticRegression(C=c, max_iter=500))])
score = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc").mean()
print(f"C={c:7.2f}: AUC={score:.4f}")
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| ロジスティック回帰は「回帰」だと思う | 名前が紛らわしい | 分類アルゴリズム。出力は確率(0〜1) |
predict だけ使い確率を捨てる |
predict があれば十分と思いがち | ROC-AUC・閾値調整には predict_proba が必須 |
| カテゴリ変数をそのまま渡す | エンコードを忘れる | get_dummies / LabelEncoder で数値化する |
max_iter の警告を無視する |
エラーではなく警告だから | 収束していない = 係数が不正確。max_iter を増やす |
C=1.0(デフォルト)から変えない |
パラメータを知らない | C が小さい → 強い正則化(過学習防止)、C が大きい → 弱い正則化 |
neg_mean_squared_error の符号を忘れる |
cross_val_score の符号反転に気づかない | -scores.mean() で正の値にする(回帰時) |
🚀 次のステップ
- 発展:
LogisticRegression(C=0.01)とC=100でスコアを比較し、正則化の効果を体験する - 次回予告 (Day 053): ロジスティック回帰の正則化と多クラス分類 —
multi_class="multinomial"、Iris データセットで3クラス分類を実装します
📋 自己評価(解いた後に記入)
自分の回答・気づき・メモ: