Day 085 — House Prices EDA — 回帰コンペの特徴量工学入門

2026-07-05 水 / Phase 3 コーディング House Prices / log変換 / TotalSF / RMSLE / 欠損補完

📚 背景知識(読んでから問題へ)

🏠
Day 084 で Playground Series の戦略を学びました。今日はKaggle最頻出コンペ「House Prices」を題材に、回帰コンペの特徴量工学を実践します。

House Prices コンペとは?

アメリカ・アイオワ州エイムズの住宅データから販売価格(SalePrice)を予測する Getting Started 系コンペです。81個の特徴量と評価指標 RMSLE が特徴的です。

SalePrice 分布確認
log1p 変換で正規化
特徴量エンジニアリング
欠損補完
相関分析

なぜ SalePrice を対数変換するのか?

住宅価格の分布は「右に引っ張られた(右裾が長い)」形です。高価な家が少数ながらスコアに大きく影響するため、log1p(SalePrice) を予測することで分布を正規分布に近づけます

💡
RMSLE の直感: 「1000万円の誤差」は安い家(2000万)では50%の誤差ですが、高い家(1億)では10%の誤差。RMSLE はこの相対誤差を均等に扱います。対数変換するとこれが自動的に実現できます。

🗂️ データスキーマ — House Prices(主要カラム)

81変数のうち特徴量工学で重要な主要カラムを抜粋

カラム名 説明 工学の方向
SalePriceint販売価格(目的変数)log1p 変換して使う
1stFlrSFint1階床面積 (sqft)TotalSF の構成要素
2ndFlrSFint2階床面積 (sqft)TotalSF の構成要素
TotalBsmtSFint地下室合計面積TotalSF の構成要素
OverallQualint総合品質 1〜10SalePrice との相関最高(r≈0.79)
GrLivAreaint地上面積(居住部分)相関高い(r≈0.70)
YearBuiltint建設年HouseAge = YrSold - YearBuilt
YrSoldint売却年築年数・リフォーム年数の計算に使用
GarageAreaintガレージ面積HasGarage フラグ作成
LotFrontagefloat道路に接する間口18%欠損 → 同地区中央値で補完
Neighborhoodstr地区名LotFrontage 補完 / Target Enc の軸
GarageTypestrガレージ種別NaN = ガレージなし → 'None' カテゴリ
PoolQCstrプール品質NaN = プールなし → 'None' カテゴリ
TotalSFint合計床面積(工学済み)SalePrice との相関がトップクラス

🎯 問題

⚠️
このコンペの評価指標は RMSLE です。log1p(SalePrice) を目的変数にして学習し、予測後に expm1 で元に戻してください。
1
SalePrice の歪度確認と log1p 変換
skew() を計算 → log1p 変換後に歪度が改善されることを確認
2
新特徴量を5つ作成
TotalSF / HouseAge / RemodelAge / TotalBath / HasGarage
3
意味のある欠損値補完を3箇所実施
GarageType, PoolQC → 'None' / LotFrontage → Neighborhood別中央値
4
SalePrice との相関係数トップ10を横棒グラフで可視化

📊 SalePrice の歪度 — log 変換の効果

歪度(Skewness)が 0 に近いほど正規分布に近い。|1| を超えたら変換を検討

元の SalePrice
歪度 ≈ 1.88(右裾が長い)
≈ 1.88
log1p(SalePrice)
≈ 0.12
≈ 0.12

SalePrice 分布のイメージ(右裾が長い → log変換で対称に)

元の SalePrice(歪んだ分布) log1p 後(正規分布に近い) log1p

📈 SalePrice(log) との相関係数 トップ10(参考値)

特徴量エンジニアリング後の典型的な相関ランキング

OverallQual
r = 0.82
0.82
TotalSF ★工学
r = 0.78
0.78
GrLivArea
r = 0.70
0.70
GarageArea
r = 0.64
0.64
TotalBath ★工学
r = 0.63
0.63
1stFlrSF
r = 0.60
0.60
YearBuilt
r = 0.52
0.52
HouseAge ★工学
r = −0.51
0.51
TotalBsmtSF
r = 0.61
0.61
FullBath
r = 0.55
0.55
💡
★工学マークは今日作る新特徴量です。TotalSF は元変数より相関が高く、TotalBath も既存変数を上回っています — 特徴量エンジニアリングの効果がわかります。

💡 ヒント

ヒント 1 方向性

TotalSF など「合計」系の特徴量は元のバラバラな変数より SalePrice と高い相関を持ちます。「部屋の数より部屋の広さの合計」の方が価格を説明しやすいからです。

欠損値の扱いは必ず「欠損の意味」を確認しましょう。GarageType = NaN は「データなし」ではなく「ガレージがない家」という重要な情報です。

ヒント 2 アプローチ

歪度の確認: df['SalePrice'].skew() — 正の値なら右裾が長い(log変換が有効)

グループ補完: df.groupby('Neighborhood')['LotFrontage'].transform('median')

相関は select_dtypes(include='number').corr()['SalePrice_log'] で一括計算できます。

ヒント 3 コード骨格
# 合計床面積
df['TotalSF'] = df['1stFlrSF'] + df['2ndFlrSF'] + df['TotalBsmtSF']

# 欠損値補完: LotFrontage はご近所の中央値で
df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'] \
                      .transform(lambda x: x.fillna(x.median()))

# 相関ランキング
corr = df.select_dtypes(include='number').corr()['SalePrice'] \
         .drop('SalePrice').abs().sort_values(ascending=False)
print(corr.head(10))

模範解答

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

# ── 0. データ読み込み ──────────────────────────────────────
try:
    train = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/train.csv')
except FileNotFoundError:
    # サンプルデータで代替(実際はKaggleからダウンロード)
    np.random.seed(42)
    n = 1460
    train = pd.DataFrame({
        'SalePrice':   np.random.lognormal(12.0, 0.4, n).astype(int),
        '1stFlrSF':    np.random.randint(400, 2000, n),
        '2ndFlrSF':    np.random.choice([0, 0, 0, 500, 800, 1200], n),
        'TotalBsmtSF': np.random.randint(0, 1500, n),
        'YrSold':      np.random.choice([2006,2007,2008,2009,2010], n),
        'YearBuilt':   np.random.randint(1900, 2010, n),
        'YearRemodAdd': np.random.randint(1950, 2010, n),
        'FullBath':    np.random.choice([1,2,3], n),
        'HalfBath':    np.random.choice([0,1], n),
        'BsmtFullBath': np.random.choice([0,1], n),
        'BsmtHalfBath': np.random.choice([0,1], n),
        'GarageArea':  np.random.choice([0, 0, 300, 500, 600, 800], n),
        'GarageType':  np.random.choice(['Attchd', 'Detchd', np.nan, np.nan], n),
        'PoolQC':      np.random.choice([np.nan, np.nan, np.nan, 'Gd', 'Ex'], n),
        'LotFrontage': np.where(np.random.random(n) < 0.18, np.nan,
                              np.random.randint(21, 130, n).astype(float)),
        'Neighborhood': np.random.choice(['NAmes','CollgCr','OldTown','Edwards','Somerst'], n),
        'OverallQual': np.random.randint(1, 11, n),
        'GrLivArea':   np.random.randint(400, 3000, n),
    })

# ── タスク1: SalePrice の歪度確認と対数変換 ────────────────
skewness_before = train['SalePrice'].skew()
skewness_after  = np.log1p(train['SalePrice']).skew()
print(f"元の歪度: {skewness_before:.4f}  →  log1p後: {skewness_after:.4f}")

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].hist(train['SalePrice'], bins=50, color='#dc2626', alpha=0.8)
axes[0].set_title(f'SalePrice(歪度={skewness_before:.2f})')
axes[1].hist(np.log1p(train['SalePrice']), bins=50, color='#16a34a', alpha=0.8)
axes[1].set_title(f'log1p(SalePrice)(歪度={skewness_after:.2f})')
plt.tight_layout()
plt.show()

# ── タスク2: 特徴量エンジニアリング ─────────────────────────
df = train.copy()
df['TotalSF']    = df['1stFlrSF'] + df['2ndFlrSF'] + df['TotalBsmtSF']
df['HouseAge']   = df['YrSold'] - df['YearBuilt']
df['RemodelAge'] = df['YrSold'] - df['YearRemodAdd']
df['TotalBath']  = (df['FullBath'] + df['BsmtFullBath']
                     + 0.5 * df['HalfBath'] + 0.5 * df['BsmtHalfBath'])
df['HasGarage']  = (df['GarageArea'] > 0).astype(int)

# ── タスク3: 意味のある欠損値補完 ──────────────────────────
df['GarageType'] = df['GarageType'].fillna('None')
df['PoolQC']     = df['PoolQC'].fillna('None')
df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'] \
                       .transform(lambda x: x.fillna(x.median()))

# ── タスク4: 相関係数トップ10 ─────────────────────────────
num_df = df.select_dtypes(include='number').copy()
num_df['SalePrice_log'] = np.log1p(df['SalePrice'])
corr_series = num_df.corr()['SalePrice_log'].drop('SalePrice_log')
corr_top10  = corr_series.abs().sort_values(ascending=False).head(10)

fig, ax = plt.subplots(figsize=(10, 5))
colors = ['#16a34a' if corr_series[f] >= 0 else '#dc2626'
          for f in corr_top10.index]
ax.barh(corr_top10.index[::-1], corr_top10.values[::-1],
        color=colors[::-1], alpha=0.85)
ax.set_xlabel('|相関係数|(対 log(SalePrice))')
ax.set_title('House Prices: SalePrice との相関トップ10')
ax.axvline(x=0.5, color='#fbbf24', linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()

🪜 Step-by-Step 解説

1 なぜ SalePrice を対数変換するのか

skewness_before = train['SalePrice'].skew()  # → ≈ 1.88(右裾が長い)
skewness_after  = np.log1p(train['SalePrice']).skew()  # → ≈ 0.12

歪度(skewness) = 分布の非対称さを数値で表したもの。0 に近い → 左右対称(正規分布に近い)。1以上 → 右裾が長い → 対数変換が有効です。

💡
log1p を使う理由: log(0) は計算できないため log(x+1) で 0 対策します。予測後は expm1 (exp(x)-1) で元に戻します。

2 TotalSF が最強の特徴量になる理由

df['TotalSF'] = df['1stFlrSF'] + df['2ndFlrSF'] + df['TotalBsmtSF']

元データでは床面積が3変数に分かれています。これを合計することで「家全体の広さ」を1変数で表現でき、SalePrice との相関が上がります。部分より全体が価格と相関するという直感通りの結果です。

3 欠損値は「情報」として使う

df['GarageType'] = df['GarageType'].fillna('None')
df['PoolQC']     = df['PoolQC'].fillna('None')

GarageType が NaN = そもそもガレージがない家 → 'None' という有意なカテゴリとして使えます。単純に削除や中央値補完するより情報を保持できます。

4 LotFrontage のグループ補完

df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'] \
                       .transform(lambda x: x.fillna(x.median()))

同じ地区(Neighborhood)の家は街路幅(LotFrontage)が似ています。全体の中央値より「同じ地区の中央値」で補完する方が精度が高くなります。これをグループ統計補完と呼び、Kaggle では頻繁に使われる定石テクニックです。

グループ補完のイメージ: 同じ Neighborhood のデータで補完

NAmes 地区 中央値: 70ft NaN → 70ft で補完 CollgCr 地区 中央値: 85ft NaN → 85ft で補完 OldTown 地区 中央値: 60ft NaN → 60ft で補完

📐 数学・統計の補足(文系向け)— RMSLE とは何か?

RMSLE = Root Mean Squared Log Error:「対数空間での誤差の二乗平均平方根」

指標罰則の与え方向いているケース
RMSE 絶対的な誤差(1000万円は常に大きい) 価格差が均等に重要な場合
RMSLE 相対的な誤差(10%ずれたことが同じ重さ) 安い家も高い家も同じ重要度の場合
📌
実装上のコツ:
目的変数を np.log1p(SalePrice) に変換して学習 → 予測値を np.expm1(pred) で元に戻す
この変換により、通常の RMSE を最小化することが RMSLE の最小化と同等になります

相関係数の強さの目安

|r| ≥ 0.7(強い相関)
特徴量として必ず使用
≥ 0.70
0.4 ≤ |r| < 0.7(中程度)
使用を検討
0.40〜0.69
|r| < 0.4(弱い相関)
単体では効きにくい
< 0.40

🏆 Kaggleでの実践的な使い方

状況House Prices での対応
最初の提出 TotalSF + OverallQual だけで LightGBM → RMSLE ≈ 0.15 が狙える
改善フェーズ Discussion で TotalSF, HouseAge などのアイデアを確認
上位狙い NeighborhoodごとのTarget Encoding を追加
最終調整 log1p(SalePrice) を予測 → expm1 で変換して submission
外れ値除去 GrLivArea > 4000 の2点は有名な外れ値 → 学習から除外すると精度向上

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
RMSLE コンペで SalePrice をそのまま予測 対数変換の必要性を知らない log1p(SalePrice) を目的変数にして最後に expm1 で戻す
NaN をすべて 0 や中央値で補完 欠損の意味を考えない 欠損が「設備なし」を意味する場合は 'None' カテゴリが有効
相関が高い特徴量だけ使う 線形相関のみに注目 非線形な関係(XGBoost が自動検出)も重要。相関係数は線形の指標に過ぎない
TotalSF と元の面積変数を両方入れる 多重共線性を気にしない GBDT は多重共線性に比較的強いが、不要な変数は削ることも有効
歪度が高い特徴量も変換しない 目的変数だけ変換すればOKと思っている 説明変数も歪度 > 0.5 なら log1p 変換を検討(線形モデルで特に有効)

🚀 次のステップ

📈
発展: NeighborhoodごとのTarget Encodingを追加し、相関係数の変化を観察する
次回予告: Optuna でハイパーパラメータ最適化 — LightGBM の num_leaves, learning_rate などを自動探索してスコアを底上げする

📝 自己評価(解いた後に記入)

自分の回答・実装メモ:

気づき・メモ: