📚 背景知識(読んでから問題へ)
House Prices コンペとは?
アメリカ・アイオワ州エイムズの住宅データから販売価格(SalePrice)を予測する Getting Started 系コンペです。81個の特徴量と評価指標 RMSLE が特徴的です。
なぜ SalePrice を対数変換するのか?
住宅価格の分布は「右に引っ張られた(右裾が長い)」形です。高価な家が少数ながらスコアに大きく影響するため、log1p(SalePrice) を予測することで分布を正規分布に近づけます。
🗂️ データスキーマ — House Prices(主要カラム)
81変数のうち特徴量工学で重要な主要カラムを抜粋
| カラム名 | 型 | 説明 | 工学の方向 |
|---|---|---|---|
SalePrice | int | 販売価格(目的変数) | log1p 変換して使う |
1stFlrSF | int | 1階床面積 (sqft) | TotalSF の構成要素 |
2ndFlrSF | int | 2階床面積 (sqft) | TotalSF の構成要素 |
TotalBsmtSF | int | 地下室合計面積 | TotalSF の構成要素 |
OverallQual | int | 総合品質 1〜10 | SalePrice との相関最高(r≈0.79) |
GrLivArea | int | 地上面積(居住部分) | 相関高い(r≈0.70) |
YearBuilt | int | 建設年 | HouseAge = YrSold - YearBuilt |
YrSold | int | 売却年 | 築年数・リフォーム年数の計算に使用 |
GarageArea | int | ガレージ面積 | HasGarage フラグ作成 |
LotFrontage | float | 道路に接する間口 | 18%欠損 → 同地区中央値で補完 |
Neighborhood | str | 地区名 | LotFrontage 補完 / Target Enc の軸 |
GarageType | str | ガレージ種別 | NaN = ガレージなし → 'None' カテゴリ |
PoolQC | str | プール品質 | NaN = プールなし → 'None' カテゴリ |
TotalSF | int | 合計床面積(工学済み) | SalePrice との相関がトップクラス |
🎯 問題
log1p(SalePrice) を目的変数にして学習し、予測後に expm1 で元に戻してください。skew() を計算 → log1p 変換後に歪度が改善されることを確認
TotalSF / HouseAge / RemodelAge / TotalBath / HasGarage
GarageType, PoolQC → 'None' / LotFrontage → Neighborhood別中央値
📊 SalePrice の歪度 — log 変換の効果
歪度(Skewness)が 0 に近いほど正規分布に近い。|1| を超えたら変換を検討
SalePrice 分布のイメージ(右裾が長い → log変換で対称に)
📈 SalePrice(log) との相関係数 トップ10(参考値)
特徴量エンジニアリング後の典型的な相関ランキング
TotalSF は元変数より相関が高く、TotalBath も既存変数を上回っています — 特徴量エンジニアリングの効果がわかります。💡 ヒント
TotalSF など「合計」系の特徴量は元のバラバラな変数より SalePrice と高い相関を持ちます。「部屋の数より部屋の広さの合計」の方が価格を説明しやすいからです。
欠損値の扱いは必ず「欠損の意味」を確認しましょう。GarageType = NaN は「データなし」ではなく「ガレージがない家」という重要な情報です。
歪度の確認: df['SalePrice'].skew() — 正の値なら右裾が長い(log変換が有効)
グループ補完: df.groupby('Neighborhood')['LotFrontage'].transform('median')
相関は select_dtypes(include='number').corr()['SalePrice_log'] で一括計算できます。
# 合計床面積 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 のデータで補完
📐 数学・統計の補足(文系向け)— RMSLE とは何か?
RMSLE = Root Mean Squared Log Error:「対数空間での誤差の二乗平均平方根」
| 指標 | 罰則の与え方 | 向いているケース |
|---|---|---|
| RMSE | 絶対的な誤差(1000万円は常に大きい) | 価格差が均等に重要な場合 |
| RMSLE | 相対的な誤差(10%ずれたことが同じ重さ) | 安い家も高い家も同じ重要度の場合 |
目的変数を
np.log1p(SalePrice) に変換して学習 → 予測値を np.expm1(pred) で元に戻すこの変換により、通常の RMSE を最小化することが RMSLE の最小化と同等になります
相関係数の強さの目安
🏆 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 変換を検討(線形モデルで特に有効) |
🚀 次のステップ
次回予告: Optuna でハイパーパラメータ最適化 — LightGBM の
num_leaves, learning_rate などを自動探索してスコアを底上げする
📝 自己評価(解いた後に記入)
自分の回答・実装メモ:
気づき・メモ: