0. AIでお得な中古マンションをあぶり出す その1
これまでやってみた分析までの流れを整理してみます。
- スクレイピング
- suumoから販売中の中古マンションの情報を取得しました。
- 前処理
- 取得したマンション情報を、分析しやすいように加工などしました。
- 分析その1
- 取得したデータを可視化して、データの特性を把握してみました。
- 分析その2
- 物件の位置情報に注目して、データ可視化をしてみました。
- 分析その3
- 市区町村ごとに、物件の特性を可視化してみました。
今回は、これらの情報を基にして説明変数を準備して、機械学習にかけていきます。
1. 機械学習手法
はっきり言って、今回は採用する手法の選択肢はほとんどありませんでした。
というのも、めちゃめちゃ精度を上げるのに頑張ります!という気力もないので、楽に使えて適度に精度が出るものというと。。。。あまり残りません。
で、選択肢は以下の3つ。
手法 | メリット | デメリット |
---|---|---|
Random Forest | 王道ですよね。使ってみて間違いない。scikit-learnに入っているし、環境構築も不要 | 古典とまでいえないものの、他の2つの手法に比べて速度や精度が(一般的に)劣る |
Gradient boosting | Random Forestより、高精度で高速とkaggleでもっぱらの噂 | 使用のために環境構築が必要 |
LightGBM | Gradient boostingよりもさらに高速で高精度ともっぱらの噂 | 使用のために環境構築が必要。特にGPUを使おうとするとちょっと面倒 |
で、結局LightGBM
を使用することにしました。
理由・・・・?そんなん、使ってみたかったからです(キリッ
2. LightGBM使用のポイント
個人的に考えるポイントは以下でした。
- scikit-learn風のinterfaceを備えている
- 学習時のGPU使用/不使用を選択しやすい
- パラメタに指定すれば、パラメタの修正だけでRandom Forestも使用できる
- カテゴリ変数(Categorical Features)をone-hot表現にしなくても良い
- ここによると、performanceも良いらしい。メモリも節約できるらしい。
- 行列のサイズが小さくなるしね!
- これは結構大きなポイント!
- カテゴリ変数のサイズが大きくなると、GPUが使えない
- でもCPUで学習すればいけた
3. LightGBMの準備
3.1 LightGBM使用のポイント
前処理済みのデータを読み込んで、カテゴリ変数に対する処理部分だけを以下に示します。
ポイントは以下の通りです。
- DataFrameの型をcategoryにする32
- データの中身はint32型とする
- 後で処理が楽なように、カラム名のprefixに
cat_
を付けておく
3.2 LightGBM使用のポイント
こんな感じで、LightGBMに食わせるデータを作ります。
#必要なライブラリをインポート
import pandas as pd
import numpy as np
import os
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import mean_squared_error
import json,codecs
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)
import pickle
def readpkl(path):
with open(path, 'rb') as f2:
return pickle.load(f2)
# データ読み込み
suumo_df = readpkl('data'+os.sep+'suumo_df.pkl')
# カテゴリ変数とするカラムをint型へ変換し、DataFrameの型をcategory型とする
suumo_df.fillna("-",inplace=True)
ccol = ["構造","向き","管理タイプ","管理形態","管理費サイクル","敷地権利","駐車場場所","交通0_交通手段","交通1_交通手段","交通2_交通手段","市区町村","交通0_最寄駅"]
cdict = {}
for d in ccol:
cdict[d] = suumo_df[d].value_counts().reset_index()["index"].to_dict()
revdic = { v:k for k,v in cdict[d].items() }
suumo_df["cat_{}".format(d)] = suumo_df[d].apply(lambda x: revdic[x])
suumo_df["cat_{}".format(d)] = suumo_df["cat_{}".format(d)].astype('category')
# "cat_を付ける前のカラムを削除"
suumo_df.drop(ccol, axis=1, inplace=True)
# カテゴリ変数の変換をファイル保存
json.dump(cdict, codecs.open("{}_categorical.json".format("カテゴリ変数"), 'w', encoding='utf-8'), separators=(',', ':'), sort_keys=True, indent=2, cls=NumpyEncoder, ensure_ascii=False, encoding='utf8')
# カテゴリ変数カラムnのlist
categorical_features = [c for c, col in enumerate(suumo_df.columns) if 'cat' in col]
#################################################
# 説明変数と目的変数に分離
X, y = suumo_df.drop('価格', axis=1), suumo_df["価格"]
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error
# 学習用と評価用にデータを分割
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=908 )
# create dataset for lightgbm
lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=categorical_features)
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
作ってみたデータを見てみると、以下のような感じです。
In [80]: X_train[0:5]
Out[80]:
専有面積 所在階 総戸数 ... cat_交通2_交通手段 cat_市区町村 cat_交通0_最寄駅
4629 98.73 1 -1 ... 1 17 85
10843 81.13 4 73 ... 0 14 155
2783 41.32 2 126 ... 0 27 48
3663 69.55 3 101 ... 2 2 2
841 65.48 5 53 ... 0 32 20
カテゴリ変数がint32型に変換されているのがわかりますね。
3.3 学習データと評価用データ
上記codeを見てもらえればわかりますが、データは学習用と評価用データに分けています。
ポイントは、学習データとして 使用していないデータを用いて評価する
という点です。
上記のcodeでは、図中の「Test」にあたるデータとして、全体の20%をランダムに抽出しました。
残る80%を学習データとして学習し、この学習結果を評価用データで評価することにします。
なお、後述しますが「validation」のために、Trainデータを10分割した交差検証を行うことで、汎化性能を担保するようにしています。
3.4 交差検証
最適な学習パラメータを探るためにGridSearchを行います。
この時、一緒に交差検証も実施することで、意味のあるパラメタを選択しようと思います。
以下、gridsearch部分のcodeです。
gridParams = {
'n_estimators': [100, 250, 300, 500],
'num_leaves': [64, 96, 128, 256],
'max_depth': [20, 30, 40],
'subsample': [0.9, 0.95],
'colsample_bytree': [0.8, 1.0],
}
gridsearch = GridSearchCV(
estimator = lgb.LGBMRegressor(
boosting_type='gbdt',
objective = "regression",
metric='rmse',
learning_rate=0.04,
random_state=614,
silent=True,
device='gpu',
),
param_grid = gridParams,
verbose=1,
n_jobs=-1,
cv=10,
refit =True
)
最後から2行目、 cv=10
と指定している部分が、交差検証を10分割して行うためのパラメタ指定です。lightgbmのパラメタは、 gridParams
で指定しています。この範囲でパラメタの検索を行います。
3.5 交差検証とgrid searchの効率化
パラメタサーチの時は、最適なパラメタを探すことが目的なので、cvの値は小さめに2などを指定しておくといいと思います。パラメタを絞り込めたら、その時はcv=10などとして、汎化性能を担保したモデルを作るようにすると、計算時間が大幅に節約できます。
4. 結果
さぁ、さっそく学習結果を紹介します。
おお、これはいい感じで予測できていると思いませんか?
この予測した価格が、販売価格より低ければ、それはお買い得
ということになる(?)はずです。
lightgbmは、random forestと同様、説明変数の重要度を求めることができます。
こちらも確認してみましょう。
ほほう、、、、
専有面積、築年数がかなり価格を左右する要素のようです。
意外にも、総戸数や階割合(最上階=1)がかなり効いています。
おそらくこれは、今回使用した乱数のseedが、たまたま高層マンションを取り入れたからなのでは?と思っています。それはぞれで一つの真実ですので、そういうこともあるだろうと思います。
マンションは立地がすべて
、とか最重要
といわれることも多いです。が、価格を決めるファクターとしては、専有面積のほうがはるかに大きいようです。そうですよね。所有権の土地面積と直結するので、間違いなく重要なファクターではあるんです。
これを最寄り駅ごとにモデルにしたら、また様子が違うのでしょうね。
大変だしデータ数がそろわないし、なによりそれを説明変数に入れているのだから、やらなくてもいいですよね。
4. 終わりに
次回は、実際に予測された物件の価格のうち、お得度が高い物件に注目していきたいと思います。
その後、国交省で公開されているデータ(例えばe-Stat)などを使って、何かやりたいと思っています。