【SIGNATE, 初心者向け】初コンペ参加と解析手法概要まとめ(Docker, Python)

冬休み中にSIGNATEさん主催のBignner向けのコンペがあったので勉強がてら参加してみました。

結果からご報告させていただくと、参加者数582人中12位でした。未経験・30代から始めた自分としては、満足する結果でした。

なので、今回は実施した内容を防備録も兼ねてまとめてみました。

最後までご覧いただけると幸いです。

1. コンペ概要

概要はコンペ概要をご覧いただければ幸いです。簡単に内容をまとめると、

  • アメリカ合衆国アイオワ州中央部に位置する都市エイムズの住居や周辺周辺環境に関する情報を元に、住宅の価格を予測する。
  • 回帰学習
  • 今回はBignner向けのコンペのため、データ欠損値なし
  • 精度評価は、評価関数「RMSE」を使用

となります。なので、はじめてコンペに参加するのはちょうどよい難易度だったと思います。

また、本コンペはSINGATEさんの情報公開ポリシーで「公開可」となっておりますので、ソースコードや解析手法と一部データを当ブログに記載しております。

ただし、生データなどは記載していませんので、予めご了承ください。

図1 本コンペにおけるSINGATEさんの情報公開ポリシー

2. コンペ環境構築

環境構築の再現性を高めるため、Dockerで環境開発を設定しています。

以下の本記事内容は、Docker環境で構築したAnacondaおよびjupyter-labをベースに作成しています。予めご了承ください。使用環境概要は以下となります。

  • ホストOS: Windows10 Pro
  • Docker image: ubuntu:18.04
  • Anaconda: Anaconda3-2020.07-Linux-x86_64
  • Python: Version 3.8.3 (64bit)

ファイルフォルダー構成は以下です。

.
|-- docker-compose.yml
|-- Dockerfile
|-- requirements.txt
`-- work
    `-- SIGNATE
        |-- housefee_analytics.ipynb
        |-- data
        |   |-- test.csv
        |   `-- train.csv
        `-- reg_analytics.py

Docker関連ファイル内容は以下です。

#Dockerfile
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
    sudo \
    wget \
  vim

#Anacondaのインストール
WORKDIR /opt
RUN wget https://repo.continuum.io/archive/Anaconda3-2020.07-Linux-x86_64.sh && \
    sh /opt/Anaconda3-2020.07-Linux-x86_64.sh -b -p /opt/anaconda3 && \
    rm -f Anaconda3-2020.07-Linux-x86_64.sh
ENV PATH /opt/anaconda3/bin:$PATH
WORKDIR /
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root", "--LabApp.token=''"]

また、Docker-compose.ymlは以下のようにしています。

version: '3'

services:
  app:
    build: .
    ports:
        - '5555:8888'
    volumes:
      - '.:/work'
    tty: true
    stdin_open: true
pip
sk-learn

それでは、コマンドプロンプトなどを用いて、Docker-compose.ymlのあるフォルダまで移動したら、以下のコマンドを入力してコンテナを立ち上げましょう。(初期のコンテナ作成時は時間がかかります)

$ docker-compose up -d --build

無事コンテナ内に環境構築できたら、ブラウザ(Chromeなど)のURLに「localhost:5555」と入力して、Jupyterlabを開きます。

その後、「work]」=> 「SIGANTE」のフォルダに移動いたら、解析用のipynbファイルを作成します。

本ブログでは、ファイル名を「housefee_analytics.ipynb」としています。

これで解析環境構築は完了です。

3. 本記事の流れ

コンペのデータ解析の手順は大まかに以下の内容で実施しました。

図2 データ解析手順

1) データ収集

ますはコンペサイトから訓練用データと、テスト用データを収集してjupyterLabに読み込みます。

2) データ前処理

おそらく、ここが一番重要だと思います。ここでのデータ処理がうまくできているかで、このあとのモデル構築およびデータ予測の結果がおおよそ決まるためです。

3) モデル構築

2)で処理した訓練データをもとに、目的変数(今回は住宅価格)を予測する解析モデルを作成します。

解析モデルを作成すると言っても、基本的にはモデル選択となるため、コード自体は結構一本道です。

4) データ予測

3)で構築した解析モデルとテストデータの説明変数を用いて、目的変数(今回は住宅価格)を予測してデータを取得します。

5) 提出用データの加工処理

4)で得られた結果をコンペが要求するデータ形式に加工し、提出用データを作成します。一回作成すれば、あとは再利用可能です。

と、以上が基本的な流れです。実際は、2)と3)を繰り返し実施し、解析モデルの精度が高くなった際にデータを予測してみる流れになると思います。

今回は特に2)の前処理手法をメインに記載してみたいと思います。

4. データ解析

以下で大まかなコードを記載していきます。

4-1. データ収集

""" 1) データ収集 """

import datetime
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns; sns.set(font='DejaVu Sans')
import reg_analytics as ra
%matplotlib inline

""" pandasの表示範囲を指定する """
pd.set_option('display.max_columns', 200)
pd.set_option('display.max_rows', 100)

""" データを取得してpandasデータにする """
train_raw = pd.read_csv('./data/train.csv') """ 学習・検証データ """
test_raw = pd.read_csv('./data/test.csv') """ テストデータ """
print('SalePrice: Mean ${0}, Median ${1}'.format(round(train_raw['SalePrice'].mean(), 0), train_raw['SalePrice'].median()))
print('The size of the train data:' + str(train_raw.shape))
print('The size of the test data:' + str(test_raw.shape))

まずはデータを取得して、shape関数でデータ配列を確認しておきます。

モデル構築ではデータ配列数がこのあと重要になります。

次に訓練データとテストデータを結合して、すべてのデータを結合したalldataを作成します。

train_data, test_data = train_raw.copy(), test_raw.copy()
train_data['train_or_test'] = 0
test_data['train_or_test'] = 1
""" テストにSalePriceカラムを仮置き """
test_data['SalePrice'] = 0 
alldata = pd.concat([train_data,test_data],sort=False,axis=0).reset_index(drop=True)

後で訓練データとテストデータを分割できるように’train or test’コラムを追加しつつ、このデータを用いて、全体データを変更していきます。

4-2. 前処理

ここからは、いくつか前処理として行ったコード例を示します。

まずは訓練データの外れ値を削除します。例えば、目的変数である住宅価格を見てみましょう。

まずはSeabornのdistplotで住宅価格を見てみます。

""" 2) データ前処理 """
sns.distplot(alldata[alldata['train_or_test'] == 0]['SalePrice'])
図2 住宅価格

図2を見ると、分布が左によっているので、正規分布とは言えないため、定石通り対数化していきます。

alldata['SalePrice_Log']=alldata['SalePrice'].apply(lambda x: np.log(x))
sns.distplot(alldata[alldata['train_or_test'] == 0]['SalePrice_Log'])
図3 住宅価格(対数表示)

正規分布に近づきましたが、高い住宅価格に外れ値があるようなので、’SalePrice_Log’が12.75以下のデータのみ採用することにします。

alldata = alldata[alldata['SalePrice_Log'] <= 12.75]    
図4 住宅価格(外れ値除去後)

これで外れ値を除去しました。

次に、シリーズデータの型変換です。特にstr型に変換すると、後ほど出てくるdummy関数に影響があるので、データを見ながら変更していきましょう。

int_list = ['<int型にしたいコラム名>']
alldata[int_list] = alldata[int_list].astype(int)

str_list = ['<str型にしたいコラム名>']
alldata[str_list] = alldata[str_list].astype(str)

主にstr型に変換するのは、データとしては数値だけど、カテゴリカルな意味合いである場合、str型に変換しています。

例えば、キッチンの状態を示すコラムがあり、評価が1から5で記載されている場合、キッチンの状態が数値で表現されていますが、数値的な意味(連続数として意味を持つ)を持つわけでなく分類として使用されているため、数値的な意味を排除すべく、str型に変換しておいたほうが良いです。

セールス年月や住宅の建築年を加工します。年月データはstr型でしたので、datetime型に変更しました。

alldata['Mo Sold'] = pd.to_datetime(alldata['Mo Sold'], format='%m').dt.strftime('%m')
alldata['Yr Sold'] = pd.to_datetime(alldata['Yr Sold'], format='%Y').dt.strftime('%Y')
alldata['Year Built'] = alldata['Year Built'].apply(lambda x: x[:3] + '0s')

建築年は範囲が広いため、apply関数で10年ごとに区切りをつけるようにしています。(例:1976年=>1970s)

また、カテゴラル変数の合計数を計算してみます。

alldata['Ex'] = (alldata == 'Ex').sum(axis=1)
alldata['Gd'] = (alldata == 'Gd').sum(axis=1)
alldata['TA'] = (alldata == 'TA').sum(axis=1)

上記コードにより、alldata内に出現している特定文字列の個数の合計を新たなコラムとして追加します。

続いて、説明変数において、訓練データには含まれており、テストデータに含まれていないで説明変数を除外します。

train_group = train_data_2['SalePrice'].groupby(train_data_2['MS SubClass']).agg(['count'])
test_group = test_data_2['SalePrice'].groupby(test_data_2['MS SubClass']).agg(['count'])

df_group = pd.concat([train_group, test_group], axis=1)
df_group

例えば、上記コードを実行すると、’MS subClass’をグループ化した際の’SalePrice’の表示数(個数)が表示されます。

この際に、下図のように訓練データには説明変数があり、テストデータにはない説明変数を訓練データから除外しておきます。

ここでは’MS subClass’=’180’を除外します。

図3 除外する説明変数
alldata = alldata[alldata['MS SubClass']!='180']

これで大まかなalldataの処理は完了です。

それでは定石通り、alldataにdummy関数を適用してdummyデータを作成します。(念の為、describeやshape関数でデータ内容を確認しておきます。)

alldata_dummy = pd.get_dummies(alldata, drop_first=True)
alldata_dummy.describe(include='all')
alldata.shape

5. データ解析(モデル構築)

前処理を施したalldata_dummyから訓練データを取り出して、説明変数(x)と目的変数(y)を作成します。

""" 3) モデル構築 """

train_dummy = alldata_dummy[alldata_dummy['train_or_test'] == 0]

""" 説明変数に必要のないコラムリスト """
exclude_list = ['SalePrice', 'SalePrice_Log']

""" 訓練データの説明変数を作成する """
x_train_reg_raw = train_dummy[train_dummy['train_or_test'] == 0].drop(exclude_list, axis=1)
x_train_reg = x_train_reg_raw.drop(['train_or_test'], axis=1)
""" 訓練データの目的変数を作成する """
y_train_reg = train_reg_select[train_reg_select['train_or_test'] == 0]['SalePrice_Log']

また、前処理を施したalldata_dummyからテストデータを取り出して、説明変数(x)を作成します。

""" テストデータの説明変数を作成する """
x_test_reg = alldata_dummy[alldata_dummy['train_or_test'] == 1].drop(exclude_list, axis=1)
x_test_reg = x_test_reg.drop(['train_or_test'], axis=1)

各データのデータ配列を確認しておきます。特に訓練データとテストデータのコラム数が一致していない場合、4)データ予測ができないため、注意が必要です。

""" 各データの配列を確認する """
print('The size of X_train data:' + str(x_train_reg.shape))
print('The size of y_train data:' + str(y_train_reg.shape))
print('The size of X_test data:' + str(test_reg.shape))

これでモデル構築の準備完了です。あとはモデルを構築するのみです。

今回は勾配ブースティング(GBR)でモデルを構築しています。なお、モデル構築やハイパーパラメータチューニングの説明は割愛します(勉強中)。

ra_Inst = ra.Regression()

""" 訓練データの分割 """
X_train, X_test, y_train, y_test = ra_Inst.train_test(x_train_reg, y_train_reg)
""" モデル学習 """
gbr = ra_Inst.gbr(X_train, y_train)
""" モデル精度検証 """
ra_Inst.accuracy_value(gbc, x_train_reg, y_train_reg)
""" reg_analytics.py """
import numpy as np
import pandas as pd
import sklearn.metrics as metrics
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import GradientBoostingRegressor 

class Regression():
    def __init__(self):
        self.cv = 3

    def train_test(self, X, y, size=0.3):
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=size, random_state=3)
        return X_train, X_test, y_train, y_test

    def gbr(self, X_train, y_train): 
        gbr = GradientBoostingRegressor()
        parameters = [{
                     'learning_rate':[0.01,0.02.0.05],
                     'n_estimators':[100,200,300],
                     'max_depth':list(range(1,7,1))}
                     ]
        gbr = GridSearchCV(gbr, parameters, cv=self.cv, n_jobs=-1)
        gbr.fit(X_train, y_train)
        gbr = gbr.best_estimator_
        return gbc

    def accuracy_value(self, reg, x_train_reg, y_train_reg):
        X_train, X_test, y_train, y_test = self.train_test(x_train_reg, y_train_reg)
        y_pred = reg.predict(X_test)

        print("R2(Train)=", reg.score(X_train, y_train).round(4))
        print("R2(Test)=", metrics.r2_score(y_test, y_pred).round(4))
        print("RMSE=", np.sqrt(metrics.mean_squared_error(y_test, y_pred)).round(4))
        print("MAE=", metrics.mean_absolute_error(y_test, y_pred).round(4))

モデル精度検証は以下のサイトを参考にさせていただきました。

いくつかの説明変数 Xn に対してそれに対応する目的変数 y が分かっているとき、 y=f(X) となる関数 f を発見することを「教師あり機械学習」と言います。その中で最も簡単なのが「線形単回帰」や「線形重回帰」です。

教師あり機械学習(分類・回帰)

6. データ予測と提出ファイル作成

モデル構築が完了したら、テストデータと構築したモデルを用いて、テストデータの目的変数(住宅価格)を予測します。

""" 4) データ予測 """
regression = gbc
test_raw['y_pred'] = regression.predict(x_test_reg)
test_raw['y_pred'] = test_raw['y_pred'].apply(lambda x: np.exp(x)).round()

これでテストデータの説明変数でテストデータの目的変数を予測できました。

最後にコンペ提出用データを作成します。これで’data’フォルダー下に提出用のcsvファイルが作成されていると思います。

""" 5) 提出データ加工 """
test_result = test_raw[['index', 'y_pred']]
test_result.columns = ['0', '1']
test_result.to_csv('./data/submit_YYMMDD_revXX.csv', index=False)

7. 所感

本コンペでデータ解析の流れと様々なデータ解析の初歩を学ぶことができた。

やはり、実践に近いデータを扱うと学習スピードが上がると思うので、これを気にKaggleにも挑戦したいですね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT US

Baran-gizagiza
経歴:浪人→理系大学院卒業→大手製造業に就職(技術職)→アメリカ赴任中 仕事は、研究・設計など上流工程の仕事に携わっています。企業勤務を継続しながら、新しいことにチャレンジしたいと思い、ブログを下記はじめました。 このブログでは、趣味である 筋トレ(健康、ダイエット) AIとデータ(高校数学、プログラミング) 読書(主に自己啓発系) を中心に、人生経験やおすすめ情報の発信しています。