📋 プロジェクト概要
- 目標:住宅の特徴から販売価格を予測するモデルを構築
- データ:California Housing Dataset(約20,000件)
- 問題タイプ:回帰(連続値の予測)
- 評価指標:RMSE(Root Mean Squared Error)
⏱️ 所要時間:約2時間
🎯 ビジネス課題
不動産会社が住宅の適正価格を自動で見積もるシステムを構築したい。
これにより、価格設定の効率化と精度向上を目指す。
【成功の定義】
・RMSE(平均二乗誤差の平方根)を最小化
・目標: RMSE < 0.5(10万ドル単位のデータなので、約5万ドル以内の誤差)
【制約】
・解釈可能性も重要(なぜその価格なのか説明できること)
・計算時間は数秒以内
最初に行うこと:
データの基本情報(サイズ、特徴量、型、欠損値)を確認します。
これにより、どのような前処理が必要かを把握できます。
# ライブラリのインポート
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万ドルの誤差
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(緯度)に負の相関(北ほど安い傾向)
- その他の特徴量は相関が弱い
重要:データ分割のタイミング
前処理の前にデータを分割します。
これにより、テストデータの情報が訓練に漏れること(データリーク)を防ぎます。
# データの分割
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の基本クラス。パラメータの取得・設定機能を提供
TransformerMixin:fit_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}”)
📊 なぜ特徴量エンジニアリングが重要か?
- 元の特徴量だけでは捉えられない関係性を表現できる
- モデルの性能を大幅に向上させる可能性がある
- ドメイン知識を活かして意味のある特徴量を作れる
各特徴量を作る理由を理解しよう
# 新しい特徴量の作成
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つの特徴量の「組み合わせ」に意味があるか?
- 比率を考える:絶対値より比率の方が比較しやすいことが多い
- 効果を検証:作った特徴量と目的変数の相関を確認する
評価関数を作ろう
なぜ評価関数を作るのか?
- 複数のモデルを同じ基準で比較できる
- コードの重複を避ける(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)
ただし、訓練とテストのギャップがあり、若干の過学習の兆候あり。
# 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
# 特徴量重要度を確認
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:他の人のノートブックを参考にする
試行錯誤:作って効果を検証する
artnasekai
#artnasekai #学習メモ