Day 049 — 線形回帰入門(最小二乗法・単回帰・重回帰)

2026-05-30 緑 / Phase 2 理論 線形回帰

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

🎉
Phase 2 スタート! Phase 1 では「データを見る・整理する」スキルを積みました。Phase 2 からはいよいよ 「予測する」 フェーズです。

線形回帰とは?身近な例で理解する

「家の面積が広いほど家賃は高い」——この直感的な関係を数式で表すのが線形回帰です。

家賃 = 傾き × 面積 + 切片(ベースとなる金額)
用語英語名意味
傾き(係数)coef_ / slope面積1㎡増加で家賃が何万円変わるか
切片intercept_面積=0のときの理論上のベース値
残差residual実際の値 − 予測値のズレ
最小二乗法OLS残差の二乗和を最小化して係数を決める手法

最小二乗法とは?

予測値と実際の値の差(残差)を最小化するように直線を引く方法です。「なぜ二乗?」→ 残差にはプラスもマイナスもあるため、そのまま足すと打ち消し合います。二乗することで符号をなくし「ズレの大きさ」だけを測ります。

💡
文系向けイメージ:的(実際の値)と矢(予測値)のズレを「ズレの二乗和」で測り、その合計が最小になるように矢の軌道(直線)を調整する。

単回帰 vs 重回帰

種類数式特徴変数数
単回帰y = β₀ + β₁x + ε1つ
重回帰y = β₀ + β₁x₁ + β₂x₂ + … + ε複数

🗂️ データスキーマ(今回使用:家賃データ 200件)

列名説明生成方法 / 真の係数
areafloat64面積(㎡)15〜100N(50, 15) クリップ | 真の係数: +0.8
agefloat64築年数(年)0〜50N(20, 10) クリップ | 真の係数: −0.3
distfloat64最寄り駅距離(分)1〜30N(10, 5) クリップ | 真の係数: −0.5
rentfloat64月額家賃(万円)3〜500.8·area − 0.3·age − 0.5·dist + N(0,5)
💡
学習後の係数(coef_)が 真の係数(0.8 / −0.3 / −0.5)に近ければ、モデルが正しくデータの構造を学習できた証拠です。ノイズがあるため完全には一致しません。

📝 問題

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt

np.random.seed(42)
n = 200

area   = np.random.normal(50, 15, n).clip(15, 100).round(0)
age    = np.random.normal(20, 10, n).clip(0, 50).round(0)
dist   = np.random.normal(10, 5, n).clip(1, 30).round(0)
rent   = 0.8 * area - 0.3 * age - 0.5 * dist + np.random.normal(0, 5, n)
rent   = rent.clip(3, 50).round(1)

df = pd.DataFrame({"area": area, "age": age, "dist": dist, "rent": rent})

問1 — 単回帰: area → rent

  • area のみを特徴量として LinearRegression で単回帰を実装する
  • 学習した 傾き(coef_)と切片(intercept_) を表示し、「面積が1㎡増えると家賃は何万円上がるか」を解釈する
  • R²スコアRMSEを表示する

問2 — 単回帰の可視化

  • area(x軸)と rent(y軸)の散布図を描き、回帰直線を重ねて描画する
  • タイトルに R² と傾きを表示する

問3 — 重回帰: area + age + dist → rent

  • 3つの特徴量すべてを使って重回帰を実装する
  • 各特徴量の係数を表示し、「どの特徴量が家賃に最も影響するか」を係数の絶対値で判断する
  • 単回帰(問1)と重回帰の R² スコアを比較する

問4 — 理論問題

以下の空欄を日本語で答えなさい:

  1. 残差の二乗の合計を最小化することを(  )法という
  2. R² が 1.0 のとき、モデルは実データを(  )説明できている
  3. 重回帰で係数がマイナスの特徴量は、その値が増えると目的変数が(  )することを意味する

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問1: LinearRegression の主要属性: coef_(係数)、intercept_(切片)
  • 問1: r2_score(y_true, y_pred) で R²、np.sqrt(mean_squared_error(...)) で RMSE
  • 問2: 散布図は plt.scatter、直線は plt.plot(x_sorted, y_pred_sorted)
  • 問3: X_multi = df[["area", "age", "dist"]] と複数列を指定
  • 問4: キーワード: 最小二乗、完全に、減少
ヒント2 — アプローチ
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

# 単回帰
X_single = df[["area"]]  # 2次元配列が必要なので [[]]
y = df["rent"]

model = LinearRegression()
model.fit(X_single, y)
y_pred = model.predict(X_single)

print(f"傾き: {model.coef_[0]:.3f}")
print(f"切片: {model.intercept_:.3f}")
print(f"R²: {r2_score(y, y_pred):.4f}")
print(f"RMSE: {np.sqrt(mean_squared_error(y, y_pred)):.4f}")
ヒント3 — コード骨格(ほぼ答え)
# 問3: 重回帰
X_multi = df[["area", "age", "dist"]]
model_multi = LinearRegression()
model_multi.fit(X_multi, y)
y_pred_multi = model_multi.predict(X_multi)

for col, coef in zip(["area", "age", "dist"], model_multi.coef_):
    print(f"{col}: {coef:+.3f}")

print(f"R²(単回帰): {r2_score(y, y_pred_single):.4f}")
print(f"R²(重回帰): {r2_score(y, y_pred_multi):.4f}")

# 問4の答え: 1.最小二乗  2.完全に  3.減少

模範解答

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt

np.random.seed(42)
n = 200

area   = np.random.normal(50, 15, n).clip(15, 100).round(0)
age    = np.random.normal(20, 10, n).clip(0, 50).round(0)
dist   = np.random.normal(10, 5, n).clip(1, 30).round(0)
rent   = 0.8 * area - 0.3 * age - 0.5 * dist + np.random.normal(0, 5, n)
rent   = rent.clip(3, 50).round(1)
df = pd.DataFrame({"area": area, "age": age, "dist": dist, "rent": rent})
y = df["rent"]

# ── 問1: 単回帰 ──
X_single = df[["area"]]
model_single = LinearRegression()
model_single.fit(X_single, y)
y_pred_single = model_single.predict(X_single)

print(f"傾き (coef_)    : {model_single.coef_[0]:.3f}")
print(f"切片 (intercept_): {model_single.intercept_:.3f}")
print(f"→ 面積1㎡増加で家賃 {model_single.coef_[0]:.2f}万円上昇")
print(f"R²  : {r2_score(y, y_pred_single):.4f}")
print(f"RMSE: {np.sqrt(mean_squared_error(y, y_pred_single)):.4f} 万円")

# ── 問2: 散布図 + 回帰直線 ──
x_line = np.linspace(df["area"].min(), df["area"].max(), 200).reshape(-1, 1)
y_line = model_single.predict(x_line)

plt.figure(figsize=(7, 4))
plt.scatter(df["area"], y, alpha=0.4, label="実データ", color="steelblue")
plt.plot(x_line, y_line, color="red", linewidth=2,
         label=f"回帰直線 (coef={model_single.coef_[0]:.2f})")
plt.xlabel("area (㎡)")
plt.ylabel("rent (万円)")
plt.title(f"単回帰: area → rent | R²={r2_score(y, y_pred_single):.3f}")
plt.legend()
plt.tight_layout()
plt.show()

# ── 問3: 重回帰 ──
X_multi = df[["area", "age", "dist"]]
model_multi = LinearRegression()
model_multi.fit(X_multi, y)
y_pred_multi = model_multi.predict(X_multi)

print("\n各特徴量の係数:")
for col, coef in zip(["area", "age", "dist"], model_multi.coef_):
    print(f"  {col:6s}: {coef:+.3f}")
print(f"切片: {model_multi.intercept_:.3f}")
print(f"\nR²(単回帰): {r2_score(y, y_pred_single):.4f}")
print(f"R²(重回帰): {r2_score(y, y_pred_multi):.4f}")

abs_coefs = pd.Series(np.abs(model_multi.coef_), index=["area", "age", "dist"])
print(f"\n影響力ランキング:\n{abs_coefs.sort_values(ascending=False)}")

# ── 問4: 理論 ──
print("\n問4:")
print("1. 残差の二乗の合計を最小化 → 最小二乗法")
print("2. R²=1.0 → データを完全に説明できている")
print("3. 係数がマイナス → その変数が増えると目的変数は減少する")
📌
実行結果(seed=42 固定)目安:
単回帰 coef_ ≈ 0.79(真値 0.8 に近い) | R²(単回帰) ≈ 0.54 | R²(重回帰) ≈ 0.69
重回帰係数: area≈+0.78, age≈−0.27, dist≈−0.45(ノイズのため真値と少しずれる)

📊 重回帰の係数(可視化)

係数の絶対値が大きいほど「その特徴量が家賃に強く影響する」。正の係数=増えると家賃アップ、負の係数=増えると家賃ダウン。

area +0.79
+0.79 (正・最大影響)
dist −0.45
−0.45 (負・中影響)
age −0.27
−0.27 (負・小影響)
⚠️
係数比較の注意点: 各変数のスケール(単位)が異なるため、係数をそのまま比較すると誤解が生じます。公平な比較には StandardScaler で標準化後に学習する必要があります。

📈 R²(決定係数)の比較

0.54
単回帰 R²
特徴量: area のみ
0.69
重回帰 R²
特徴量: area + age + dist
単回帰 (area のみ) R² = 0.54 重回帰 (area + age + dist) R² = 0.69
R²の値意味評価
1.0残差ゼロ、完全な予測※訓練データなら過学習を疑う
0.7〜0.9変動の70〜90%を説明良好(回帰問題では十分な出発点)
0.5〜0.7変動の半分以上を説明改善の余地あり
0.0平均値と同じ予測力モデルが役に立っていない
< 0平均より悪い予測バグ・スケールミスを確認

📉 問2: 回帰直線のイメージ(area → rent)

area (㎡) vs rent (万円) — 単回帰 15 35 55 75 100 area (㎡) → 5 20 35 50 rent (万円) coef≈0.79 残差 実データ 回帰直線 (R²≈0.54)

🪜 Step-by-Step 解説

1 なぜ X を [[]] で渡すのか(2次元配列の必要性)

X_single = df[["area"]]   # OK: shape (200, 1) の DataFrame
X_wrong  = df["area"]     # NG: shape (200,) の Series → ValueError

scikit-learnの fit は「行=サンプル数、列=特徴量数」の2次元配列を要求します。df["area"] は1次元(Series)なので df[["area"]] で括弧2つを使う必要があります。

2 coef_ と intercept_ の読み方

# モデル式: rent = coef_[0] * area + intercept_
# 例: rent = 0.79 * area + (-5.32)
print(f"傾き: {model.coef_[0]:.3f}")      # coef_ はリスト(重回帰で複数になる)
print(f"切片: {model.intercept_:.3f}")    # intercept_ はスカラー

傾き(係数)の意味: area が 1 増えると rentcoef_[0] 万円変化する。符号がプラスなら増加、マイナスなら減少。

3 R²(決定係数)の直感的な理解

# R² = 1 - (予測残差の二乗和) / (全分散の二乗和)
# sklearn では
from sklearn.metrics import r2_score
r2 = r2_score(y_true, y_pred)

R² は「データの変動のうち、モデルが説明できた割合」。R²=0.69 なら「家賃の変動の69%を面積・築年数・駅距離で説明できた」という意味です。

4 RMSE の単位は目的変数と同じ

from sklearn.metrics import mean_squared_error
rmse = np.sqrt(mean_squared_error(y, y_pred))
print(f"RMSE: {rmse:.2f} 万円")
# → 平均でこのくらいの誤差がある、という絶対量の評価

rent が「万円」ならRMSEも「万円」。R² が相対評価(比率)、RMSEが絶対評価。両方をセットで報告するのがKaggle式です。

5 重回帰で複数係数を確認する

for col, coef in zip(["area", "age", "dist"], model_multi.coef_):
    print(f"{col}: {coef:+.3f}")
# area: +0.784   → 1㎡広がると家賃+0.78万円
# age : -0.274   → 1年古くなると家賃-0.27万円
# dist: -0.451   → 駅1分遠くなると家賃-0.45万円

+ フォーマット指定子で正負の符号を明示。係数の符号が真値(+0.8, −0.3, −0.5)と一致していれば、モデルが正しく学習できた証拠です。

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

最小二乗法の「正規方程式」

微分を使って残差の二乗和を最小化すると、以下の方程式(正規方程式)が得られます:

β = (XᵀX)⁻¹ Xᵀ y
# X: 特徴量行列, y: 目的変数ベクトル, β: 係数ベクトル

難しそうに見えますが「行列演算」だけ。scikit-learn はこれを内部で計算するので .fit() を呼ぶだけでOKです。

なぜ係数が「真の値」とずれるのか?

ノイズ np.random.normal(0, 5, n) を加えているため。現実データにも「説明できない偶然のバラつき」があり、係数はその影響でずれます。サンプル数 n を大きくすると真の値に近づきます(大数の法則)。

真の係数学習後の係数(目安)差の原因
area: +0.80+0.78〜0.82ノイズによる推定誤差
age: −0.30−0.25〜−0.35ノイズによる推定誤差
dist: −0.50−0.45〜−0.55ノイズによる推定誤差

🏆 Kaggleでの実践的な使い方

線形回帰は単独でGoldメダルを取るモデルではありませんが、以下の場面で必須です:

用途説明重要度
ベースラインモデル最初に線形回帰で提出し改善の「起点」を作る必須
特徴量の寄与確認係数の絶対値でどの特徴量が効くか把握必須
リーク検出R² が異常に高い特徴量はターゲットリークを疑う重要
スタッキングGBDTの予測値を線形回帰でブレンドする(最終層)上級
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import numpy as np

# Kaggle 標準: 5-fold CVで評価(単一のR²より信頼性が高い)
scores = cross_val_score(LinearRegression(), X, y, cv=5, scoring="r2")
print(f"CV R²: {scores.mean():.4f} ± {scores.std():.4f}")

# House Prices コンペのベースライン例
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

train = pd.read_csv("train.csv")
num_cols = train.select_dtypes(include="number").columns.drop("SalePrice")
X = train[num_cols].fillna(train[num_cols].median())
y = train["SalePrice"]

pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("lr", LinearRegression())
])
scores = cross_val_score(pipe, X, y, cv=5, scoring="r2")
print(f"Baseline CV R²: {scores.mean():.4f}")

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
df["area"] を X に渡してエラー Series と2次元配列の違いを知らない df[["area"]] で括弧2つ(2次元DataFrame)
R²が高い = 良いモデルと判断 訓練データへの当てはまりだけ見ている test データの R² を必ず確認(過学習チェック)
係数の大きさ = 特徴量の重要度 スケールが違う列を比較している 係数比較は StandardScaler 後に行う
線形回帰で分類問題を解く 回帰と分類の違いを混同 0/1 の分類にはロジスティック回帰を使う
RMSE が極小 = 完璧なモデル 訓練データで評価している テストデータか CV で評価しなければ意味がない

🚀 次のステップ

  • 発展: StandardScaler でスケーリング後に重回帰を実装し、係数を公平に比較する。また cross_val_score でCV評価を実施する
  • 次回予告 (Day 050): 評価指標の深掘り(MAE / RMSE / R² / MAPE の使い分けと実装)— 「どの指標を使うべきか」の判断がKaggleで重要になる

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

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