📚 背景知識(読んでから問題へ)
線形回帰とは?身近な例で理解する
「家の面積が広いほど家賃は高い」——この直感的な関係を数式で表すのが線形回帰です。
家賃 = 傾き × 面積 + 切片(ベースとなる金額)
| 用語 | 英語名 | 意味 |
|---|---|---|
| 傾き(係数) | coef_ / slope | 面積1㎡増加で家賃が何万円変わるか |
| 切片 | intercept_ | 面積=0のときの理論上のベース値 |
| 残差 | residual | 実際の値 − 予測値のズレ |
| 最小二乗法 | OLS | 残差の二乗和を最小化して係数を決める手法 |
最小二乗法とは?
予測値と実際の値の差(残差)を最小化するように直線を引く方法です。「なぜ二乗?」→ 残差にはプラスもマイナスもあるため、そのまま足すと打ち消し合います。二乗することで符号をなくし「ズレの大きさ」だけを測ります。
単回帰 vs 重回帰
| 種類 | 数式 | 特徴変数数 |
|---|---|---|
| 単回帰 | y = β₀ + β₁x + ε | 1つ |
| 重回帰 | y = β₀ + β₁x₁ + β₂x₂ + … + ε | 複数 |
🗂️ データスキーマ(今回使用:家賃データ 200件)
| 列名 | 型 | 説明 | 生成方法 / 真の係数 |
|---|---|---|---|
area | float64 | 面積(㎡)15〜100 | N(50, 15) クリップ | 真の係数: +0.8 |
age | float64 | 築年数(年)0〜50 | N(20, 10) クリップ | 真の係数: −0.3 |
dist | float64 | 最寄り駅距離(分)1〜30 | N(10, 5) クリップ | 真の係数: −0.5 |
rent | float64 | 月額家賃(万円)3〜50 | 0.8·area − 0.3·age − 0.5·dist + N(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 — 理論問題
以下の空欄を日本語で答えなさい:
- 残差の二乗の合計を最小化することを( )法という
- R² が 1.0 のとき、モデルは実データを( )説明できている
- 重回帰で係数がマイナスの特徴量は、その値が増えると目的変数が( )することを意味する
🔍 ヒント(段階的開示)
ヒント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. 係数がマイナス → その変数が増えると目的変数は減少する")
単回帰 coef_ ≈ 0.79(真値 0.8 に近い) | R²(単回帰) ≈ 0.54 | R²(重回帰) ≈ 0.69
重回帰係数: area≈+0.78, age≈−0.27, dist≈−0.45(ノイズのため真値と少しずれる)
📊 重回帰の係数(可視化)
係数の絶対値が大きいほど「その特徴量が家賃に強く影響する」。正の係数=増えると家賃アップ、負の係数=増えると家賃ダウン。
StandardScaler で標準化後に学習する必要があります。📈 R²(決定係数)の比較
| R²の値 | 意味 | 評価 |
|---|---|---|
| 1.0 | 残差ゼロ、完全な予測 | ※訓練データなら過学習を疑う |
| 0.7〜0.9 | 変動の70〜90%を説明 | 良好(回帰問題では十分な出発点) |
| 0.5〜0.7 | 変動の半分以上を説明 | 改善の余地あり |
| 0.0 | 平均値と同じ予測力 | モデルが役に立っていない |
| < 0 | 平均より悪い予測 | バグ・スケールミスを確認 |
📉 問2: 回帰直線のイメージ(area → rent)
🪜 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 増えると rent が coef_[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で重要になる