1.はじめに
実務では教育用データを扱っていますがそこには連続値だけでなくカテゴリ変数も多くあります。
例えば「英語がどのくらい得意か」「授業理解度が概ね満足しているかしていないか」など
Kaggleにはあまり教育データがないので代用として「House Prices - Advanced Regression Techniques」のデータを使い、「ベースライン → 特徴量エンジニアリング → モデルの改善 → アンサンブル」 と段階を踏んで精度を高めていきました。
また実務ではよくあるドキュメントの知識を使って特徴量エンジニアリングをするのでそこも意識しています。
この記事では、そのステップごとの工夫と改善プロセスを紹介します。
完成版コード:GitHub
2.分析内容
Step 1: データの可視化とEDA
データ読み込み
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
train.describe()
corr = train.corr()
sns.histplot(train["SalePrice"], kde=True)
sns.heatmap(corr, cmap="coolwarm", annot=False)
※ 最初に相関を可視化することで「どの特徴量がSalePriceと強い関係があるか」を把握。
※ 目的変数の歪みを確認→対数変換を実施
※ OverallQual(家の品質評価)と目的変数(SalePrice)にはある程度強い正の相関があること、OverallQualが上がると価格が一気に上昇することを可視化で確認
→ベースラインに使用検討
GitHub:01_EDA_and_Preprocessing
Step 2: 線形回帰・対数変換のベースライン
# OverallQualだけを使った単回帰
folds = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
X = train[["OverallQual"]]
y = np.log1p(train["SalePrice"])
def rmsle(y_true, y_pred):
y_pred[y_pred < 0] = 0
return np.sqrt(mean_squared_log_error(y_true, y_pred))
linearmodel = LinearRegression()
※ ベースライン(面積のみLinear)のRMSLE: 0.23(CV)
Step 3:LightGBMのベースライン
まずはデータをそのままLightGBMに学習させてみます。
(欠損値や外れ値の処理は一切しません)
import lightgbm as lgb
model = lgb.LGBMRegressor()
model.fit(X_train, np.log1p(y_train))
preds = np.expm1(model.predict(X_valid))
print("Baseline RMSLE:", rmsle(y_valid, preds))
※ ベースライン(面積のみlgbm)のRMSLE: 0.23(CV)
※ 線形回帰・対数変換とスコアが変わらず→アンサンブルも検討
Step 4: lgbmにおけるSHAP値の確認
LabelEncodingを行い、
各重要度のSHAP値を可視化し、すべての変数(Idと目的変数は除く)を説明変数としたときの精度も確認
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
shap.summary_plot(shap_values, X, max_display=max_display)
※ OverallQual,GrLivAreaなど重要度が高い変数を確認
※ labellgbmのRMSLE:0.1305(CV)
0.23 → 0.1305まで向上
※ その後、説明変数を重要度が高い変数のみで同じくモデル化しても精度はほぼ変わらず

Step 5: 特徴量エンジニアリング
住宅価格に効きそうな特徴量を作成します。
特徴量の追加
TotalSF = GrLivArea + TotalBsmtSF
OverallQual × GrLivArea
def create_features(df):
df['TotalSF'] = df['GrLivArea'] + df['TotalBsmtSF']
df['OverallQual_x_GrLivArea'] = df['OverallQual'] * df['GrLivArea']
return df
欠損値処理
次に欠損値を補完します。
Garage や Basement の欠損は「存在しない」と解釈して None や 0 に。
LotFrontage は Neighborhood ごとの中央値で補完。
def fill_missing_values(df):
df['GarageType'] = df['GarageType'].fillna('None')
df['GarageCars'] = df['GarageCars'].fillna(0)
df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'].transform(lambda x: x.fillna(x.median()))
return df
外れ値の除去
GrLivArea > 4000 なのに SalePrice < 300000 という極端なデータを除外します。
train = train.drop(train[(train['GrLivArea'] > 4000) & (train['SalePrice'] < 300000)].index)
※ 特徴量エンジニアリング後のRMSLE: 0.1301(CV)
※ 0.1305 → 0.1301へ
Step 6: One-Hot化と線形モデルの追加(Ridge/Lasso)
非線形モデルだけでなく、リッジ回帰やラッソ回帰も試してみました。
シンプルながら、カテゴリ変数のOne-Hot化との相性が良いです。
from sklearn.linear_model import Ridge, Lasso
ridge = Ridge(alpha=1.0)
ridge.fit(X_train, np.log1p(y_train))
ridge_preds = np.expm1(ridge.predict(X_valid))
combined = pd.concat([X_train, test], axis=0).reset_index(drop=True)
combined = pd.get_dummies(combined, dummy_na=True)
Step 7: アンサンブル
LightGBM・Ridge・Lassoを組み合わせて最終的なアンサンブルを構築しました。
ridge_oof, lasso_oof = run_linear_models(X_train, y_train_log)
lgbm_cv_rmsle, lgbm_oof, lgbm_model = run_lgbm_cv(X_train, y_train)
ensemble_oof = (lgbm_oof * 0.5 + ridge_oof * 0.25 + lasso_oof * 0.25)
※ 結果: Ensemble RMSLE 0.115(CV)
Kaggle Testデータでは 0.131 でした。
3.まとめ
特徴量エンジニアリングをしていると、「この変数を新しく作ると精度あがるかな」と思っても全く制度が変わらなかったり、逆にhotencodingするだけで精度が格段に向上したりと、やってみないとわからない面白さ・奥の深さを改めて実感しました。
なぜその作業をするのか、1つ1つ説明できるようにさらに技量をあげていきたい
