「scikit-learnで予測モデルを作ってみたけど、この予測って当たってるの?」
「R²が0.97って出たけど、それが良いのか悪いのか、正直よくわからない…」

機械学習を始めたばかりだと、こんなモヤモヤありますよね。分類(迷惑メールかどうか、など)の評価は「正解率」で直感的にわかりますが、家賃や売上のような「数値」を当てる回帰は、どれくらい外れたら失敗なのかがイメージしにくいところです。

この記事では、家賃のデータを使って scikit-learn の線形回帰で予測モデルを作り、

  • MAE・RMSE・R² で「どれくらい当たっているか」を数字で測る
  • 「平均で答えるだけのモデル」と比べて、本当に役に立っているか確かめる
  • 残差(実際−予測)をグラフにして、モデルの「クセ」を見つけて改善する

ところまで、実際にコードを動かしながら進めます。

  • 対象:Pythonの基本と pandas の読み込みくらいがわかる初〜中級者
  • 使うもの:Python 3、pandas、scikit-learn、matplotlib
  • 所要時間:30分ほど

「むずかしそう」を「できそう」に変えるつもりで、一緒に見ていきましょう!🚀

準備:ライブラリを入れる

apartment rent data analysis
apartment rent data analysis / Photo by Juan Pablo Daniel via Pexels

まだ入っていない方は、次のコマンドでインストールしてください(この記事は scikit-learn 1.9.0・pandas 3.0.3 で動作を確認しています)。

pip install pandas scikit-learn matplotlib

ステップ1:練習用の家賃データを作る

本物の物件データは手に入れにくいので、今回はPythonで練習用のデータを作ります。「広さ」「駅徒歩」「築年数」から家賃が決まる、架空の物件300件です。

ここが重要です👇

  • 家賃は「広さ1㎡で+2,000円、駅徒歩1分で−1,000円、築1年で−500円」という本当のルールで作っています
  • さらに「徒歩15分を超えると1分ごとに追加で−1,500円」という、ちょっとひねったルールをこっそり入れています(バス便エリアのイメージです)
  • モデルはこのルールを知りません。データだけからどこまで見抜けるかが今回のテーマです

答えを知っているデータで練習すると、「モデルが正しく学べたか」を答え合わせできるのがメリットです😊

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)  # 乱数の種を固定(毎回同じデータになる)
n = 300

area = rng.uniform(18, 60, n).round(1)   # 広さ(㎡)
walk = rng.integers(1, 26, n)            # 駅徒歩(分)
age = rng.integers(0, 41, n)             # 築年数(年)

# 「本当の家賃のルール」(モデルはこれを知らない)
rent = 55000 + 2000 * area - 1000 * walk - 500 * age
rent = rent - np.where(walk > 15, (walk - 15) * 1500, 0)  # 徒歩15分を超えると急に安くなる
rent = rent + rng.normal(0, 3000, n)                       # 物件ごとのばらつき

df = pd.DataFrame({
    "広さ": area,
    "駅徒歩": walk,
    "築年数": age,
    "家賃": ((rent / 100).round() * 100).astype(int),  # 100円単位に丸める
})
df.to_csv("rent.csv", index=False, encoding="utf-8-sig")

print(df.head())
print(f"件数: {len(df)}件 / 家賃の平均: {df['家賃'].mean():,.0f}円")

実行すると rent.csv ができて、先頭5件が表示されます。

     広さ  駅徒歩  築年数      家賃
0  50.5   16    0  142200
1  36.4   20    1   99000
2  54.1   19    8  135300
3  47.3    4   36  120400
4  22.0   21   18   56500
件数: 300件 / 家賃の平均: 104,648円

ステップ2:線形回帰で学習する

線形回帰は、イメージとしては「家賃 = 基本料金 + 広さ×いくら + 駅徒歩×いくら + 築年数×いくら」という式の「いくら」の部分を、データにいちばん当てはまるように決める方法です。

ポイントをまとめるとこんな感じです👇

  • train_test_split で、8割を学習用、2割をテスト用に分ける。テスト用は学習に使わず、最後の「本番テスト」に取っておきます
  • fit() で学習すると、intercept_(切片=基本料金)と coef_(係数=各列の「いくら」)が決まる
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

df = pd.read_csv("rent.csv", encoding="utf-8-sig")
X = df[["広さ", "駅徒歩", "築年数"]]  # 入力(特徴量)
y = df["家賃"]                       # 当てたい答え(目的変数)

# 8割で学習、2割はテスト用に取っておく
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=0
)

model = LinearRegression()
model.fit(X_train, y_train)

print(f"切片: {model.intercept_:,.0f}円")
for name, coef in zip(X.columns, model.coef_):
    print(f"{name}が1増えると: {coef:+,.0f}円")
切片: 57,223円
広さが1増えると: +2,035円
駅徒歩が1増えると: -1,548円
築年数が1増えると: -506円

広さ(+2,035円)と築年数(−506円)は、本当のルール(+2,000円・−500円)にかなり近い値になりました。

ところが駅徒歩は −1,548円。本当は −1,000円なのに、だいぶ大きめです。なぜでしょう? 実は、ここに後で見つける「クセ」のヒントが隠れています。先に評価のしかたを見ていきましょう。

ステップ3:MAE・RMSE・R²で「当たり具合」を測る

回帰の評価でよく使う指標は、この3つです。

指標 ざっくり言うと 見方
MAE(平均絶対誤差) 予測が平均で何円ずれたか 小さいほど良い。単位が「円」なので一番わかりやすい
RMSE(二乗平均平方根誤差) ずれを2乗して平均し、ルートで戻したもの 小さいほど良い。大外れがあると大きくなる
R²(決定係数) 「平均で答える」よりどれだけマシか 1に近いほど良い。0なら平均と同じ、マイナスなら平均以下

つまり、MAEで「何円ずれるか」をつかみ、RMSEとの差で「大外れが混ざっていないか」を見て、R²で「そもそも役に立っているか」を確かめる、という使い分けです。

比べる相手として、DummyRegressor で「いつも学習データの平均家賃を答えるだけ」のモデルも用意します。これに勝てないモデルは、どれだけ数字がそれっぽくても意味がありません⚠️

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

df = pd.read_csv("rent.csv", encoding="utf-8-sig")
X = df[["広さ", "駅徒歩", "築年数"]]
y = df["家賃"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=0
)

def report(name, model):
    model.fit(X_train, y_train)
    pred = model.predict(X_test)  # テストデータ(学習に使っていない物件)で予測
    mae = mean_absolute_error(y_test, pred)
    rmse = np.sqrt(mean_squared_error(y_test, pred))
    r2 = r2_score(y_test, pred)
    print(f"【{name}】 MAE: {mae:,.0f}円 / RMSE: {rmse:,.0f}円 / R²: {r2:.3f}")

# 比較用:いつも「学習データの平均家賃」と答えるだけのモデル
report("平均で答える", DummyRegressor(strategy="mean"))
report("線形回帰", LinearRegression())

平均で答えるだけだと、平均で約2.5万円もずれます(R²はほぼ0)。線形回帰はMAEが約3,500円、R²は0.978。家賃10万円前後の物件で3,500円程度のずれなので、かなり当たっていると言えそうです。

⚠️ R²は「データのばらつき方」によっても値が変わるので、R²だけで「0.9以上なら合格」のように決めつけないのがコツです。MAEを実際の単位(円)で見て、使う場面で許せるずれかどうかを考えましょう。

ステップ4:残差をグラフにして「クセ」を見つける

平均のずれが小さくても、「特定の条件の物件だけ、いつも同じ方向に外れる」ということがあります。それを見つけるのが残差(実際の家賃−予測した家賃)です。プラスなら「予測より実際が高い」、マイナスなら「予測より実際が安い」物件です。

  • 左のグラフ:実際と予測の散布図。点線に乗っているほど予測ぴったり
  • 右のグラフ:横軸を駅徒歩にして残差を並べる。ランダムに散らばっていれば問題なし、形があったらクセあり
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

plt.rcParams["font.family"] = "Meiryo"  # 日本語ラベル用(Macなら "Hiragino Sans")

df = pd.read_csv("rent.csv", encoding="utf-8-sig")
X = df[["広さ", "駅徒歩", "築年数"]]
y = df["家賃"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=0
)
model = LinearRegression().fit(X_train, y_train)

result = X_test.copy()
result["実際"] = y_test
result["予測"] = model.predict(X_test).round(-2)
result["残差"] = result["実際"] - result["予測"]  # プラス=予測より高い物件

# 外れ方が大きい物件トップ5
worst = result.reindex(result["残差"].abs().sort_values(ascending=False).index)
print(worst.head(5).to_string())

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
ax1.scatter(result["実際"], result["予測"], alpha=0.7)
lims = [result["実際"].min(), result["実際"].max()]
ax1.plot(lims, lims, color="gray", linestyle="--")  # この線に乗れば予測ぴったり
ax1.set_xlabel("実際の家賃")
ax1.set_ylabel("予測した家賃")
ax1.set_title("実際 vs 予測")

ax2.scatter(result["駅徒歩"], result["残差"], alpha=0.7, color="tab:orange")
ax2.axhline(0, color="gray", linestyle="--")
ax2.set_xlabel("駅徒歩(分)")
ax2.set_ylabel("残差=実際−予測")
ax2.set_title("駅徒歩ごとの残差")

# 目盛りを「12万」のような表記にして読みやすく
man = FuncFormatter(lambda v, _: f"{v / 10000:.0f}万")
ax1.xaxis.set_major_formatter(man)
ax1.yaxis.set_major_formatter(man)
ax2.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / 10000:+.1f}万"))

plt.tight_layout()
plt.show()
実行結果:平均で答えるモデルと線形回帰の比較
実行結果:平均で答えるモデルと線形回帰の比較

外れ方が大きい物件トップ5はこうなりました。

       広さ  駅徒歩  築年数      実際        予測       残差
92   36.7   24    8   79200   90700.0 -11500.0
15   27.5   16   18   89000   79300.0   9700.0
176  36.3   11   29  108200   99400.0   8800.0
52   51.1    6   25  131500  139300.0  -7800.0
206  23.5   14   10   85700   78300.0   7400.0
実行結果:実際と予測の散布図(左)と駅徒歩ごとの残差(右)
実行結果:実際と予測の散布図(左)と駅徒歩ごとの残差(右)

右のグラフを見ると、残差が0のまわりにランダムに散らばっているのではなく、徒歩10〜16分あたりはプラス、徒歩20分以上はマイナスに寄った「山なり」の形になっています。一番外れた物件も徒歩24分(予測より1.15万円安い)でした。

これは、「徒歩15分を超えると急に安くなる」ルールを、線形回帰がまっすぐな線1本で無理やり表そうとした結果です。遠い物件の安さに引っぱられて駅徒歩の係数が −1,548円と大きめになり、そのぶん中間の物件を安く見積もりすぎていたんですね。ステップ2の「なぜ?」の答えはこれでした。

ステップ5:列を1つ足して改善する

クセの正体がわかったので、「徒歩15分を何分こえているか」という列を追加してみます(15分以内は0)。こうすると線形回帰でも「15分を超えたら下がり方が変わる」を表せるようになります。

ついでに、こういう折れ曲がりを自動で学べるランダムフォレスト(3列のまま)とも比べてみましょう。

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

df = pd.read_csv("rent.csv", encoding="utf-8-sig")
# 残差グラフを見て追加する列:「徒歩15分を何分こえているか」(15分以内は0)
df["徒歩15分超"] = (df["駅徒歩"] - 15).clip(lower=0)

y = df["家賃"]
train_idx, test_idx = train_test_split(df.index, test_size=0.2, random_state=0)

def report(name, model, cols):
    X = df[cols]
    model.fit(X.loc[train_idx], y.loc[train_idx])
    pred = model.predict(X.loc[test_idx])
    mae = mean_absolute_error(y.loc[test_idx], pred)
    r2 = r2_score(y.loc[test_idx], pred)
    print(f"【{name}】 MAE: {mae:,.0f}円 / R²: {r2:.3f}")
    return model

base = ["広さ", "駅徒歩", "築年数"]
report("線形回帰(3列)", LinearRegression(), base)
lin = report("線形回帰(4列)", LinearRegression(), base + ["徒歩15分超"])
report("ランダムフォレスト(3列)",
       RandomForestRegressor(n_estimators=300, random_state=0), base)

print("--- 4列モデルの係数 ---")
for name, coef in zip(base + ["徒歩15分超"], lin.coef_):
    print(f"{name}: {coef:+,.0f}円")

# 新しい物件の家賃を予測してみる
new = pd.DataFrame({"広さ": [30.0, 30.0], "駅徒歩": [10, 20], "築年数": [15, 15]})
new["徒歩15分超"] = (new["駅徒歩"] - 15).clip(lower=0)
for (_, row), p in zip(new.iterrows(), lin.predict(new)):
    print(f"{row['広さ']}㎡・徒歩{row['駅徒歩']:.0f}分・築{row['築年数']:.0f}年 → 約{p:,.0f}円")

結果を見てみましょう。これ、実行する前に予想できますか? 「複雑なランダムフォレストが一番強いはず」と思った方も多いのではないでしょうか。

実際は、列を1つ足しただけの線形回帰が MAE 2,528円・R² 0.988 で一番でした。ランダムフォレストは MAE 3,829円で、元の線形回帰(3,513円)にも届きませんでした。学習データが240件と少ないうえ、元のルールがほぼ「足し算」でできているので、線形回帰のほうが向いていたわけです。

さらに4列モデルの係数を見ると、広さ +2,013円・駅徒歩 −992円・築年数 −494円・徒歩15分超 −1,513円。データを作ったときの本当のルールをほぼそのまま見抜けています👏

新しい物件の予測でも、同じ30㎡・築15年で徒歩10分なら約9.7万円、徒歩20分なら約8.0万円と、15分を超えた分の下がり方がきちんと反映されています。

ここが重要です👇

  • モデルを複雑にする前に、残差を見てクセを探す
  • クセの原因がわかれば、列(特徴量)を工夫するだけで大きく改善することがある
  • モデルを比べるときは、同じテストデータ・同じ指標で比べる(今回は random_state=0 で分け方を固定)

実際のデータで使うときの注意

  • 今回は答えを知っている練習用データなので、きれいな結果になっています。実データでは、立地・設備・階数など、表に無い理由で外れることも多いです。残差が大きい物件を1件ずつ見ると、「角部屋だった」「リフォーム済みだった」のような、足すべき列のヒントが見つかります
  • テストデータの結果を見ながら何度も調整すると、テストデータに合わせすぎてしまいます。調整が多くなってきたら、cross_val_score での交差検証も使いましょう
  • 学習データにない範囲(今回なら100㎡の物件や徒歩40分の物件)の予測は、当てにならないと考えておくのが安全です

各指標の詳しい定義は、scikit-learn 公式ドキュメントの「Regression metrics」にまとまっています。

まとめ

  • 回帰の評価は、MAE(何円ずれるか)・RMSE(大外れの有無)・R²(平均より役立つか)をセットで見る
  • DummyRegressor の「平均で答えるモデル」と比べると、本当に意味があるかがわかる
  • 残差をグラフにして形があれば、モデルが見落としているクセがある
  • クセに合わせて列を1つ足すだけで、MAEが3,513円→2,528円に改善した。複雑なモデルが勝つとは限らない

「R²が高いからOK」で終わらせずに、残差まで見る習慣をつけると、モデルの理解がぐっと深まります。手元のデータでも、ぜひ試してみてください😊

実行結果:列を足した線形回帰とランダムフォレストの比較
実行結果:列を足した線形回帰とランダムフォレストの比較

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る
Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

もしも

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

FastAPIでWebAPI開発を実践的に学ぶ

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。