House Prices: Advanced Regression Techniques やってみた

#0.Intro
何ごとも始める時がまず大変で、その次は「自分なりの形」で「ある程度なんとかなりそう」というところに持っていくところが一つの山かと思います。
TitanicをSubmitしたのが7ヶ月前らしいです。それから半年以上経っています。何にもしていなかったわけではないですがなかなか時間がとれず。
しかし、幸か不幸か少し長い休み(1ヶ月程)を得たので、この機会に自分なりのKaggleのやり方みたいな物を形にしたいなと思い取り組み再開しました。
結果一つ纏まった形を作る事が出来たので記事にしたいと思い綴ることとしました。

#1.始める前に
機械学習もプログラミングに違いないのですが、所謂普通のプログラミングとはかなり毛色が違うように思えます。通常のプログラミングも結局データとにらめっこなのですが、機械学習のプログラミングはご存じの通りデータの処理の比重が極端に大きい。ですので、作業の進め方もしっくりくる形に落ち着くまでの時間かかりました。

##1.1.開発環境
今回のWindows 10上のWSLにUbuntuを入れ、そこにAnacondaの環境を構築。コーディングとコードの実行はVScodeで、データの確認等をJupyter notebookで行うという形にしました。将来的にがっつり開発するようになればおんぼろですが一応のグラボ付きサーバー機があるのでそちらに開発環境を移行したいです。

##1.2.コードの構成について
全体のコードのイメージが出来ないということは作る物がイメージできていないということで、それでは良い作業は出来ません。
色々なKaggleのカーネルを参照した結果

```
def feature_create(train,test):
#データのクリーニング
#変数のダミー化等データの作成を行う

def model_create(train):
#作成されたデータを目的変数とその他のに分割、
#標準化を行いモデルを作成する。


if __name__ == "__main__":
#メインロジック、ここでデータのインポートをし、上記の関数
#feature_create(train,test)とmodel_create(train)を呼び出し
#最終的にモデルをテストデータに適用し、予測結果を取得
#submit用csvデータを作成する。
```
という形で大枠を3段構成にしました。今回の少しサブルーチンが入ったぐらいですが、概ねメインと二つの関数でいけそうです。

##1.3.開発のスタイル
機械学習の開発は如何にデータを加工するかというところに比重が大きく傾いているのでエディタだけを見ていては進められません。色々試行錯誤とした結果、Jupyter notebookでデータの確認を行い、その上でデータの編集するコードを確認し、上手くいったコードを上の構成で言うところのfeature_create関数に追加していくという形が良さそうでした。

#2.プログラム全体

```
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import  cross_val_score

def feature_create(train,test):
    df_train = train.copy()
    df_test = test.copy()
    df_train["evel_set"] = 0
    df_test["evel_set"] = 1
    df_temp = pd.concat([df_train,df_test])
    del df_train,df_test
    df_temp.drop(["MiscFeature","MiscVal","PoolQC","PoolArea","Fireplaces","FireplaceQu","Alley","Fence"],axis=1,inplace=True)
    df_temp["MasVnrType"][df_temp["MasVnrType"].isnull()==True]="None"
    df_temp["MasVnrArea"][df_temp["MasVnrArea"].isnull()==True] = df_temp["MasVnrArea"].mean()
    df_temp["BsmtQual"][df_temp["BsmtQual"].isnull()==True] = "TA"
    df_temp["BsmtCond"][df_temp["BsmtCond"].isnull()==True] = "TA"
    df_temp["BsmtExposure"][df_temp["BsmtExposure"].isnull()==True] = "No"
    df_temp["BsmtFinType1"][df_temp["BsmtFinType1"].isnull()==True] = "Unf"
    df_temp["BsmtFinType2"][df_temp["BsmtFinType2"].isnull()==True] = "Unf"
    df_temp["Electrical"][df_temp["Electrical"].isnull()==True] = "SBrkr"
    df_temp["GarageType"][df_temp["GarageType"].isnull()==True] = "Attchd"
    df_temp["GarageYrBlt"][df_temp["GarageYrBlt"].isnull()==True] = df_temp["GarageYrBlt"][df_temp["GarageYrBlt"] > 2000].mean()
    df_temp["GarageFinish"][df_temp["GarageFinish"].isnull()==True] = "Unf"
    df_temp["GarageQual"][df_temp["GarageQual"].isnull()==True] = "TA"
    df_temp["GarageCond"][df_temp["GarageCond"].isnull()==True] = "TA"
    df_temp["BsmtFinSF1"][df_temp["BsmtFinSF1"].isnull()==True] = 0
    df_temp["BsmtFinSF2"][df_temp["BsmtFinSF2"].isnull()==True] = 0
    df_temp["BsmtFullBath"][df_temp["BsmtFullBath"].isnull()==True] = 0
    df_temp["BsmtHalfBath"][df_temp["BsmtHalfBath"].isnull()==True] = 0
    df_temp["BsmtUnfSF"][df_temp["BsmtUnfSF"].isnull()==True] = 0
    df_temp["Exterior1st"][df_temp["Exterior1st"].isnull()==True] = "VinylSd"
    df_temp["Exterior2nd"][df_temp["Exterior2nd"].isnull()==True] = "VinylSd"
    df_temp["Functional"][df_temp["Functional"].isnull()==True] = "Typ"
    df_temp["GarageArea"][df_temp["GarageArea"].isnull()==True] = 576
    df_temp["GarageCars"][df_temp["GarageCars"].isnull()==True] = 2
    df_temp["KitchenQual"][df_temp["KitchenQual"].isnull()==True] = "TA"
    df_temp["LotFrontage"][df_temp["LotFrontage"].isnull()==True] = 60
    df_temp["MSZoning"][df_temp["MSZoning"].isnull()==True] = "RL"
    df_temp["SaleType"][df_temp["SaleType"].isnull()==True] = "WD"
    df_temp["TotalBsmtSF"][df_temp["TotalBsmtSF"].isnull()==True] = 0
    df_temp["Utilities"][df_temp["Utilities"].isnull()==True] = "AllPub"

    #df_temp.drop(["MSSubClass","MSZoning","Street","LotShape","LandContour","Utilities","LotConfig","LandSlope","Neighborhood","Condition1","Condition2","BldgType","HouseStyle","OverallCond","RoofStyle","RoofMatl","Exterior1st","Exterior2nd","MasVnrType","ExterQual","ExterCond","Foundation","BsmtQual","BsmtCond","BsmtExposure","BsmtFinType1","BsmtFinType2","BsmtFinSF2","BsmtUnfSF","Heating","HeatingQC","CentralAir","Electrical","LowQualFinSF","BsmtFullBath","BsmtHalfBath","HalfBath","BedroomAbvGr","KitchenAbvGr","KitchenQual","Functional","GarageType","GarageYrBlt","GarageFinish","GarageQual","GarageCond","PavedDrive","EnclosedPorch","3SsnPorch","ScreenPorch","MoSold","YrSold","SaleType","SaleCondition"],axis=1,inplace=True)

    df_temp = pd.get_dummies(df_temp)
    
    df_train = df_temp[df_temp["evel_set"]==0]
    df_test = df_temp[df_temp["evel_set"]==1]
    df_train.drop("evel_set",axis=1,inplace=True)
    df_test.drop("evel_set",axis=1,inplace=True)
    del df_temp
    return df_train,df_test

def model_create(train):
    sc_x = StandardScaler()
    sc_y = StandardScaler()
    x_train = train
    y_train = train["SalePrice"]
    x_train.drop("SalePrice",axis=1,inplace=True)
    x_train.drop("Id",axis=1,inplace=True)

    x_train_std = sc_x.fit_transform(x_train)
    y_train_std = sc_y.fit_transform(y_train[:,np.newaxis]).flatten()

    gbrt = GradientBoostingRegressor(n_estimators=1000, learning_rate=.03, max_depth=3, max_features=.04, min_samples_split=4,
                                    min_samples_leaf=3, loss='huber', subsample=1.0, random_state=0)
    cv_gbrt = rmse_cv(gbrt,x_train_std, y_train_std)
    gbrt.fit(x_train_std, y_train_std)
    print('GradientBoosting CV score min: ' + str(cv_gbrt.min()) + ' mean: ' + str(cv_gbrt.mean()) 
        + ' max: ' + str(cv_gbrt.max()) )

    return gbrt,sc_y


def rmse_cv(model,X,y):
    rmse = np.sqrt(-cross_val_score(model, X, y, scoring = "neg_mean_squared_error", cv = 5))
    return(rmse)


if __name__ == "__main__":
    df_train_org = pd.read_csv("~/kaggle_train/House_Prices_Advanced/train.csv")
    df_test_org = pd.read_csv("~/kaggle_train/House_Prices_Advanced/test.csv")

    df_train,df_test = feature_create(df_train_org,df_test_org)
    del df_train_org,df_test_org
    
    model,scaler = model_create(df_train)
    sc_x = StandardScaler()
    df_test_Id = df_test["Id"]
    df_test = df_test.drop("Id",axis=1)
    df_test.drop("SalePrice",axis=1,inplace=True)

    df_test_std = sc_x.fit_transform(df_test)
    pred = model.predict(df_test_std)
    pred = scaler.inverse_transform(pred)
    df_sub_pred = pd.DataFrame(pred).rename(columns={0:"SalePrice"})
    df_submit = pd.DataFrame({
        "Id": df_test_Id,
        "SalePrice": df_sub_pred["SalePrice"]
    })
    df_submit.to_csv('submission.csv', index=False)

```
##2.1.feature_create
上述のようにJyupter notebookでコードを確認し、上手くいったコードを本関数に貼っていくという手法を採りました。作業中にローカルで作っていたJyupter notebookはこんな感じでした。
[House_Prices_Advanced_Regression_Techniques](https://www.kaggle.com/hazigin/house-prices-advanced-regression-techniques?scriptVersionId=40698642)

まずは訓練データとテストデータの結合ですが、多くの方はそのまま結合して、訓練データとテストデータの分割は並びが保証されているのでそのまま件数で分割されています。しかし、個人的にそれは少々気持ちが悪いので「evel_set」というカラムを作成して訓練データを0、テストデータを1として格納。あとで当該カラムを頼りに分割、後にカラムを落とすという方法を採りました。

今回のデータのクリーニングは欠損データの補完のみです。
カテゴリー値の場合は
1.存在するデータの種類と出現する回数を調べる。
2.最頻値を入れる
を繰り返してます。
数値の場合は時々平均値を入れたりもしています。

もっと色々やらなきゃならないがいいというのは重々知っていることは知っていますが、次回の課題としたいと思います。

あと最後のヒートマップですが最初整えたデータをそのままheatmap関数に入れていました。しかし、正しくは df_temp.corr()と相関係数のみにしたDataFrameをheatmap関数に渡すが正解でした。こちらも一人で嵌まってました。

あとDataFrameってinfo()で全カラム名とNANで無い値の数(つまりNANの数)と型が見られるんですね。今回初めて知りました。

##2.2.model_create

整えられた訓練データを取得、目的変数の「SalePrice」y_trainに、x_trainには訓練データを一旦全部入れ不要な「Id」と目的変数の「SalePrice」を落とし従属変数としました。
次に標準化です。今回はStandardScalerを使いました。この際、最初はx_trainのみ標準化しy_trainはそのままにして居ました。結果まともな評価値にならず…x_train,y_train双方を標準化すると一応まともな数値になりました。ここは落とし穴でした。

今回はモデリングについては拾ってきたコードをそのまま使ってます。本当はグリッドサーチなどしてパラメーターを自分で見付けるないといけないのでしょうが、今回は端折りました。

##2.3.if __name__ == "__main__":
データそのものを読み込み、feature_create関数に渡しています。返された編集済みデータを次のmodel_createに渡し学習されたモデルと標準化された目的変数を戻すStandardScalerのインスタンを得ます。
その後予測のデータの編集をします。テストデータのIdはSubmitに必要なので待避、予測には必要無いのでdropします。また最初結合した際本当はないはずの「SalePrice」もカラムとして出来ているのでこちらもdropします。
StandardScalerは一度fitした後、同じデータ形式のデータのみinverse_transform出来る仕様なのでしょぅか？今回はy_trainをfit_transformしたインスタンスを貰ってくると言う少々不格好な実現の仕方をしましたが。model.predictが返すのは一次元配列なので適当な一次元配列をfitさせてからだったら別のインスタンスでもinverse_transform出来るのでしょうか？また試してみたいです(というより、正しくStandardScalerの使い方調べるべきですね)。
予測された値をカラム名を付けてDataFrameへ変換します。
Submit用のDataFrameを作成します。先に待避したテストデータのIdと予測値が入ったDataFrameの「SalePrice」でそのままDataFrameで作成します。並びが保証されているので出来る作り方ですね。
あとはto_csvで出力して終わりです。

#3.結果
![EfS3QVZUEAAiGsD.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66198/b2b4d5fd-269a-fb38-4092-de826c6a0cc3.jpeg)
という結果になりました。5222中 1638位なので一応まともに参加している感じがするので参加賞ぐらいは貰えそうですw
8ヶ月前にSubmitした[Housing Prices Competition for Kaggle Learn Users]が43230中43000付近という何もしない方が良い様な結果だったので、そこからみると大分進歩したなぁと我ながら思います。

##3.1.今後の課題
分類はTitanicで少し触れているのではあとは時系列データに取り組んでみたいです。
その後は本格的にEDAを行い、データの調整や新しい特徴量の追加等を行えるようになりたいです。
それからDeep Learningの方に進みたいと思います。
でも仕事的にデータ分析の方に進んだ方が良いのかも知れませんけれどw