見出し画像

Nishikaコンペの練習1:中古マンション価格予測 2022夏の部

1.概要

 Nishikaというコンペサイトを見つけたので簡単なコンペに参加しました。

 とりあえずSIGNATEでやったことと同じ感じで処理してみました。

 参考までにPCスペックは下記の通りです。

2.事前準備:コンペサイト

2-1.データ概要の確認

 データは一般的(専門知識が不要)な情報からマンション価格を推定します。評価指標はMAEのため一般的な回帰での指標となります。

$$
MAE(Mean Ablolute Error)=\frac{1}{n} \sum_{i=1}^{n}|\hat{y}_{i} - y_{i}| =\frac{1}{データ数} \sum_{i=1}^{n}|予測値−正解値| 
$$

 データ内の「data_explanation.xlsx」からデータ概要を確認できます。

2-2.学習・評価用データのダウンロード

 学習・評価用データを下記からダウンロードします。trainフォルダの中に複数のcsvがあり、提出用データである「test.csv」は同フォルダに一つであることが確認できました。

 自分が処理しやすいようにフォルダ構成は下記に変更しました。

3.事前準備:Google Colab側

 PyCaretを使用するためGoogle Colabを使用します。

3-1.APIトークンの配置/CLI準備

 特に見当たらないので不要

3-2.必要なファイルの配置

 contentフォルダ(作業dir)にDLしたファイルをアップロードします。 

3-3.必要ライブラリのインポート

 google colabではPythonを使用するための基本的な環境構築(ライブラリ・Version管理)はされておりますが、入っていないライブラリは自分でインポートします。
 今回はPyCaretに必要な環境構築を実行します。

[IN]
#必要なライブラリのインストール
!pip install pandas-profiling==3.1.0
!pip install pycaret
!pip install shap

#Google Colabでのインタラクティブ化
from pycaret.utils import enable_colab
enable_colab()

3-4.記事紹介用:Pandasのdf並列表示

 記事用+見やすさのためDataFrameを並列表示できる関数を作成します。

[IN]
from IPython.display import display_html
def display_dfs(dfs, gap=50, justify='center'):
    html = ""
    for title, df in dfs.items():  
        df_html = df._repr_html_()
        cur_html = f'<div> <h3>{title}</h3> {df_html}</div>'
        html +=  cur_html
    html= f"""
    <div style="display:flex; gap:{gap}px; justify-content:{justify};">
        {html}
    </div>
    """
    display_html(html, raw=True)

4.データ解析・前処理

 Scikit-Learnの記事で紹介した通り、機械学習でのモデル訓練の大まかな流れは下記の通りです。まず初めに下図のデータ構成の部分を実施します。

【訓練の流れ】
1.データセットの準備
2.AIモデルの選定
3.目的関数(損失関数)の選定
4.最適化手法の選定
5.モデルの学習

機械学習品質マネジメントガイドライン 第2版

4-1.データ確認

 データの中身に関しては最低でも下記を確認します。


【データ概要の確認事項】

①統計量
②欠損値の有無
③不要そうなカラムの有無
④ラベルデータの偏り ※カテゴリデータではないため統計量から確認

[IN]  
import pandas as pd 
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import glob


#pandasの表示件数を設定
pd.set_option('display.max_columns', 30)

#学習データ(train)の読み込み・結合
train_files = glob.glob('data/train/*.csv')
df_train = None #学習データ

for file in train_files:
    df = pd.read_csv(file)
    if df_train is None: #df_trainが初期化されていない場合
        df_train = df
    else:
        df_train = pd.concat([df_train, df], axis=0) #axis=0は行結合

#学習・テスト・サンプルデータの読み込み
# df_train = pd.read_csv() #上記で処理済み
df_test = pd.read_csv('data/test.csv')
df_sample = pd.read_csv('data/sample_submission.csv') #カラムありのためheader=Noneは無し

print('df_train',df_train.shape, 'df_test', df_test.shape, 'df_sample', df_sample.shape)
display(df_train.head(5)) #提出データの形確認

# #データの中身確認
display(df_train.describe()) 
display(df_test.describe())

display_dfs( {'欠損値_train':pd.DataFrame(df_train.isnull().sum()), 
              '欠損値_test': pd.DataFrame(df_test.isnull().sum())}
            , justify='flex-start')

[OUT]
df_train (722574, 28) df_test (21005, 27) df_sample (21005, 2)

4-2.欠損値の確認

 本データセットは欠損値が多いため前処理が必要となります。方向性としては下記があります。

【欠損値処理対応の方向性】
1.欠損値が多いカラムはすべて除去して、欠損値が比較的少ないデータは補完する
2.欠損値があるカラムはすべて削除
3.PyCaretにほぼ丸投げ

 個人的な感覚的としてマンション価格で重要な要因は「都市、場所(駅・エリア)、築年数、構造、面積」だと思います。よって欠損値があるけど「建築年」は外せないためある程度補完は必要になります。
 時系列補完ならPandasの欠損値処理でひとつ前のデータや前後の平均値で処理しやすいですが今回はランダム配置のため、最頻値や中央値での処理になると思います。
 とりあえず簡単にできる方法として「欠損値が多い不要そうなカラムは削除して、あとはPyCaretに丸投げ」で実施してみます。

5.CASE1:シンプルなPyCaretで処理

 まずはPyCaretをシンプルに使用してどの程度性能が出るか確認しました。

5-1.前処理1:簡易実行

 不要なカラムを除去して前処理を実行するとエラーが出ました。理由としてカテゴリカルデータをOne hot Encodingすると大量の変数(16860列)ができてデータ数が爆発的に増えたためメモリに乗らないことが原因です。

 私のPCスペックで実行すると次元数(学習データのカラム数)が674個だと学習はできるけどハイパーパラメータ調整でメモリが足りなくなりました。
 データセット内のカテゴリカルデータのユニーク数を下記で確認できます。結果として、地区名や市区町村コードなどはデータをグルーピングしないと(私のPCのメモリでは)使用できません。

[IN]
def unique_count(df_train, df_test):
    columns = df_train.columns
    datas = []
    for column in columns:
        if column == '取引価格(総額)_log':
            datas.append([column, df_train[column].dtype, len(df_train[column].unique()), 0])
        else:  
            datas.append([column, df_train[column].dtype, len(df_train[column].unique()), len(df_test[column].unique())])      
    
    df_unique = pd.DataFrame(datas, columns=['カラム名', 'データ型', 'unique_train', 'unique_test'])        
    return df_unique

unique_count(df_train, df_test)

[OUT]

5-1’.前処理2:カテゴリカルー>数値変換

 データを確認すると ['最寄駅:距離(分)', '面積(㎡)', '建ぺい率(%)', '容積率(%)'] は数値データでもよさそうですが、前処理ではカテゴリカルで認識されています。よってデータの詳細をpd.unique()で確認しました。

[IN]
df_master = pd.concat([df_train, df_test], axis=0) #学習・テストデータの両方とも確認するため結合
print(df_master['建築年'].unique())
print(df_master['最寄駅:距離(分)'].unique())
print(df_master['面積(㎡)'].unique())
print(df_master['建ぺい率(%)'].unique())
print(df_master['容積率(%)'].unique())
[OUT]
#建築年
['昭和49年' '平成7年' '平成6年' '平成15年' '昭和64年' '昭和52年' '平成20年' '平成9年' '平成19年'
 '平成13年' '平成8年' nan '平成27年' '平成10年' '昭和54年' '昭和62年' '平成30年' '平成25年' '平成3年'
 '昭和58年' '平成2年' '昭和57年' '平成17年' '平成16年' '平成18年' '平成5年' '平成11年' '昭和48年'
 '平成4年' '昭和63年' '平成14年' '昭和56年' '昭和60年' '平成21年' '平成24年' '平成28年' '昭和61年'
 '平成12年' '昭和53年' '昭和59年' '昭和55年' '昭和51年' '昭和50年' '平成22年' '平成29年' '昭和46年'
 '昭和43年' '昭和47年' '平成23年' '昭和45年' '平成26年' '昭和44年' '平成31年' '戦前' '令和2年'
 '昭和39年' '令和3年' '昭和41年' '昭和42年' '昭和40年' '昭和22年' '昭和38年' '昭和21年' '昭和27年'
 '昭和35年' '令和4年' '昭和31年' '昭和37年' '昭和28年' '昭和36年' '昭和34年' '昭和25年' '昭和33年'
 '昭和29年' '昭和24年' '昭和30年' '昭和26年' '昭和32年' '昭和23年']

#最寄駅:距離(分)
['5' '10' '4' '3' '6' '9' '8' '18' '30分?60分' '1' '13' '2' '23' '2H?' nan
 '29' '7' '11' '15' '12' '19' '20' '16' '25' '0' '14' '28' '24' '27'
 '1H?1H30' '21' '17' '26' '22' '1H30?2H']

#面積(㎡)
[85 70 20 60 55 90 75 95 115 65 105 50 45 210 25 80 100 260 35 30 110 15
 40 135 130 125 120 170 150 145 155 140 160 220 190 10 165 185 180 360 250
 710 200 '40' '15' '80' '30' '100' '55' '85' '70' '50' '35' '65' '75' '45'
 '20' '25' '60' '300' '90' '145' '95' '110' '130' '120' '125' '2000㎡以上'
 '115' '105' '140' 530 450 490 470 300 600 970 440 390 175 740 460 195 270
 290 420 240 320 1000 230 580 330 860 310 500 '1000' '10' '150' '135'
 '340' '630' '1300' '210' '250' '165' '290' '180' '690' '195' '280' '160'
 '155' '170' '200' '220' '190' '175' '330' '185' '450' '260' '440' '310'
 '500' '240' '590' '370' '1500' '380' 990 410 350 720 280 890 380 480 560
 630 620 1500 400 340 870 820 370 430 680 1200 770 730 520 700 1400 940
 590 780 850 900 550 660 1100 960 540 690 650 760 670 790 610 '780' '650'
 '410' '660' '620' '320' '230' '760' '830' '670' '270' '460' 800 950 510
 '520' '360' '560' '730' '390' '400' '480' '740' '470' '980' '710' '820'
 570 880]

#建ぺい率(%)
[80. 60. nan 40. 50. 70. 30.]

#容積率(%)
[ 400.  200.  300.  600.   nan  800.  500.   80.  150.  700.  100.   60.
  900. 1000. 1300. 1200.   50. 1100.]

 結果よりそれぞれのカラムでカテゴリカルと認識される不要なデータが存在しました。

【不要なデータ】
●建築年:そもそも和暦、要注意なのは”戦前”と"nan"
●最寄駅:距離(分):'30分?60分' , '2H?', nan, '1H?1H30' , '1H30?2H'
●面積(㎡):'2000㎡以上', 
●建ぺい率(%):nan
●容積率(%):nan

 各カラムで①文字列は適当な数値に変換、②出力はfloatで処理しました。

[IN]
def convertdf_traintest(df_train, df_test, column, func):
    df_train[column] = df_train[column].apply(func)
    df_test[column] = df_test[column].apply(func)
    return df_train, df_test

#カテゴリカルデータ変換:面積(㎡)
def tofloat_area(x):
    if x=='2000㎡以上':
        return 2000
    else:
        return float(x)

def tofloat_distance(x):
    if x=='30分?60分':
        return 45
    elif x=='2H?':
        return 120
    elif x=='1H?1H30':
        return 75
    elif x=='1H30?2H':
        return 105
    else:
        return float(x)

def tofloat(x):
    return float(x)      

df_train, df_test = convertdf_traintest(df_train, df_test, column='面積(㎡)', func=tofloat_area)
df_train, df_test = convertdf_traintest(df_train, df_test, column='最寄駅:距離(分)', func=tofloat_distance)
df_train, df_test = convertdf_traintest(df_train, df_test, column='建ぺい率(%)', func=tofloat)
df_train, df_test = convertdf_traintest(df_train, df_test, column='容積率(%)', func=tofloat)

[OUT]
nanを除く、文字列はすべて数値化

 なお”建築年”の和暦は"jeraconv"を使用して西暦に変換しました。使用方法は簡単のため記事をご確認ください。なお”戦前”という値が紛れ込んでおりjeraconvでは変換できないためif文で処理しました。

[IN]
from jeraconv import jeraconv
j2w = jeraconv.J2W() # J2W クラスのインスタンス生成ー>JSON データが load される

def j2w_convert(wareki):
    if wareki=='戦前':
        return 1868 #戦前を1868年(明治元年)と仮定
    elif type(wareki)!=str:
        return wareki #欠損値はそのまま
    else:
        seireki = j2w.convert(wareki)
        return seireki

df_train, df_test = convertdf_traintest(df_train, df_test, column='建築年', func=j2w_convert)

#データ確認用
df_master = pd.concat([df_train, df_test], axis=0) #学習・テストデータの両方とも確認するため結合
print(df_master['建築年'].unique())
print('df_train',df_train.shape, 'df_test', df_test.shape, 'df_sample', df_sample.shape)

[OUT]
[1974. 1995. 1994. 2003. 1989. 1977. 2008. 1997. 2007. 2001. 1996.   nan
 2015. 1998. 1979. 1987. 2018. 2013. 1991. 1983. 1990. 1982. 2005. 2004.
 2006. 1993. 1999. 1973. 1992. 1988. 2002. 1981. 1985. 2009. 2012. 2016.
 1986. 2000. 1978. 1984. 1980. 1976. 1975. 2010. 2017. 1971. 1968. 1972.
 2011. 1970. 2014. 1969. 2019. 1868. 2020. 1964. 2021. 1966. 1967. 1965.
 1947. 1963. 1946. 1952. 1960. 2022. 1956. 1962. 1953. 1961. 1959. 1950.
 1958. 1954. 1949. 1955. 1951. 1957. 1948.]

df_train (722574, 28) df_test (21005, 27) df_sample (21005, 2)

5-1’’.前処理(本番):setup()

 それでは実際にPyCaretで前処理を実施しますが注意点は下記の通りです。

【注意点】
●ほしい情報をそのままカラムに入れると"Memory Error"になるため、一部のカラムは機械学習に使用しませんでした。
 ー>真面目にやるならデータの中身を確認してグループ化することで情報量を圧縮可能(最寄り駅を"市街地", "田舎"に分類など)
●数値/カテゴリカルデータとして認識されないカラムは引数で調整

[IN ※全カラム− ほしいカラム=不要なカラムリスト作成]
cols = ['都道府県名',
 '最寄駅:距離(分)', #flaot
 '間取り',
 '面積(㎡)', #flaot
 '建築年', #flaot
 '建物の構造',
 '建ぺい率(%)', #flaot
 '容積率(%)', #flaot
 '改装',
 '取引価格(総額)_log']

dropcols = list(df_train.columns)
for col in cols:
    if col in dropcols:
        dropcols.remove(col) #dropcolsにcolが含まれていたら削除

print(dropcols)

[OUT]
['ID', '種類', '地域', '市区町村コード', '市区町村名', '地区名', '最寄駅:名称', '土地の形状', '間口', '延床面積(㎡)', '用途', '今後の利用目的', '前面道路:方位', '前面道路:種類', '前面道路:幅員(m)', '都市計画', '取引時点', '取引の事情等']
[IN]
from pycaret.regression import * #回帰:regression

#データの前処理
exp = setup(df_train, 
            ignore_features=dropcols, 
            use_gpu = True,
            silent=False, #手動で出力を表示
            numeric_features= ['建ぺい率(%)', '容積率(%)'],
            # fix_imbalance = True, #偏りは小さいため不要
            target = '取引価格(総額)_log')

[OUT]
session_id=5806

5-2.学習・モデル比較:compare_models()

 各モデルの学習および性能比較を実施します。学習時は「RandomForestRegressor」がベストスコアとなりました。

[IN]
best = compare_models() #モデルの比較
print(best)
print(type(best))

[OUT]
※参考:処理時間=109min

RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse',
                      max_depth=None, max_features='auto', max_leaf_nodes=None,
                      max_samples=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      n_estimators=100, n_jobs=-1, oob_score=False,
                      random_state=5806, verbose=0, warm_start=False)
<class 'sklearn.ensemble._forest.RandomForestRegressor'>

5-3.モデルの選択/ハイパーパラメーター調整

 compare_models()時点では過学習やハイパーパラメータ―未調整により性能が出ないモデルなどの可能性があるため上位モデルを何個か試します。
 選択時は多様性を考慮して別種のモデル(例:LightGBMを選択するならXGBoostは不要など)を選択します。
※今回のトライアルではtuned時にエラーが出たためハイパラ調整していないモデルで実行

[IN]
rf = create_model('rf')
lightgbm = create_model('lightgbm')
knn = create_model('knn')
linear = create_model('lr')
ridge = create_model('ridge')


#ハイパーパラメータ調整
tuned_rf, tuner_rf = tune_model(rf,
                                      optimize ='MAE',
                                      return_tuner=True,
                                      choose_better = True) 
tuned_lightgbm, tuner_lightgbm = tune_model(lightgbm, 
                                              optimize ='MAE', 
                                              return_tuner=True,
                                              choose_better = True) 
tuned_knn, tuner_knn = tune_model(knn, 
                                    optimize ='MAE', 
                                    return_tuner=True,
                                    choose_better = True) 
tuned_linear, tuner_linear = tune_model(linear,
                                            optimize ='MAE', 
                                            return_tuner=True,
                                            choose_better = True) 
tuned_ridge, tuner_ridge = tune_model(ridge,
                                  optimize ='MAE', 
                                  return_tuner=True,
                                  choose_better = True) 

5-4.モデルの評価/結果の可視化

 モデルの評価/結果の可視化を実施します。自分が詳しいモデルならハイパーパラメータの確認は重要ですが、よくわからない場合は最低でも①AUC:性能、②混同行列:精度、③学習カーブ:過学習は確認します。

[IN]

 可視化はplot_modelで実施します

[IN]

5-5.テストデータの推論

 predict_modelにテストデータを渡してデータを推論します。また提出用CSVの形にするため1.必要カラムの抽出、2.カラム名変更 しました。

[IN]
column_get = ['ID', 'Label']
column_submit = ['ID', '取引価格(総額)_log']

pred_rf = predict_model(rf, data=df_test)
df_output = pred_rf[column_get]
df_output.columns = column_submit
df_output.to_csv('220718_output_rf.csv', header=True, index=False)

pred_lightgbm = predict_model(lightgbm, data=df_test)
df_output = pred_lightgbm[column_get]
df_output.columns = column_submit
df_output.to_csv('220718_output_lightgbm.csv', header=True, index=False)

pred_knn = predict_model(knn, data=df_test)
df_output = pred_knn[column_get]
df_output.columns = column_submit
df_output.to_csv('220718_output_knn.csv', header=True, index=False)

pred_linear = predict_model(linear, data=df_test)
df_output = pred_linear[column_get]
df_output.columns = column_submit
df_output.to_csv('220718_output_linear.csv', header=True, index=False)

pred_ridge = predict_model(ridge, data=df_test) 
df_output = pred_ridge[column_get]
df_output.columns = column_submit
df_output.to_csv('220718_output_ridge.csv', header=True, index=False)


[OUT]
csv作成

5-6.結果の提出

 結果(CSVファイル)をブラウザから投稿します。

5-7.提出結果の確認

 実際のスコア(テストデータでの予想)は下記の通りです。学習時と比較的同等スコアがでているため、学習とテストデータに大きな差はなさそうです。より精度を上げるためには特徴量エンジニアリングの方が重要になってきます。

6.最終結果

 AutoMLだけだと全然足りないですね・・・・


参考資料

あとがき

 コンペに力入れる時間がない・・・・・

 

この記事が気に入ったらサポートをしてみませんか?