Pythonと機械学習で東京23区の地価（住宅地）を予測する②：CatBoostモデルをグリッドサーチでさらに最適化！

# 1. はじめに

前回の記事では、
CatBoostを使った東京23区の住宅地地価予測モデル構築について紹介しました。

https://qiita.com/samuraiyongt/items/6f5246bf785adfb429f4

データの準備から、300万円以下の物件に絞り込んだモデルの学習までを行い、
その時点で既に高い予測精度を達成していました。

:::note info
しかし、機械学習モデルの真の力を引き出すには、
**ハイパーパラメータチューニング**が欠かせません。
:::
モデルの学習挙動を制御するこれらのパラメータを最適化することで、
さらなる精度向上や汎化性能の改善が期待できます。

今回の記事では、前回の分析で構築したCatBoostモデルに対し、
グリッドサーチを用いたハイパーパラメータチューニングを実施した結果と、
その成果について記載します。

# 2. グリッドサーチとは？

機械学習モデルの性能は、
ハイパーパラメータと呼ばれる設定値によって大きく変わることがあります。

例えば、CatBoostモデルであれば、
**・何本の決定木を作るか（iterations）**
**・どのくらいの速さで学習を進めるか（learning_rate）**
**・木の深さをどこまでにするか（depth）**
といった値がハイパーパラメータにあたります。

これらのハイパーパラメータの最適な組み合わせを見つけるための、
最も基本的ながら強力な手法の一つが**グリッドサーチ**です。


:::note info
グリッドサーチとは
チューニングしたいハイパーパラメータごとに、
試したい値のリストを定義します。
そして、**定義したすべての組み合わせを網羅的に試し、
最も良い結果（今回の場合は予測誤差が最も小さい組み合わせ**）を
出した設定を探し出します。
:::
これにより、**特定の範囲内での最適な組み合わせを効率的に見つけ出すことができます**。

# 3. チューニングの実施
前回の記事では、データの前処理と特徴量エンジニアリングに加え、
地価300万円以下の物件に絞り込むことで、
高い予測精度を達成したCatBoostモデルを構築しました。

しかし、さらにモデルの性能を最大化するため、
今回は**GridSearchCVを用いてハイパーパラメータの最適化**を行いました。

ハイパーパラメータはモデルの「設定値」であり、
その選び方でモデルの学習の仕方が大きく変わります。
**闇雲に設定するのではなく、これまでの経験から効果的だと考えられる範囲を定義し、
その中で最適な組み合わせを探します**。

今回チューニングの対象としたのは以下の主要なハイパーパラメータです。

:::note info
**iterations:** 
決定木の数。多すぎると過学習のリスクがあり、少なすぎると学習不足になります。
**learning_rate:** 
学習の速さ。値が大きいと学習が速いですが、最適解を飛び越える可能性があります。
小さいと学習に時間がかかりますが、より安定した学習が期待できます。
**depth:** 
決定木の最大深度。
深い木は複雑なパターンを学習できますが、過学習しやすくなります。
浅い木はシンプルなモデルになります。
**l2_leaf_reg:** 
L2正則化の強さ。
モデルの複雑さを抑え、過学習を防ぐ効果があります。
:::

これらのハイパーパラメータに対し、以下の範囲でグリッドサーチを行いました。

:::note info
**cv=3** と設定することで、データを3分割して交差検証を行い、
より信頼性の高い評価で最適な組み合わせを選びます。
また、**n_jobs=-1** とすることで、
利用可能なすべてのCPUコアを使って並列計算を行い、
チューニング時間を短縮しています。
:::

```python
# 28.ハイパーパラメーターチューニング
print("CatBoostモデルのハイパーパラメーターチューニングを開始します。")

# 調整するハイパーパラメーターの範囲を定義
param_grid = {
    'iterations': [220, 250, 280],
    'learning_rate': [0.11, 0.12, 0.13],
    'depth': [6, 7, 8],
    'l2_leaf_reg': [1.5, 2, 2.5]
}

# CatBoostRegressorのインスタンスを作成
cb_model = CatBoostRegressor(random_state=42, verbose=0)

# GridSearchCVを設定
grid_search = GridSearchCV(estimator=cb_model, param_grid=param_grid,
                           cv=3, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1)

# グリッドサーチを実行
# ここでは300万円以下のデータで学習したモデル (X_train_filtered, y_train_filtered) を使用します。
grid_search.fit(X_train_filtered, y_train_filtered,
                cat_features=categorical_features_indices_filtered)

# 最適なハイパーパラメーターとスコアを表示
print("\n最適なハイパーパラメーター:", grid_search.best_params_)
best_rmse_log_tuned = np.sqrt(-grid_search.best_score_)
print(f"ベストスコア (RMSE - 学習データ - 対数スケール - チューニング後): {best_rmse_log_tuned:.5f}")
```
```text
CatBoostモデルのハイパーパラメーターチューニングを開始します。
Fitting 3 folds for each of 81 candidates, totalling 243 fits

最適なハイパーパラメーター: {'depth': 6, 'iterations': 280, 'l2_leaf_reg': 2, 'learning_rate': 0.11}
ベストスコア (RMSE - 学習データ - 対数スケール - チューニング後): 0.04677
```
# 4. チューニング結果とモデル評価
グリッドサーチを実行した結果、
定義したハイパーパラメータの範囲内で、
最も高い予測精度を達成する最適な組み合わせが見つかりました。
その結果は以下の通りです。
```text
最適なハイパーパラメーター: {'depth': 6, 'iterations': 280, 'l2_leaf_reg': 2, 'learning_rate': 0.11}
```

:::note info
この最適なハイパーパラメータを適用したモデルは、
**grid_search.best_estimator_** に格納されます。
将来的な再利用のため、この最適なモデルをファイルに保存します。
:::
```python
# 最適なモデルを取得
best_catboost_model = grid_search.best_estimator_

# モデルをファイルに保存
model_filename = 'best_catboost_price_predictor.pkl'
joblib.dump(best_catboost_model, model_filename)
print(f"最適なモデルを '{model_filename}' として保存しました。")
```
この最適なハイパーパラメータを適用したモデルを、
フィルタリング後のテストデータ（地価300万円以下の物件）で評価しました。

まず、対数スケールでのRMSEを計算します。
```python
# 29.対数スケールでのRMSEを計算
print("-" * 50)
print("チューニング後のモデルの評価:")

# テストデータでの予測
y_pred_tuned = best_catboost_model.predict(X_test_filtered)

# RMSEの計算 (対数スケール)
rmse_test_log_tuned = np.sqrt(mean_squared_error(y_test_filtered, y_pred_tuned))
print(f"CatBoost RMSE (テストデータ - 対数スケール - チューニング後): {rmse_test_log_tuned:.5f}")
```
```text
チューニング後のモデルの評価:
CatBoost RMSE (テストデータ - 対数スケール - チューニング後): 0.02945
```
次に、元のスケールに戻してRMSEを計算します。
```python
#30. 元のスケールに戻してRMSEを計算
y_test_original_filtered = np.expm1(y_test_filtered)
y_pred_original_tuned = np.expm1(y_pred_tuned)
rmse_original_scale_tuned = np.sqrt(mean_squared_error(y_test_original_filtered, y_pred_original_tuned))
print(f"CatBoost RMSE (テストデータ - 元のスケール - チューニング後): {int(round(rmse_original_scale_tuned)):,} 円")
print("-" * 50)
```
```text
CatBoost RMSE (テストデータ - 元のスケール - チューニング後): 37,068 円
```
さらに、MAEとR²スコアも確認します。
```python
#31. MAEとR²の計算
mae_tuned = mean_absolute_error(y_test_filtered, y_pred_tuned)
r2_tuned = r2_score(y_test_filtered, y_pred_tuned)
print(f"CatBoost MAE (対数スケール - チューニング後): {mae_tuned:.5f}")
print(f"CatBoost R² (チューニング後): {r2_tuned:.5f}")
print("-" * 50)
```
```text
atBoost MAE (対数スケール - チューニング後): 0.02129
CatBoost R² (チューニング後): 0.99638
```

以下に、チューニング後のモデルの評価指標をまとめます。
```text
CatBoost RMSE (テストデータ - 対数スケール - チューニング後): 0.02129
CatBoost RMSE (テストデータ - 元のスケール - チューニング後): 37,068 円
--------------------------------------------------
CatBoost MAE (対数スケール - チューニング後): 0.02129
CatBoost R² (チューニング後): 0.99638
--------------------------------------------------
```
:::note info
その結果、**予想以上に精度改善が確認できました**。
:::

## 結果の考察 
:::note info
最も注目すべきは、
**元のスケールでのテストデータRMSEが37,068円という結果です。**
:::

これは、前回の学習したモデルのRMSEと比較して、
**さらなる予測誤差の削減に成功したことを意味します。**

この数値は、
**約300万円までの地価を予測するモデルとしては非常に小さな誤差であり、実用上極めて高い精度と言えます。**

**MAEも0.02129と非常に低い値を示しており、
予測の平均的なずれが小さいことを示しています。**

**R²スコアは0.99638と、1に限りなく近い値となりました。**
これは、モデルが地価の変動の約99.6%を説明できていることを意味し、
今回のデータセットに対してモデルが極めて高い適合度を持っていることを裏付けています。

:::note info
**この結果から、
地価予測モデルの性能を最大限に引き出すことができたと結論付けられます。**
:::
# 5. 予測結果の可視化
最後に、チューニングによって最適化されたモデルの予測が、
**実際の地価にどれほど近いのかを視覚的に確認します**。
```python
#32. 予測結果の可視化 (チューニング後のモデル)
plt.figure(figsize=(8, 8))
sns.scatterplot(x=y_test_original_filtered, y=y_pred_original_tuned)
plt.plot([y_test_original_filtered.min(), y_test_original_filtered.max()], [y_test_original_filtered.min(), y_test_original_filtered.max()], '--k')
plt.xlabel("実測値 (元のスケール)")
plt.ylabel("予測値 (元のスケール)")
plt.title("実測値 vs. 予測値 (300万円以下データ - チューニング後 - 元のスケール)")
plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
plt.show()
```
![#32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4091959/0c4b03cc-c720-45ad-a51b-ff66eb3ff670.png)

### グラフから読み取れること
この散布図を見ると、ほとんどの点が黒い点線（y=x）の周りに集まっていることが分かります。
**これは、モデルが実際の地価を非常に高い精度で予測できていることを視覚的に裏付けています。**

特に300万円以下の地価帯では、
**前回よりも予測値と実測値がほぼ一致しており**、
モデルがこの価格帯の変動をうまく捉えられていることが確認できます。
一部に点線からわずかに離れている点もありますが、**全体としては非常に良好な結果です。**

:::note info
この可視化によって、
**これまでの数値的な評価指標（RMSEやR²スコア）が示す高精度が、
実際の予測においても確かであることが明確に示されました。**
:::

# 6. 今後の展望（まとめ）
今回のグリッドサーチによるハイパーパラメータチューニングを通じて、

:::note info
東京23区の住宅地地価予測モデルの精度を、
**RMSE 37,068円**という非常に高い水準まで引き上げることができました。

**R²スコアも0.99638**と、
地価の変動をほぼ完璧に説明できるモデルが構築できました。
:::

この高い精度を達成したモデルは、
以下のような様々な形で活用できる可能性があります。

**不動産価格査定の補助:**
短時間で物件の推定価格を算出し、査定業務の効率化に貢献できます。

**不動産投資判断の参考:** 
投資家が物件選定を行う際の、客観的な価格予測情報として活用できます。

**市場動向の分析:** 
特定の地域や条件における地価の傾向を、モデルを通してより深く理解するための基礎とすることも可能です。
もちろん、データ分析には常に改善の余地があります。
# 7. 今後の展望（まとめ2）

:::note info
**新たな特徴量エンジニアリングを探索**
モデルに組み込むことで、さらなる精度向上を目指す。
:::

:::note info
**モデルがなぜそのような予測を出したのか**
各特徴量が予測にどれだけ貢献しているのかをより深く掘り下げて分析する。
:::

:::note info
**構築したモデルをWebアプリケーションや外部ツール**
簡単に地価予測を利用できるようにする。
:::
今回の地価予測モデル構築は、
データ収集から前処理、特徴量エンジニアリング、そしてモデルの最適化まで、
一連の機械学習プロジェクトのプロセスを実践する良い機会となりました。

:::note warn
**この知見が、同様の課題に取り組む方々の一助となれば幸いです。**
:::

以上

次の記事では、「なぜモデルがそのような予測をしたのか？」という根拠をSHAP値で解釈し、そのブラックボックスを解き明かします。

https://qiita.com/samuraiyongt/items/7b0dbf13e1c18427d310