今回House Pricesというタイタニックとは別のコンペにも参加してみました。
https://www.kaggle.com/competitions/house-prices-advanced-regression-techniques
72の説明変数を用いて、住宅価格を予測するというものです。
講座を見て、見よう見まねでやっただけでは頭に残らないので、自分で考えてデータ分析を行ってみました。
ただ、途中からどうすればいいか分からなくなり、Qiitaの記事を参考にしながら(ほぼパクリ)進めてみました。
参考にしたQiita
https://qiita.com/muscle_nishimi/items/901ed94f3cdf1c8d893a
学んだこと
・対数変換
・ラッソ回帰
・提出フォーマットの確認
データの加工
型変換と欠損値処理
#パッケージの読み込み
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib import pyplot as plt
#データの読み込みはPandasの「read_csv()」
data_train = pd.read_csv('../input/house-prices-advanced-regression-techniques/train.csv')
data_test = pd.read_csv('../input/house-prices-advanced-regression-techniques/test.csv')
data_submission = pd.read_csv('../input/house-prices-advanced-regression-techniques/sample_submission.csv')
# 学習データの説明変数と、予測用データを結合
all_df = pd.concat([data_train.drop(columns='SalePrice'),data_test])
#数字が入っているが、数字の大小関係が予測に影響を与えない方が良いものはカテゴリ変数にする
#astype:データの型をstrに変換
num2str_list = ['MSSubClass','YrSold','MoSold']
for column in num2str_list:
all_df[column] = all_df[column].astype(str)
# 変数の型ごとに欠損値の扱いが異なるため、変数ごとに処理
for column in all_df.columns:
# dtypeがobjectの場合、文字列の変数
if all_df[column].dtype=='O':
all_df[column] = all_df[column].fillna('None')
# dtypeがint , floatの場合、数字の変数
else:
all_df[column] = all_df[column].fillna(0)
データの加工における下準備については、前回実施したタイタニックとおおむね同じです。
まず最初にライブラリのインポート、データを読み込んだ後、カテゴリ変数の変換や欠損値の処理をしました。
説明変数の追加
データの下準備でタイタニックと異なるのは以下の点です。
# 特徴量エンジニアリングによりカラムを追加する関数
def add_new_columns(df):
# 建物内の総面積 = 1階の面積 + 2階の面積 + 地下の面積
df["TotalSF"] = df["1stFlrSF"] + df["2ndFlrSF"] + df["TotalBsmtSF"]
# 一部屋あたりの平均面積 = 建物の総面積 / 部屋数
df['AreaPerRoom'] = df['TotalSF']/df['TotRmsAbvGrd']
# 築年数 + 最新リフォーム年 : この値が大きいほど値段が高くなりそう
df['YearBuiltPlusRemod']=df['YearBuilt']+df['YearRemodAdd']
# 合計の屋根付きの玄関の総面積
# Porch : 屋根付きの玄関 日本風にいうと縁側
df['TotalPorchSF'] = (df['OpenPorchSF'] + df['3SsnPorch'] + df['EnclosedPorch'] + df['ScreenPorch'] + df['WoodDeckSF'])
# プールの有無
df['HasPool'] = df['PoolArea'].apply(lambda x: 1 if x > 0 else 0)
# 2階の有無
df['Has2ndFloor'] = df['2ndFlrSF'].apply(lambda x: 1 if x > 0 else 0)
# カラムを追加
add_new_columns(all_df)
こちらは、訓練データから計算・加工されたデータを新たな列として追加しています。より精度の高いモデルを作成するために、新たな変数を増やしています。
別メソッドを用いてデータの加工・抽出
One-Hot-Encodingでは、タイタニックで使っていなかったget_dummies
を用いて、カテゴリ変数に変換しました。
訓練データから特異な物件のデータは削除します。ilocはindexを指定することで特定の列を外れ値として、除去できます。
こちらもタイタニックでは使っていませんでしたが、有用なメソッドです。
# One-Hot-Encoding
# pd.get_dummiesを使うとカテゴリ変数化できる。
all_df = pd.get_dummies(all_df)
all_df.head()
all_df.info()
# 学習データと予測データに分割して元のデータフレームに戻す。
# iloc:indexを指定することで特定の値を抽出できる
data_train = pd.merge(all_df.iloc[data_train.index[0]:data_train.index[-1]],data_train['SalePrice'],left_index=True,right_index=True)
data_test = all_df.iloc[data_train.index[-1]:]
対数変換について
ここで、今回特に新しく学んだ対数変換です。
多くの機械学習アルゴリズムは正規分布のデータを想定しているため、正規分布ではないデータに対して精度が出ない場合が多いです。
https://qiita.com/muscle_nishimi/items/901ed94f3cdf1c8d893a
そのとき、正規分布に従わない変数の対数変換をすると正規分布に近付くことがあります。
# SalePriceLogに対数変換した値を入れる。説明の都合上新たなカラムを作るが、基本的にそのまま代入して良い。
# np.log()は底がeの対数変換を行う。
data_train['SalePriceLog'] = np.log(data_train['SalePrice'])
# 対数変換後のヒストグラム、歪度、尖度
sns.histplot(data_train['SalePriceLog'])
print(f"歪度: {round(data_train['SalePriceLog'].skew(),4)}" )
print(f"尖度: {round(data_train['SalePriceLog'].kurt(),4)}" )
そもそも対数変換について知らなかったので、少し調べました。
対数変換が重要な理由
前提として、上記引用の通り、対数変換すると正規分布に近づくデータが存在します。
(対数正規分布と呼ばれる)
対数変換が重要なのは、正規分布に近づけることで、正規分布を仮定した解析手法(パラメトリックな手法)を適用することができるからです。
対数変換の特徴
左側から最頻値にかけて急激に上昇し、右に行くに連れてなだらかになるという特徴があります。
続いて、説明変数と目的変数をそれぞれ分けます。
# 学習データ、説明変数
train_X = data_train.drop(columns = ['SalePrice','SalePriceLog'])
#train_X = data_train
# 学習データ、目的変数
train_y = data_train['SalePriceLog']
# 予測データ、目的変数
test_X = data_train
# scikit-learn
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Lasso
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
ラッソ回帰を用いて、モデル作成
今回は機械学習アルゴリズムとしてラッソ回帰を用いて、モデルを作成しました。
def lasso_tuning(train_x,train_y):
# alphaパラメータのリスト
param_list = [0.001, 0.01, 0.1, 1.0, 10.0,100.0,1000.0]
for cnt,alpha in enumerate(param_list):
# パラメータを設定したラッソ回帰モデル
lasso = Lasso(alpha=alpha)
# パイプライン生成
pipeline = make_pipeline(StandardScaler(), lasso)
# 学習データ内でホールドアウト検証のために分割 テストデータの割合は0.3 seed値を0に固定
X_train, X_test, y_train, y_test = train_test_split(train_x, train_y, test_size=0.3, random_state=0)
# 学習
pipeline.fit(X_train,y_train)
# RMSE(平均誤差)を計算
train_rmse = np.sqrt(mean_squared_error(y_train, pipeline.predict(X_train)))
test_rmse = np.sqrt(mean_squared_error(y_test, pipeline.predict(X_test)))
# ベストパラメータを更新
if cnt == 0:
best_score = test_rmse
best_param = alpha
elif best_score > test_rmse:
best_score = test_rmse
best_param = alpha
# ベストパラメータのalphaと、そのときのMSEを出力
print('alpha : ' + str(best_param))
print('test score is : ' +str(round(best_score,4)))
# ベストパラメータを返却
return best_param
# best_alphaにベストパラメータのalphaが渡される。
best_alpha = lasso_tuning(train_X,train_y)
# ラッソ回帰モデルにベストパラメータを設定
lasso = Lasso(alpha = best_alpha)
# パイプラインの作成
pipeline = make_pipeline(StandardScaler(), lasso)
# 学習
pipeline.fit(train_X,train_y)
# 結果を予測
pred = pipeline.predict(test_X)
ラッソ回帰についても初めて知ったので、調べてみました。
ラッソ回帰を知る前に、まずは回帰分析について調べました。
回帰分析とは、予測したい値(目的変数)を1つもしくは複数の変数(説明変数)を用いて予測する分析手法です。
回帰分析には、単回帰分析と重回帰分析の2種類あります。
単回帰分析は、1つの説明変数からゴールとなる1つの目的変数(数値)を予測する分析手法です。
重回帰分析は、テレビCMや販促物、雑誌など複数の説明変数からゴールとなる1つの獲得契約件(目的変数)を予測するのが重回帰分析になります。
https://aiacademy.jp/texts/show/?id=33
ここまで調べると、重回帰分析の方がより精度の高いモデルを作成できそうというのは分かりました。(多すぎてもダメだとは思いますが)
それでは本題のラッソ回帰ですが、
ラッソ回帰では、不要なパラメータ(次元や特徴量)を削ることができ、リッジ回帰では過学習を抑えることができます。
過学習を防ぐために、使用する特徴量が減ってもいい場合はラッソ回帰、過学習を防ぎたいが、使用する特徴量が全て重要は場合はラッソ回帰を選択すると良いです。
https://aiacademy.jp/texts/show/?id=215
なるほど。何となく理解できました。今回は変数の量が多いため、ラッソ回帰が選ばれているのだと思います。
提出フォーマットの確認
最後は指数変換をして、提出用のデータに入れました。
データを加工している段階で行数が変わったのか、行数が違うというエラーが出ました。
遡ってinfo
メソッドで各段階の行数を確認して、ズレた時の処理を削除しました。
最終的には無事行数が一致して、提出用のCSVファイルを提出することができました。
# 指数変換
pred_exp = np.exp(pred)
# 指数変換した予測結果をプロット
sns.histplot(pred_exp)
# 歪度と尖度
print(f"歪度: {round(pd.Series(pred_exp).skew(),4)}" )
print(f"尖度: {round(pd.Series(pred_exp).kurt(),4)}" )
# 指数変換した値を代入
data_submission['SalePrice'] = pred_exp
# submission.csvを出力
data_submission.to_csv('submission.csv',index=False)
まとめ
今回は住宅価格のコンペに参加することで、統計学における重要な対数変換や回帰分析を学びました。
モデルの精度としては低いですが、1つずつ学ぶことで、点数を少しずつ上げていきます。
Kaggleを通じて、学ぶのもいいですが、データサイエンスそのものを体系的に学ぶのも良いと思うので、Joma Techという人のデータサイエンスコースを受けてみようと思いました。