テストエンジニアが機械学習してみた備忘録

広島とジト目が好きなテストエンジニアが機械学習に手を出した備忘録。

「機械学習を使って東京23区のお買い得賃貸物件を探してみた」を京都駅周辺でやってみる 〜苦悩編〜

前回前々回の続きで、 以下の記事を参考にいよいよ予測モデル作ったりしようとしました。

www.analyze-world.com

…が、何やかんやでデータ収集からやり直すことにしました。

実行環境

  • Mac OS High Sierra (10.13.1)
  • Python 3.6.2
    • Pandas 0.20.3
    • Numpy 1.13.1
    • matplotlib 2.0.2
    • scikit-learn 0.19.1

データ収集

今回は「最寄駅から京都駅まで30分」を重視していたのですが、 最初にそこをあまりケアせずにデータ収集して前処理でどうにかしようとした結果、 膨大なデータから「最寄駅」を上手く扱うことに苦戦したため、改めてデータを集める時点でケアします。 (データ収集の時点でミスるとリカバリー効かないことが身に染みました…)

収集条件

「最寄駅から京都駅まで30分」という条件は据え置きで、乗り換え1回という条件を加味して路線図から駅を絞り込んで検索します。

各物件のデータには最寄駅の情報が最大3つまで入っていますが、 上記の路線に該当しないデータは取り除いた上で、最も近い距離の駅をその物件の「京都駅まで1番早く行ける最寄駅」とします。 もしかすると駅までの時間が長い方が、電車込みで考えると短時間で到着するというものもあるかもしれませんが、 そこまで選り分けるのは難しいため許容します。

その他にも単身者であることと、個人的な価値観を考慮して以下の条件を付与しています。

  • 家賃
    • 4万〜10万
  • 最寄駅
    • 徒歩15分以内
  • 間取り
  • 築年数
    • 15年以内

前処理を経て収集したデータは以下のようになりました(約12000件)。 上から順に「物件名」「築年数」「建物の階数」「部屋の階」「家賃+共益費」「占有面積(平米)」「初期費用(敷金礼金等)」「市区」「市区に続く住所」「キッチン(K)の有無」「ダイニング(D)の有無」「リビング(L)の有無」「納屋(S)の有無」「部屋数」「最寄駅の路線」「最寄駅」「最寄駅までの徒歩時間(分)」です。kitchen, dining, living, storeroomはダミー変数としています。

name         SkyGrace吉祥院
age                    2
height                 2
floor                  1
rent               56000
area               21.44
initial                0
address_1              南
address_2        吉祥院這登東町
kitchen                1
dining                 0
living                 0
storeroom              0
room_num               1
line             JR東海道本線
station             西大路駅
walk                  15

この部屋は「西大路駅徒歩15分にある、家賃+共益費が5.6万円の、築2年の1K」となります。

pandasによるダミー変数の生成

所在地も考慮したいのでaddress_1かstationもダミー変数化したいと思います。 stationの方が細かく指定できますが、100個近くあって次元数が多くなってしまいそうなので、今回は25個に収まるaddress_1を選択しました。 pandasにはget_dummiesなるダミー変数生成のメソッドが用意されているのでそれを利用します。

pd.get_dummies(df[['address_1']])

データ探索

一応、前回と同様にデータを見ておきます。

地域毎の物件数

f:id:gratk:20171129234918p:plain

京都に絞らなくなったので高槻とか大津とか府外も入ってきていますが、 京都府内については前回と同じような並びになりました。

地域毎の家賃+共益費(箱ひげ図)

f:id:gratk:20171129234925p:plain

さすが高槻。

地域毎の家賃+共益費(ヒストグラム

f:id:gratk:20171129235147p:plain

中央値の最も高い高槻、真ん中くらいの南区、最も低い亀岡で描いてみました。 中央値が高い方が山が右にあり、分散も大きいという傾向は同じようです。

部屋数と家賃(散布図)

f:id:gratk:20171129235154p:plain

相変わらず部屋数の換算が雑なままですが、一応回帰線を見ると部屋数に応じて家賃があがるようです。 部屋数3、4、5の少し集団から外れている物件を少し見てみると、基本的には古かったり京都市内から離れてたりですが、 以下のような「なんかいいんじゃね?」な物件もチラホラ。

suumo.jp

suumo.jp

重回帰分析

説明変数の選択

というわけでようやく本題の分析です。まずは重回帰ということで、散布図と相関係数を見ていきます。

f:id:gratk:20171202152033p:plain

f:id:gratk:20171202153116p:plain

height(建物の最大階数)が30を超えていていわゆる外れ値っぽくなっているデータがあります。 floor(物件の階数)との間の相関係数も高いので、ここはheightは説明変数の候補から外します。 あとは以下の相関係数も高いです。

  • リビングの有無と占有面積
  • ダイニングの有無と占有面積
  • リビングの有無とダイニングの有無

また、キッチンの有無、納屋の有無、部屋数は家賃との相関係数が他よりも一桁低いです。 あまり特徴量の次元を増やすのも良くないと聞きますし、これらは占有面積で全て包含できそうな気がするため、 ここは占有面積のみを説明変数として採用してみようと思います。

学習データとテストデータ

単純にデータを2つにわけるのが手っ取り早い(ホールドアウト法)ですが、はじめてのパターン認識を読むとデータが少ない時はイマイチとのことなので、交差確認法(cross validation)を今回は使ってみようと思います。

scikit-learnにおいては0.18系までの使用方法は0.20系で廃止されるとのこと。

scikit-learn 0.20からクロスバリデーションの使い方が変更される模様 - verilog書く人

今回は0.19系を利用しているので変更後の方法で使っていきます。

sklearn.model_selection.KFold — scikit-learn 0.19.2 documentation

モデル作成

sklearn.linear_model.LinearRegression — scikit-learn 0.19.2 documentation

scikit-learn で線形回帰 (単回帰分析・重回帰分析) – Python でデータサイエンス

上記を参考に、データを4つに分割してモデル生成と性能評価をしてみます。 評価指標は機械学習を使って東京23区のお買い得賃貸物件を探してみた - データで見る世界と同じく残差平方和を用います。

# Xに説明変数のDataFrame、Yに応答変数のDataFrameを読み込み済み
from sklearn import linear_model
from sklearn.model_selection import KFold

kf = KFold(n_splits=5)
kf.get_n_splits(X)
lm = linear_model.LinearRegression(n_jobs=-1)

columns = [x for x in X.columns]
columns.append('切片')
columns.append('決定係数')
columns.append('残差平方和')
results = pd.DataFrame(columns=columns)
rss = 0

for train, test in kf.split(X):
    # 教師データとテストデータをNumpy行列に変換
    trainX = X.loc[train].as_matrix()
    trainY = Y.loc[train].as_matrix()
    testX = X.loc[test].as_matrix()
    testY = Y.loc[test].as_matrix()

    # モデル生成
    lm.fit(trainX, trainY)
    
    # 偏回帰係数、切片、決定係数
    params = lm.coef_
    params = np.append(params, lm.intercept_)
    params = np.append(params, lm.score(trainX, trainY))

    # 検証
    test = pd.DataFrame(
        {
            "predict": lm.predict(testX),
            "answer": testY
        }
    )
    
    # 残差平方和
    params = np.append(params, pow(test.predict - test.answer, 2).sum())
    
    results = pd.concat([results, pd.DataFrame([params], columns=columns)])

for i in results.mean().index:
    print(f"{i} {results.mean().round(2)[i]}")

上記コードで出した平均は以下のようになりました。

age -480.62
floor 895.62
area 832.13
initial 0.02
walk -285.68
三島 -2599.81
上京 6865.39
下京 6750.1
中京 7637.05
乙訓 1251.15
亀岡 -5806.1
京田辺 -3012.07
伏見 -1260.99
北 5198.01
南 1424.04
右京 1181.56
向日 -2303.99
城陽 -4640.63
大津 -5972.81
宇治 -1954.68
守山 -8132.28
山科 -44.27
左京 4995.72
東山 8294.45
栗東 -7700.59
神戸 -4148.87
草津 -4721.87
西京 2654.13
長岡京 -1299.4
高槻 7346.77
切片 42805.88
決定係数 0.68
残差平方和 103729802188.07
  • 築年数と最寄駅までの徒歩時間が増えると家賃が下がる
  • 部屋の階数と広さが増えると家賃が増える
  • 初期費用(敷金・礼金とか)は家賃にあまり影響しない

といったことが読み取れます。立地による影響も含めて直感には合致してそうです。

f:id:gratk:20171202211545p:plain

…と思いきや、最後に分割したデータで作成したモデルを用いてテストデータの予測をしたものをプロットしたところ、 ブレが大きくて全然ダメでした。また、家賃が高くなる部分は全然予測できていません。

ランダムフォレスト

ですが、機械学習を使って東京23区のお買い得賃貸物件を探してみた - データで見る世界でも重回帰分析ではダメという結論が出ていましたし、ここまでは予定調和です。やってみることが大事なんです。というわけで次はランダムフォレストしてみます。

パラメータが色々あるみたいですがとりあえずデフォルトで行きます。

ランダムフォレスト

f:id:gratk:20171202213558p:plain

重回帰分析(さっきの結果)

f:id:gratk:20171202211545p:plain

うーん…大差ない気がする…残差平方和もそんなに変わらないです。

f:id:gratk:20171202213726p:plain

やっぱりパラメータチューニングとか必要なのだろうか。ひとまず今回はここまでで、次回ランダムフォレストについてもう少し学んで掘り下げてみようと思います。