STEP 29:ガイド付きプロジェクト

🏠 STEP 29: ガイド付きプロジェクト

住宅価格予測プロジェクトをステップバイステップで実践

📋 プロジェクト概要

  • 目標:住宅の特徴から販売価格を予測するモデルを構築
  • データ:California Housing Dataset(約20,000件)
  • 問題タイプ:回帰(連続値の予測)
  • 評価指標:RMSE(Root Mean Squared Error)

⏱️ 所要時間:約2時間

1
問題定義とビジネス理解
🎯 ビジネス課題

不動産会社が住宅の適正価格を自動で見積もるシステムを構築したい。
これにより、価格設定の効率化と精度向上を目指す。

【成功の定義】 ・RMSE(平均二乗誤差の平方根)を最小化 ・目標: RMSE < 0.5(10万ドル単位のデータなので、約5万ドル以内の誤差) 【制約】 ・解釈可能性も重要(なぜその価格なのか説明できること) ・計算時間は数秒以内
2
データの取得と確認
最初に行うこと:

データの基本情報(サイズ、特徴量、型、欠損値)を確認します。
これにより、どのような前処理が必要かを把握できます。

# ライブラリのインポート import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # scikit-learnからデータセットを読み込み from sklearn.datasets import fetch_california_housing # データをロード # fetch_california_housing()はBunchオブジェクトを返す # .data: 特徴量、.target: 目的変数、.feature_names: 特徴量名 california = fetch_california_housing() # DataFrameに変換(扱いやすくするため) X = pd.DataFrame(california.data, columns=california.feature_names) y = pd.Series(california.target, name=’MedHouseVal’) # DataFrameにまとめる df = X.copy() df[‘MedHouseVal’] = y # 基本情報を表示 print(“=”*60) print(“California Housing Dataset”) print(“=”*60) print(f”データの形状: {df.shape}”) # (行数, 列数) print(f”サンプル数: {len(df):,}”) print(f”特徴量数: {len(df.columns) – 1}”) # 目的変数を除く
============================================================ California Housing Dataset ============================================================ データの形状: (20640, 9) サンプル数: 20,640 特徴量数: 8
# 特徴量の説明を表示 print(“\n特徴量の説明:”) print(” MedInc : 世帯収入の中央値(万ドル)”) print(” HouseAge : 築年数の中央値”) print(” AveRooms : 平均部屋数”) print(” AveBedrms : 平均寝室数”) print(” Population : 地区の人口”) print(” AveOccup : 平均世帯人数”) print(” Latitude : 緯度”) print(” Longitude : 経度”) print(” MedHouseVal: 住宅価格の中央値(目的変数、10万ドル単位)”) # 欠損値の確認 print(“\n欠損値の確認:”) print(df.isnull().sum())
特徴量の説明: MedInc : 世帯収入の中央値(万ドル) HouseAge : 築年数の中央値 AveRooms : 平均部屋数 AveBedrms : 平均寝室数 Population : 地区の人口 AveOccup : 平均世帯人数 Latitude : 緯度 Longitude : 経度 MedHouseVal: 住宅価格の中央値(目的変数、10万ドル単位) 欠損値の確認: MedInc 0 HouseAge 0 AveRooms 0 AveBedrms 0 Population 0 AveOccup 0 Latitude 0 Longitude 0 MedHouseVal 0 dtype: int64
✅ データ確認のポイント
  • 欠損値なし → 欠損処理は不要
  • 全て数値データ → カテゴリカル変換は不要
  • 目的変数は10万ドル単位 → RMSE 0.5 = 約5万ドルの誤差
3
探索的データ分析(EDA)
EDAの目的:
  • 目的変数の分布を確認
  • 特徴量と目的変数の関係を把握
  • 外れ値やデータの問題を発見
# 目的変数の分布を確認 print(“目的変数(住宅価格)の統計:”) print(f” 最小値: {df[‘MedHouseVal’].min():.2f}”) print(f” 最大値: {df[‘MedHouseVal’].max():.2f}”) print(f” 中央値: {df[‘MedHouseVal’].median():.2f}”) print(f” 平均値: {df[‘MedHouseVal’].mean():.2f}”)
目的変数(住宅価格)の統計: 最小値: 0.15 最大値: 5.00 中央値: 1.80 平均値: 2.07
⚠️ 発見:価格に上限がある

最大値が5.00で打ち切られている($500,000がキャップ)。
これは予測精度に影響する可能性がある。

# 特徴量と目的変数の相関を確認 # corrwith(): 各特徴量とyの相関係数を計算 correlations = df.corr()[‘MedHouseVal’].drop(‘MedHouseVal’) # 相関の絶対値でソート correlations_sorted = correlations.abs().sort_values(ascending=False) print(“目的変数との相関係数(絶対値順):”) print(correlations_sorted.round(3))
目的変数との相関係数(絶対値順): MedInc 0.688 AveRooms 0.152 Latitude 0.144 HouseAge 0.106 Longitude 0.046 AveBedrms 0.047 Population 0.025 AveOccup 0.024 dtype: float64
✅ EDAの発見
  • MedInc(収入)が最も強い正の相関(0.688)→ 最重要特徴量
  • Latitude(緯度)に負の相関(北ほど安い傾向)
  • その他の特徴量は相関が弱い
4
データ前処理と分割
重要:データ分割のタイミング

前処理の前にデータを分割します。
これにより、テストデータの情報が訓練に漏れること(データリーク)を防ぎます。

# データの分割 from sklearn.model_selection import train_test_split # 特徴量(X)と目的変数(y)を分離 X = df.drop(‘MedHouseVal’, axis=1) # 目的変数以外 y = df[‘MedHouseVal’] # 目的変数 # 訓練データとテストデータに分割(80:20) # random_state: 再現性のため固定 # test_size=0.2: 20%をテストに使用 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, # テストデータの割合 random_state=42 # 乱数シード(再現性のため) ) print(f”訓練データ: {X_train.shape[0]:,} サンプル”) print(f”テストデータ: {X_test.shape[0]:,} サンプル”)
訓練データ: 16,512 サンプル テストデータ: 4,128 サンプル

カスタム前処理クラスを作ろう

📊 なぜカスタムクラスを作るのか?
  • Pipeline統合:scikit-learnのPipelineに組み込める
  • データリーク防止:交差検証で各フォールドの訓練データのみから統計量を計算
  • 再利用性:同じ前処理を訓練とテストに適用できる
  • 本番対応:モデルと一緒に保存・読み込みできる

scikit-learn互換のカスタムクラスに必要な要素:

  • BaseEstimator:scikit-learnの基本クラス。パラメータの取得・設定機能を提供
  • TransformerMixinfit_transform()メソッドを自動生成
  • fit(X, y=None):訓練データから統計量を計算(学習)
  • transform(X):fitで計算した統計量を使ってデータを変換
# 外れ値の処理(キャッピング)- カスタムトランスフォーマー from sklearn.base import BaseEstimator, TransformerMixin import numpy as np class OutlierClipper(BaseEstimator, TransformerMixin): “”” 外れ値をクリッピング(上下限でカット)するカスタムトランスフォーマー Parameters: ———– lower_percentile : int, default=1 下限のパーセンタイル(これ以下の値は下限値に置き換え) upper_percentile : int, default=99 上限のパーセンタイル(これ以上の値は上限値に置き換え) Attributes: ———– lower_ : array fit()で計算された各列の下限値 upper_ : array fit()で計算された各列の上限値 “”” def __init__(self, lower_percentile=1, upper_percentile=99): # パラメータを保存(これはfit()の前に呼ばれる) self.lower_percentile = lower_percentile self.upper_percentile = upper_percentile def fit(self, X, y=None): “”” 訓練データから上下限を計算(学習) なぜfit()が必要か? → テストデータに適用するとき、訓練データの統計量を使うため → これによりデータリークを防ぐ “”” # 各列のlower_percentileとupper_percentileを計算 self.lower_ = np.percentile(X, self.lower_percentile, axis=0) self.upper_ = np.percentile(X, self.upper_percentile, axis=0) return self # selfを返すのがscikit-learnの慣例 def transform(self, X): “”” fit()で計算した上下限を使ってデータをクリップ np.clip(X, min, max): Xの値をmin〜maxの範囲に収める “”” X_clipped = np.clip(X, self.lower_, self.upper_) return X_clipped
# 外れ値が多い列を確認 print(“AveOccupの統計(外れ値あり):”) print(f” 平均: {X_train[‘AveOccup’].mean():.2f}”) print(f” 中央値: {X_train[‘AveOccup’].median():.2f}”) print(f” 最大値: {X_train[‘AveOccup’].max():.2f}”) # 非常に大きい! # OutlierClipperを使用 clipper = OutlierClipper(lower_percentile=1, upper_percentile=99) # fit(): 訓練データから上下限を学習 clipper.fit(X_train) print(f”\n学習された上下限(AveOccup列、インデックス5):”) print(f” 下限(1%タイル): {clipper.lower_[5]:.2f}”) print(f” 上限(99%タイル): {clipper.upper_[5]:.2f}”)
5
特徴量エンジニアリング
📊 なぜ特徴量エンジニアリングが重要か?
  • 元の特徴量だけでは捉えられない関係性を表現できる
  • モデルの性能を大幅に向上させる可能性がある
  • ドメイン知識を活かして意味のある特徴量を作れる

各特徴量を作る理由を理解しよう

# 新しい特徴量の作成 def create_features(df): “”” 住宅価格予測のための特徴量を作成する関数 Parameters: ———– df : DataFrame 元の特徴量を含むDataFrame Returns: ——– DataFrame : 新しい特徴量が追加されたDataFrame “”” df = df.copy() # 元のDataFrameを変更しないようにコピー # ========== 特徴量1: BedroomRatio ========== # 部屋数に対する寝室の割合 # # なぜ作る? # → 寝室が多い物件は家族向け # → 家族向け物件は価格帯が異なる可能性 # → AveBedrmsとAveRoomsを別々に見るより、比率の方が情報量が多い df[‘BedroomRatio’] = df[‘AveBedrms’] / df[‘AveRooms’] # ========== 特徴量2: PopPerRoom ========== # 部屋あたりの人口(混雑度の指標) # # なぜ作る? # → 人気エリアほど人口密度が高い # → 混雑度は住みやすさに影響 # → 住みやすさは価格に反映される df[‘PopPerRoom’] = df[‘Population’] / df[‘AveRooms’] # ========== 特徴量3: IncomeAge ========== # 収入と築年数の交互作用 # # なぜ作る? # → 高収入エリアの古い物件は「ビンテージ」として価値が高い傾向 # → 低収入エリアの古い物件は単に「古い」だけで価値が低い傾向 # → 収入と築年数の「組み合わせ」が重要 df[‘IncomeAge’] = df[‘MedInc’] * df[‘HouseAge’] return df # 特徴量を作成 X_train_fe = create_features(X_train) X_test_fe = create_features(X_test) print(“新しい特徴量:”) print(X_train_fe[[‘BedroomRatio’, ‘PopPerRoom’, ‘IncomeAge’]].head()) print(f”\n特徴量数: {X_train.shape[1]} → {X_train_fe.shape[1]}”)
新しい特徴量: BedroomRatio PopPerRoom IncomeAge 14196 0.153 73.124 226.520 8267 0.213 120.320 109.200 17445 0.196 80.000 174.720 14265 0.200 59.600 186.560 2732 0.213 66.780 119.600 特徴量数: 8 → 11
💡 特徴量エンジニアリングのコツ
  • ドメイン知識を活用:不動産なら「立地」「広さ」「築年数」が重要と知っている
  • 交互作用を考える:2つの特徴量の「組み合わせ」に意味があるか?
  • 比率を考える:絶対値より比率の方が比較しやすいことが多い
  • 効果を検証:作った特徴量と目的変数の相関を確認する
6
モデルの構築と評価

評価関数を作ろう

なぜ評価関数を作るのか?

  • 複数のモデルを同じ基準で比較できる
  • コードの重複を避ける(DRY原則)
  • 結果を辞書形式でまとめられる
# モデル評価関数の作成 from sklearn.metrics import mean_squared_error, r2_score import numpy as np def evaluate_model(model, X_train, X_test, y_train, y_test): “”” モデルを訓練し、評価指標を計算する関数 Parameters: ———– model : estimator 評価するモデル(未訓練) X_train, X_test : array-like 訓練/テストの特徴量 y_train, y_test : array-like 訓練/テストの目的変数 Returns: ——– dict : 評価結果の辞書 – train_rmse: 訓練データのRMSE – test_rmse: テストデータのRMSE – test_r2: テストデータのR²スコア – model: 訓練済みモデル “”” # ステップ1: モデルを訓練 # fit()で訓練データを学習 model.fit(X_train, y_train) # ステップ2: 予測を生成 # predict()で訓練/テストデータの予測値を計算 y_train_pred = model.predict(X_train) # 訓練データの予測 y_test_pred = model.predict(X_test) # テストデータの予測 # ステップ3: RMSE(平均二乗誤差の平方根)を計算 # RMSE = √(Σ(実際値 – 予測値)² / n) # 小さいほど良い。単位は目的変数と同じ(ここでは10万ドル) train_rmse = np.sqrt(mean_squared_error(y_train, y_train_pred)) test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred)) # ステップ4: R²スコア(決定係数)を計算 # R² = 1 – (残差の分散 / 目的変数の分散) # 1に近いほど良い。0は平均値で予測したのと同じ test_r2 = r2_score(y_test, y_test_pred) # ステップ5: 結果を辞書で返す return { ‘train_rmse’: train_rmse, ‘test_rmse’: test_rmse, ‘test_r2’: test_r2, ‘model’: model # 訓練済みモデルも返す(後で使う) }

複数のモデルを比較する

# 比較するモデルの定義 from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline # モデルを辞書で定義 # なぜ辞書?→ forループで順番に処理できる models = { ‘Linear Regression’: Pipeline([ (‘scaler’, StandardScaler()), # 線形モデルはスケーリングが重要 (‘model’, LinearRegression()) ]), ‘Ridge’: Pipeline([ (‘scaler’, StandardScaler()), (‘model’, Ridge(alpha=1.0)) # L2正則化 ]), ‘Random Forest’: RandomForestRegressor( n_estimators=100, # 100本の木 max_depth=10, # 深さを制限(過学習防止) random_state=42, n_jobs=-1 # 並列処理 ), ‘Gradient Boosting’: GradientBoostingRegressor( n_estimators=100, max_depth=5, random_state=42 ) } # 各モデルを評価 print(“=” * 70) print(“モデル比較(特徴量エンジニアリング後)”) print(“=” * 70) results = {} for name, model in models.items(): # evaluate_model()で評価 result = evaluate_model(model, X_train_fe, X_test_fe, y_train, y_test) results[name] = result # 結果を表示 print(f”\n{name}:”) print(f” 訓練 RMSE: {result[‘train_rmse’]:.4f}”) print(f” テスト RMSE: {result[‘test_rmse’]:.4f}”) print(f” テスト R²: {result[‘test_r2’]:.4f}”) # 過学習の診断 gap = result[‘train_rmse’] – result[‘test_rmse’] if gap < -0.1: print(f" ⚠️ 過学習の可能性(ギャップ: {gap:.4f})")
====================================================================== モデル比較(特徴量エンジニアリング後) ====================================================================== Linear Regression: 訓練 RMSE: 0.7246 テスト RMSE: 0.7276 テスト R²: 0.6024 Ridge: 訓練 RMSE: 0.7246 テスト RMSE: 0.7276 テスト R²: 0.6024 Random Forest: 訓練 RMSE: 0.3745 テスト RMSE: 0.5126 テスト R²: 0.8026 Gradient Boosting: 訓練 RMSE: 0.4523 テスト RMSE: 0.5234 テスト R²: 0.7942
✅ モデル比較の結論

Random Forestが最も良い結果(RMSE: 0.5126、R²: 0.8026)
ただし、訓練とテストのギャップがあり、若干の過学習の兆候あり。

7
ハイパーパラメータチューニング
# Random Forestをチューニング from sklearn.model_selection import GridSearchCV # チューニングするパラメータの候補 param_grid = { ‘n_estimators’: [100, 200], # 木の数 ‘max_depth’: [10, 15, 20], # 各木の最大深さ ‘min_samples_split’: [2, 5], # 分割に必要な最小サンプル数 ‘min_samples_leaf’: [1, 2] # 葉に必要な最小サンプル数 } # GridSearchCVを作成 grid_search = GridSearchCV( RandomForestRegressor(random_state=42, n_jobs=-1), param_grid, cv=5, # 5-Fold CV scoring=’neg_root_mean_squared_error’, # RMSE(負値) n_jobs=-1, verbose=1 ) # 実行 grid_search.fit(X_train_fe, y_train) print(f”\n最適なパラメータ: {grid_search.best_params_}”) print(f”最良のCV RMSE: {-grid_search.best_score_:.4f}”)
Fitting 5 folds for each of 24 candidates, totalling 120 fits 最適なパラメータ: {‘max_depth’: 20, ‘min_samples_leaf’: 1, ‘min_samples_split’: 2, ‘n_estimators’: 200} 最良のCV RMSE: 0.5023
# 最適モデルでテストデータを評価 best_model = grid_search.best_estimator_ y_test_pred = best_model.predict(X_test_fe) test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred)) test_r2 = r2_score(y_test, y_test_pred) print(f”最終テスト RMSE: {test_rmse:.4f}”) print(f”最終テスト R²: {test_r2:.4f}”)
最終テスト RMSE: 0.4987 最終テスト R²: 0.8131
8
特徴量重要度の分析と結論
# 特徴量重要度を確認 importances = pd.DataFrame({ ‘feature’: X_train_fe.columns, ‘importance’: best_model.feature_importances_ }).sort_values(‘importance’, ascending=False) print(“特徴量重要度:”) print(importances.to_string(index=False))
特徴量重要度: feature importance MedInc 0.524 IncomeAge 0.123 Latitude 0.089 Longitude 0.078 AveOccup 0.052 HouseAge 0.042 PopPerRoom 0.035 AveRooms 0.025 Population 0.018 BedroomRatio 0.009 AveBedrms 0.005
📊 最終結果サマリー

モデル:Random Forest(チューニング済み)
テスト RMSE:0.4987(約49,870ドルの誤差)
テスト R²:0.8131(81.3%の分散を説明)
最重要特徴量:MedInc(世帯収入)が52.4%

✅ プロジェクト完了チェックリスト

問題定義とビジネス目標を明確にした
データの取得と基本的な確認を行った
EDAで重要な特徴を発見した
データを訓練/テストに分割した
新しい特徴量を作成した
複数のモデルを比較した
ハイパーパラメータをチューニングした
特徴量重要度を分析した
最終結果をまとめた

📋 完成コード:住宅価格予測プロジェクト

# 住宅価格予測プロジェクト – 完成コード import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score from sklearn.base import BaseEstimator, TransformerMixin # ========== 1. データ読み込み ========== california = fetch_california_housing() df = pd.DataFrame(california.data, columns=california.feature_names) df[‘MedHouseVal’] = california.target # ========== 2. 特徴量エンジニアリング ========== def create_features(df): df = df.copy() df[‘BedroomRatio’] = df[‘AveBedrms’] / df[‘AveRooms’] df[‘PopPerRoom’] = df[‘Population’] / df[‘AveRooms’] df[‘IncomeAge’] = df[‘MedInc’] * df[‘HouseAge’] return df # ========== 3. データ分割 ========== X = df.drop(‘MedHouseVal’, axis=1) y = df[‘MedHouseVal’] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 特徴量エンジニアリング適用 X_train_fe = create_features(X_train) X_test_fe = create_features(X_test) # ========== 4. モデル構築とチューニング ========== param_grid = { ‘n_estimators’: [100, 200], ‘max_depth’: [10, 15, 20], ‘min_samples_split’: [2, 5] } rf = RandomForestRegressor(random_state=42, n_jobs=-1) grid_search = GridSearchCV( rf, param_grid, cv=5, scoring=’neg_root_mean_squared_error’, n_jobs=-1 ) grid_search.fit(X_train_fe, y_train) # ========== 5. 評価 ========== best_model = grid_search.best_estimator_ y_test_pred = best_model.predict(X_test_fe) test_rmse = np.sqrt(mean_squared_error(y_test, y_test_pred)) test_r2 = r2_score(y_test, y_test_pred) print(f”最適パラメータ: {grid_search.best_params_}”) print(f”テスト RMSE: {test_rmse:.4f}”) print(f”テスト R²: {test_r2:.4f}”) # ========== 6. 特徴量重要度 ========== importances = pd.DataFrame({ ‘feature’: X_train_fe.columns, ‘importance’: best_model.feature_importances_ }).sort_values(‘importance’, ascending=False) print(“\n特徴量重要度:”) print(importances.head(10))

📝 STEP 29 のまとめ

✅ このプロジェクトで実践したこと
  • EDA:データの分布、相関、外れ値の確認
  • 前処理:データ分割(訓練/テスト)
  • 特徴量エンジニアリング:新しい特徴量の作成
  • モデル比較:線形回帰、Ridge、Random Forest、Gradient Boosting
  • チューニング:GridSearchCVでハイパーパラメータ最適化
  • 評価:RMSE、R²、特徴量重要度
🚀 次のステップへ

次のSTEP 30では、独立プロジェクトに挑戦します。自分で選んだテーマで、学んだスキルを総動員してプロジェクトを完成させましょう!

❓ よくある質問

Q1. このモデルをさらに改善するには?
・XGBoostやLightGBMを試す
・より高度な特徴量エンジニアリング(地理的特徴量など)
・アンサンブル(スタッキング)を使う
・外部データ(学区の評価、犯罪率など)を追加
Q2. このモデルを本番環境で使うには?
・モデルをpickleやjoblibで保存
・Flask/FastAPIでAPIを作成
・入力データの検証を追加
・定期的な再学習の仕組みを構築
Q3. 特徴量エンジニアリングのアイデアはどこから?
ドメイン知識:不動産業界の知識(立地、広さ、築年数が重要)
EDA:データを探索して関係性を発見
Kaggle:他の人のノートブックを参考にする
試行錯誤:作って効果を検証する
📝

学習メモ

機械学習入門 - Step 29

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE