身近なデータで試すPythonの機械学習! その2 お住まいの地域の不動産取引価格の予測モデル作成
不動産の価格は、立地、築年数、面積など様々な要因によって決まります。これらの要因と実際の取引価格のデータがあれば、機械学習を使って将来の取引価格を予測するモデルを構築できる可能性があります。よく紹介されるモデルとしてscikit-learnを使ったボストンの住宅価格の回帰予測モデルの例が紹介されていますが、身近な例として日本のある地域の不動産の価格の予測モデルの作成を検討してみました。
この記事では、国土交通省が提供する「不動産取引価格情報」を利用し、Pythonと人気の機械学習ライブラリScikit-learnを使って、不動産価格予測モデルを作成・評価するプロセスを紹介します。皆さんのお住まいのデータを使ってモデル作成をするとより興味を持ってプログラミング、機械学習の習得に取り組めるかもしれませんね。
プロセスの流れ
- データ読み込み: CSV形式の不動産取引データを読み込みます。
- データ前処理:
◦ 和暦の「建築年」を西暦に変換します。
◦ 「取引時点」から取引年を抽出します。
◦ 取引年と建築年から「築年数」を計算します。
◦ 予測に使用する特徴量(駅からの距離、面積など)と目的変数(取引価格)を選択します。
◦ 欠損値(データがない項目)を処理します。
◦ データスケーリング: 機械学習モデルがうまく学習できるように、特徴量のスケールを揃えます(標準化)。 - モデルの定義: 線形回帰、決定木、ランダムフォレスト、サポートベクターマシンなど、様々な種類の回帰モデルを準備します。
- 学習と評価:
◦ データを学習用とテスト用に分割します。
◦ 準備した各モデルを学習用データで学習させます。
◦ テスト用データを使ってモデルの予測精度を評価します。評価指標にはR2スコア(決定係数)、MSE(平均二乗誤差)、RMSE(二乗平均平方根誤差)を使用します。
◦ より信頼性の高い評価のため、データの分割と学習・評価を複数回(クロスバリデーション風)繰り返し、結果を平均します。 - 結果の表示: 各モデルの平均的な性能を比較し、どのモデルが今回のデータセットに対して良い予測を提供できそうかを確認します。
データの読み込み
国土交通省の以下のページから取引時期や都道府県、市町村を選択することでcsv形式のファイルをダウンロードすることができます。
注意点ですが、不動産取引情報のデータはアンケートに基づき集約されているようなので、あくまで目安程度として考える必要があるようです(実際の価格情報は不動産関連の他の情報なども勘案する必要があるかと思いますので、その点ご留意ください)。
実際にダウンロードして得られるデータは以下のようなデータになります(すこし前にダウンロードしたデータで、また必要に応じてカラムを削除しているため、現在とは表示カラムが少し異なっているかもしれません。その点ご留意ください)

取得データ例1

取得データ例2
選択したエリアでの住宅(土地)取引情報として、駅からの距離や土地面積、延床面積、建築年、取引価格等がリスト化されています。これらのデータから学習に必要なデータをピックアップ、前処理してゆきます。
データの前処理:機械学習の要
生のデータは、そのままでは機械学習モデルに入力できないことがよくあります。「建築年」が和暦(例:平成元年)であったり、「取引時点」が特定の四半期(例:2020年第1四半期)を示していたりするためです。また、データが欠けている(欠損値)場合もあります。
今回のコードでは、以下の前処理を行っています。
1. 和暦から西暦へ: jeraconv というライブラリを使って、「建築年」を扱いやすい西暦に変換します。
# 機能化された関数の一部
def convert_wareki_to_seireki(wareki_series: pd.Series) -> pd.Series:
j2w = jeraconv.J2W()
# ... (エラー処理を含む変換ロジック) ...
return wareki_series.apply(safe_convert).astype(float)
df_processed['建築年_西暦'] = convert_wareki_to_seireki(df_processed['建築年'])
- 築年数の計算: 「取引年」(「取引時点」から抽出)と西暦の「建築年」から、取引時点での「築年数」を計算します。これは不動産価格に影響を与える重要な特徴量です。
# 機能化された関数の一部
df_processed['取引年'] = df_processed['取引時点'].astype(str).str.extract(r'(\d{4})').astype(float)
valid_years_mask = df_processed['取引年'].notna() & df_processed['建築年_西暦'].notna()
df_processed.loc[valid_years_mask, '築年数'] = df_processed.loc[valid_years_mask, '取引年'] - df_processed.loc[valid_years_mask, '建築年_西暦'] + 1
- 特徴量とターゲットの選択: 今回は、「最寄駅:距離(分)」「面積(㎡)」「築年数」などを特徴量(X)とし、「取引価格(総額)」を予測したいターゲット(y)として選択します。
FEATURES = [
'最寄駅:距離(分)', '間口', '面積(㎡)', '延床面積(㎡)',
'前面道路:幅員(m)', '容積率(%)', '築年数'
]
TARGET_COLUMN = '取引価格(総額)'
X = df_processed[FEATURES]
y = df_processed[TARGET_COLUMN]
-
欠損値の処理: 簡単のため、特徴量またはターゲットの値が一つでも欠けている行は削除 (dropna()) しています。
-
データスケーリング(標準化): 特徴量によって数値の範囲(スケール)が大きく異なります(例:「面積」は大きいが「築年数」は比較的小さい)。多くのモデル(特に線形モデル、SVM、ニューラルネットワークなど)は、スケールが揃っている方がうまく学習できます。StandardScaler を使い、学習データでスケールを学習し、その学習結果を使って学習データとテストデータの両方を変換します。
# 評価ループ内
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 学習データでfit & transform
X_test_scaled = scaler.transform(X_test) # テストデータはtransformのみ
# ... モデルの学習と予測には X_train_scaled と X_test_scaled を使用 ...
model.fit(X_train_scaled, y_train)
y_test_pred = model.predict(X_test_scaled)
多様なモデルで挑戦
不動産価格のような複雑な現象を予測するには、一つのモデルだけで決め打ちするのではなく、複数の異なるアプローチを試すのが有効です。Pycaretなど有用なライブラリーも知られていますが、今回はモデル作成の練習の意味も含めて、様々なモデルを時前で作成するコードにしています。
今回のコードでは、get_regressors 関数で多数の回帰モデルを定義しています。
def get_regressors() -> Dict[str, Any]:
regressors = {
"LinearRegression": LinearRegression(),
"Ridge": Ridge(random_state=42),
"Lasso": Lasso(random_state=42),
# ... (Polynomial, KNN, Trees, Ensembles, SVM, etc.) ...
"RandomForestRegressor": RandomForestRegressor(random_state=42, n_estimators=100),
"GradientBoostingRegressor": GradientBoostingRegressor(random_state=42, n_estimators=100),
"HistGradientBoostingRegressor": HistGradientBoostingRegressor(random_state=42),
# ... 他多数 ...
}
return regressors
これには、基本的な線形回帰から、より複雑なランダムフォレストや勾配ブースティング、サポートベクターマシンなどが含まれます。Pipeline を使って多項式特徴量(特徴量の組み合わせや二乗など)を自動生成するモデルも試しています。
堅牢な評価:複数回の試行
データの一部だけを使ってテストすると、たまたまその分割で性能が良かったり悪かったりする可能性があります。より信頼できるモデルの性能を測るため、データをランダムに分割する試行を複数回 (N_TRIALS = 5) 行い、各モデルの評価指標(R2, MSE, RMSE)の平均値を最終的な性能としています。
# train_evaluate_models 関数内
results = # ... 結果格納用の辞書 ...
trial_scores = # ... 各試行のスコアを一時保存 ...
for i in range(n_trials):
# ... データ分割、スケーリング ...
for reg_name, model in regressors.items():
# ... 学習、予測、評価 ...
# trial_scores に今回の試行のスコアを追加
# 全試行終了後
for idx, name in enumerate(results['Model']):
# trial_scores から平均値を計算して results に格納
results['Avg R2 Test'][idx] = np.nanmean(trial_scores[name]['r2_test'])
# ... 他の指標も同様に ...
results_df = pd.DataFrame(results)
# R2 Test の平均値でソートして表示
results_df.sort_values(by='Avg R2 Test', ascending=False, inplace=True)
結果の解釈
コードを実行すると、最後に各モデルの平均パフォーマンスがR2スコア(テストデータ)の高い順に表示されます。
• R2スコア (決定係数): 0から1の間の値で、1に近いほどモデルがデータの変動をよく説明できていることを示します。
• MSE / RMSE: 予測値と実際の値の誤差の大きさを示します。0に近いほど誤差が小さい良いモデルです。RMSEは元のターゲット変数(価格)と同じ単位になるため、誤差の大きさを直感的に理解しやすいです。
この結果を見て、どのモデルが今回のデータと設定において最も有望か判断できます。一般的に、ランダムフォレストや勾配ブースティング系のアンサンブルモデルが高い性能を示すことが多いですが、データの特性や前処理によって最適なモデルは異なります。
今回私の住んでいるエリアでの情報で検討した結果は以下の通りでした。

残念ながら、テストデータでの評価値が高いモデルはなく、今回の特徴量だけでは良いモデルはえられていないようです。
まとめと今後のステップ
今回は、Scikit-learnを用いて、不動産取引価格を予測する基本的なワークフローを紹介しました。具体的にはデータの前処理、複数のモデルを試すアプローチ、そして複数回の試行による評価方法をのコードを紹介しました。ある程度良い評価値を示すモデルは得られましたが、ここからさらに改善するためのステップとしては、以下のようなものが考えられます。
• さらに別のモデルでのアプローチ:
XgboostやlightGBMなど近年特に活用が進んでいるモデルで検討を進める。
• 特徴量エンジニアリング:
◦ 「市区町村」「最寄り駅」などのカテゴリカル変数を適切に処理してモデル入力に追加する(One-Hot Encodingなど)。
◦ 土地の形状、用途地域などの情報を追加する。
◦ より高度な特徴量(例:駅距離と面積の交互作用項)を作成する。
• 欠損値処理: 単純な削除ではなく、統計的な手法(平均値、中央値、回帰など)で欠損値を補完する。
• ハイパーパラメータチューニング: 各モデルには調整可能なパラメータ(例:ランダムフォレストの木の数)があります。Grid SearchやRandomized Searchなどで最適なパラメータを見つけることで、性能が向上する可能性があります。
• より新しいデータの利用: 定期的に最新の取引情報を取得し、モデルを更新する。
不動産価格予測は挑戦的なタスクですが、身近なデータとしては興味を持って取り組める課題の一つかと思います。ご自身の環境での取り組みの参考になれば幸いです。
コードの全体の記載
import pandas as pd
import numpy as np
import random
import os
from jeraconv import jeraconv # 和暦・西暦変換ライブラリ
from typing import Dict, List, Tuple, Any
from IPython.display import display
# Scikit-learnのインポート
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.pipeline import Pipeline
from sklearn.linear_model import (LinearRegression, Ridge, Lasso, ElasticNet, SGDRegressor,
PassiveAggressiveRegressor, ARDRegression, RidgeCV,
TheilSenRegressor, RANSACRegressor, HuberRegressor)
from sklearn.neural_network import MLPRegressor
from sklearn.svm import SVR, LinearSVR
from sklearn.neighbors import KNeighborsRegressor
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.experimental import enable_hist_gradient_boosting # HistGradientBoostingRegressor を有効にするため
from sklearn.ensemble import (RandomForestRegressor, AdaBoostRegressor, ExtraTreesRegressor,
HistGradientBoostingRegressor, BaggingRegressor, GradientBoostingRegressor,
VotingRegressor, StackingRegressor)
from sklearn.cross_decomposition import PLSRegression
from warnings import simplefilter
from sklearn.exceptions import ConvergenceWarning
simplefilter("ignore", category=ConvergenceWarning)
# --- 設定 ---
FILE_PATH = "########.csv" # データファイルのパス
TARGET_COLUMN = '取引価格(総額)' # 予測したい値のカラム名
FEATURES = [ # 予測に使用する特徴量のカラム名リスト
'最寄駅:距離(分)',
'間口', # 注意: データソースやフィルタリングによってはNaNが多い可能性あり
'面積(㎡)',
'延床面積(㎡)', # 注意: 土地のみの取引などではNaNの可能性あり
'前面道路:幅員(m)', # 注意: NaNが多い可能性あり
'容積率(%)',
'築年数' # データ前処理で生成する特徴量
]
TEST_SIZE = 0.3 # 全データを学習用とテスト用に分割する際のテストデータの割合
N_TRIALS = 5 # 評価の安定性を高めるためのランダム分割・評価の繰り返し回数
# --- モデル定義 ---
def get_regressors() -> Dict[str, Any]:
"""評価対象とする回帰モデルの辞書を返します。"""
# アンサンブル学習などでベースモデルが必要な場合に定義
svr_base = SVR()
rf_final = RandomForestRegressor(n_estimators=10, random_state=42) # 最終段の推定器
lr_ridge_cv = RidgeCV()
linear_svr = LinearSVR(random_state=42, max_iter=2000) # 収束のためにmax_iterを増やす
# 評価するモデルを辞書形式で定義 (キーは日本語説明付き)
regressors = {
"線形回帰 (LinearRegression)": LinearRegression(),
"リッジ回帰 (Ridge)": Ridge(random_state=42),
"ラッソ回帰 (Lasso)": Lasso(random_state=42),
"ElasticNet": ElasticNet(random_state=42),
"多項式回帰 (2次)": Pipeline([('poly', PolynomialFeatures(degree=2)), ('linear', LinearRegression())]),
"多項式回帰 (3次)": Pipeline([('poly', PolynomialFeatures(degree=3)), ('linear', LinearRegression())]), # 計算コストが高い場合がある
"K近傍法 (KNeighborsRegressor)": KNeighborsRegressor(n_neighbors=5), # n_neighborsを一般的なデフォルト値5に変更
"決定木 (DecisionTreeRegressor)": DecisionTreeRegressor(random_state=42),
"ランダムフォレスト (RandomForestRegressor)": RandomForestRegressor(random_state=42, n_estimators=100), # n_estimatorsを明示
"サポートベクター回帰 (SVR_RBF)": SVR(kernel='rbf', C=1e3, gamma='scale', epsilon=0.1), # gamma='scale'は多くの場合良いデフォルト
"ガウス過程回帰 (GaussianProcessRegressor)": GaussianProcessRegressor(random_state=42), # 計算が遅いことがある
"確率的勾配降下法 (SGDRegressor)": SGDRegressor(random_state=42, max_iter=1000, tol=1e-3),
"多層パーセプトロン (MLPRegressor)": MLPRegressor(hidden_layer_sizes=(50, 50), max_iter=500, early_stopping=True, n_iter_no_change=10, random_state=42), # 層やイテレーション数を調整
"ExtraTreesRegressor": ExtraTreesRegressor(n_estimators=100, random_state=42),
"部分的最小二乗回帰 (PLSRegression)": PLSRegression(n_components=min(len(FEATURES), 10)), # n_componentsは特徴量数以下
"PassiveAggressiveRegressor": PassiveAggressiveRegressor(max_iter=1000, tol=1e-3, random_state=42),
"TheilSenRegressor": TheilSenRegressor(random_state=42),
"RANSACRegressor": RANSACRegressor(random_state=42),
"ヒストグラム勾配ブースティング (HistGradientBoostingRegressor)": HistGradientBoostingRegressor(random_state=42),
"AdaBoostRegressor": AdaBoostRegressor(random_state=42, n_estimators=100),
"勾配ブースティング (GradientBoostingRegressor)": GradientBoostingRegressor(random_state=42, n_estimators=100), # n_estimatorsを明示
"スタッキング (StackingRegressor)": StackingRegressor(estimators=[('ridge', lr_ridge_cv), ('svr', linear_svr)], final_estimator=rf_final),
"ARD回帰 (ARDRegression)": ARDRegression(),
"HuberRegressor": HuberRegressor(max_iter=1000), # max_iterを増やす
}
return regressors
# --- データハンドリング関数 ---
def load_data(filepath: str) -> pd.DataFrame:
"""CSVファイルからデータを読み込みます。"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"エラー: ファイルが見つかりません: {filepath}")
print(f"{filepath} からデータを読み込んでいます...")
try:
df = pd.read_csv(filepath, encoding='utf-8') # まずUTF-8で試す
except UnicodeDecodeError:
print("UTF-8での読み込みに失敗しました。日本語CSV用エンコーディング'cp932'で再試行します...")
df = pd.read_csv(filepath, encoding='cp932') # 日本語Windows環境のCSVで一般的なエンコーディング
print("データの読み込みが完了しました。")
return df
def convert_wareki_to_seireki(wareki_series: pd.Series) -> pd.Series:
"""Pandas Seriesの和暦(文字列)を西暦(数値)に変換します。"""
j2w = jeraconv.J2W() # 和暦西暦変換オブジェクト
# 1つの値を安全に変換する内部関数(エラーや特殊な形式を処理)
def safe_convert(wareki_val):
if pd.isna(wareki_val) or not isinstance(wareki_val, str) or '年' not in wareki_val:
# NaN、文字列以外、または予期しない形式の場合はNaNを返す
return np.nan
try:
# 元号と年数を抽出、「元年」のようなケースを処理
if "元年" in wareki_val:
year_part = wareki_val.replace("元年", "1年")
else:
year_part = wareki_val
# jeraconvで変換
return j2w.convert(year_part)
except Exception as e:
# print(f"警告: '{wareki_val}' を変換できませんでした。エラー: {e}")
return np.nan # 変換に失敗した場合もNaNを返す
# Seriesの各要素にsafe_convert関数を適用し、結果をfloat型(NaNを許容)にする
return wareki_series.apply(safe_convert).astype(float)
def preprocess_data(df: pd.DataFrame, feature_cols: List[str], target_col: str) -> Tuple[pd.DataFrame, pd.Series]:
"""データの前処理(日付変換、特徴量エンジニアリング、クリーニング)を実行します。"""
df_processed = df.copy() # 元のDataFrameを変更しないようにコピー
# 1. 和暦の建築年 ('建築年') を西暦に変換
if '建築年' in df_processed.columns:
df_processed['建築年_西暦'] = convert_wareki_to_seireki(df_processed['建築年'])
else:
print("警告: '建築年'カラムが見つかりません。建築年の変換をスキップします。")
df_processed['建築年_西暦'] = np.nan # 元のカラムがない場合はNaNで列を追加
# 2. '取引時点' から取引年を抽出
if '取引時点' in df_processed.columns:
# "YYYY年Q期" や "YYYY" のような形式からデータ取得
df_processed['取引年'] = df_processed['取引時点'].astype(str).str.extract(r'(\d{4})').astype(float)
else:
print("警告: '取引時点'カラムが見つかりません。取引年の抽出をスキップします。")
df_processed['取引年'] = np.nan # 元のカラムがない場合はNaNで列を追加
# 3. 築年数 ('築年数') を計算
if '建築年_西暦' in df_processed.columns and '取引年' in df_processed.columns:
# 取引年と建築年西暦が両方有効な数値の場合のみ計算
valid_years_mask = df_processed['取引年'].notna() & df_processed['建築年_西暦'].notna()
df_processed['築年数'] = np.nan # まずNaNで初期化
df_processed.loc[valid_years_mask, '築年数'] = df_processed.loc[valid_years_mask, '取引年'] - df_processed.loc[valid_years_mask, '建築年_西暦'] + 1
# 負の築年数(未来の取引日など)がありえる場合、0またはNaNに処理(ここでは0に)
df_processed.loc[df_processed['築年数'] < 0, '築年数'] = 0
else:
print("警告: 年のカラムが不足しているため'築年数'を計算できません。")
# '築年数'が特徴量リストに含まれていない場合のみNaN列を追加
if '築年数' not in feature_cols:
df_processed['築年数'] = np.nan
# 4. 特徴量 (X) と目的変数 (y) を選択
print(f"特徴量を選択: {feature_cols}")
print(f"目的変数を選択: {target_col}")
# 選択された全ての特徴量カラムが存在することを確認。なければNaNで作成('築年数'を除く)
for col in feature_cols:
if col not in df_processed.columns and col != '築年数':
print(f"警告: 特徴量カラム '{col}' がデータ内に見つかりません。NaNで埋められます。")
df_processed[col] = np.nan
elif col not in df_processed.columns and col == '築年数':
# '築年数'は上で生成されたか、依存関係で失敗した場合はここで生成する必要がある
if '築年数' not in df_processed.columns:
df_processed['築年数'] = np.nan
# 目的変数カラムが存在することを確認
if target_col not in df_processed.columns:
raise ValueError(f"エラー: 目的変数カラム '{target_col}' がDataFrame内に見つかりません。")
# 関連するカラムを数値型に変換し、変換できない場合はNaNにする
potential_numeric_cols = feature_cols + [target_col]
for col in potential_numeric_cols:
if col in df_processed.columns:
# '10分' や '300㎡' のような文字列から数値部分を抽出する処理
if df_processed[col].dtype == 'object':
df_processed[col] = df_processed[col].astype(str).str.extract(r'(\d+\.?\d*)')[0]
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce') # errors='coerce'で変換不能値はNaNに
X = df_processed[feature_cols]
y = df_processed[target_col]
# 5. 欠損値 (NaN) の処理
print(f"処理前の形状 (X, y): {X.shape}, {y.shape}")
# 目的変数 (y) が欠損している行を削除
valid_y_mask = y.notna()
X = X[valid_y_mask]
y = y[valid_y_mask]
print(f"目的変数がNaNの行を削除後の形状: {X.shape}, {y.shape}")
# 特徴量 (X) のいずれかが欠損している行を削除
initial_rows = X.shape[0]
combined = pd.concat([X, y], axis=1) # Xとyを一時的に結合して処理
# 特徴量または目的変数がNaNの行を削除 (数値変換でNaNになった場合もここで削除される)
combined.dropna(subset=feature_cols + [target_col], inplace=True)
X = combined[feature_cols]
y = combined[target_col]
rows_dropped = initial_rows - X.shape[0]
print(f"特徴量または目的変数のNaNにより {rows_dropped} 行を削除しました。")
print(f"最終的な形状 (X, y): {X.shape}, {y.shape}")
if X.empty or y.empty:
raise ValueError("エラー: 前処理とNaN除去の結果、データが残りませんでした。データの品質や特徴量の選択を確認してください。")
print("データ前処理が完了しました。")
return X, y
# --- モデル学習・評価関数 ---
def train_evaluate_models(
X: pd.DataFrame,
y: pd.Series,
regressors: Dict[str, Any],
n_trials: int,
test_size: float
) -> pd.DataFrame:
"""複数の回帰モデルを、複数回のランダム試行で学習・評価します。"""
# 結果を格納するための辞書を初期化 (カラム名は日本語)
results_template = {
'モデル名': list(regressors.keys()),
'平均R2(学習)': [np.nan] * len(regressors), # NaNで初期化
'平均R2(テスト)': [np.nan] * len(regressors),
'平均MSE(テスト)': [np.nan] * len(regressors),
'平均RMSE(テスト)': [np.nan] * len(regressors)
}
# 各試行のスコアをモデルごとに蓄積するためのリスト
trial_scores = {name: {'r2_train': [], 'r2_test': [], 'mse_test': [], 'rmse_test': []} for name in regressors.keys()}
for i in range(n_trials):
print(f"\n--- 試行 {i+1}/{n_trials} ---")
# 各試行で異なるランダムシードを使用してデータを分割
random_state = random.randint(0, 1000)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=random_state
)
# --- スケーリング ---
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 学習データでfit & transform
X_test_scaled = scaler.transform(X_test) # テストデータはtransformのみ
print(f"学習データサイズ: {X_train_scaled.shape[0]}, テストデータサイズ: {X_test_scaled.shape[0]}")
# --- 各モデルの学習と評価 ---
for reg_name, model in regressors.items():
print(f"モデル '{reg_name}' を学習中...")
try:
# スケーリングされたデータを使ってモデルを学習
model.fit(X_train_scaled, y_train)
# スケーリングされたデータを使って予測
y_train_pred = model.predict(X_train_scaled)
y_test_pred = model.predict(X_test_scaled)
# 評価指標の計算
r2_tr = r2_score(y_train, y_train_pred) # R2スコア(学習データ)
r2_tes = r2_score(y_test, y_test_pred) # R2スコア(テストデータ)
mse_tes = mean_squared_error(y_test, y_test_pred) # 平均二乗誤差(テストデータ)
rmse_tes = np.sqrt(mse_tes) # 二乗平均平方根誤差(テストデータ)
# この試行の結果を格納
trial_scores[reg_name]['r2_train'].append(r2_tr)
trial_scores[reg_name]['r2_test'].append(r2_tes)
trial_scores[reg_name]['mse_test'].append(mse_tes)
trial_scores[reg_name]['rmse_test'].append(rmse_tes)
except Exception as e:
print(f"!!! モデル '{reg_name}' の学習/評価中にエラーが発生しました: {e}")
# モデルが失敗した場合、NaNを追加するかスキップ(ここではNaNを追加)
trial_scores[reg_name]['r2_train'].append(np.nan)
trial_scores[reg_name]['r2_test'].append(np.nan)
trial_scores[reg_name]['mse_test'].append(np.nan)
trial_scores[reg_name]['rmse_test'].append(np.nan)
# --- 結果の集計 ---
print("\n--- 全試行の結果を集計 ---")
results_df = pd.DataFrame(results_template) # 結果格納用のDataFrameを作成
results_df.set_index('モデル名', inplace=True) # モデル名をインデックスに設定
for model_name in results_df.index:
if model_name in trial_scores: # モデル名が trial_scores に存在するか確認
# np.nanmean を使い、試行中にNaNが発生した場合でも平均を計算
results_df.loc[model_name, '平均R2(学習)'] = np.nanmean(trial_scores[model_name]['r2_train'])
results_df.loc[model_name, '平均R2(テスト)'] = np.nanmean(trial_scores[model_name]['r2_test'])
results_df.loc[model_name, '平均MSE(テスト)'] = np.nanmean(trial_scores[model_name]['mse_test'])
results_df.loc[model_name, '平均RMSE(テスト)'] = np.nanmean(trial_scores[model_name]['rmse_test'])
else:
print(f"警告: 集計中にモデル名 '{model_name}' が trial_scores に見つかりませんでした。")
# テストデータのR2スコアで降順にソート
results_df.sort_values(by='平均R2(テスト)', ascending=False, inplace=True)
# 表示用に小数点以下4桁に丸める
results_df = results_df.round(4)
# インデックスをリセットして 'モデル名' を通常の列に戻す (オプション)
results_df.reset_index(inplace=True)
return results_df
# --- メイン実行部分 ---
def main():
"""全体の処理を実行するメイン関数。"""
try:
# 1. データ読み込み
df_raw = load_data(FILE_PATH)
# 2. データ前処理
X, y = preprocess_data(df_raw, FEATURES, TARGET_COLUMN)
# 3. モデル定義
regressors = get_regressors()
# 4. モデルの学習と評価
results_df = train_evaluate_models(X, y, regressors, N_TRIALS, TEST_SIZE)
# 5. 結果の表示
print("\n--- モデル性能の最終結果 (複数試行の平均) ---")
display(results_df)
except FileNotFoundError as e:
print(e)
except ValueError as e:
print(e)
except Exception as e:
print(f"予期せぬエラーが発生しました: {e}")
# ここでより詳細なエラーハンドリングやロギングを追加することも可能
# このスクリプトが直接実行された場合に main() 関数を呼び出す
if __name__ == "__main__":
main()
Discussion