このチュートリアルでは不動産取引データに対して
- データの読み込み
- データの確認と前処理
- 重回帰モデルの作成、学習
- モデルの評価
- テストデータに対する出力
を行います。
環境
- python 3.6.8
- numpy 1.17.3
- pandas 0.25.2
- matplotlib 3.1.1
- sklearn 0.21.3
データの読み込み
まずはデータを読み込んで見ましょう。csvデータの読み込みは複数のやり方がありえますが、pandas
のread_csv
関数はその中でも機能が豊富で、扱いやすいためこれを使います。
これを使うと、csvデータを読み込み、pandas.DataFrame
にして返してくれます。
import pandas as pd
path = "/content/drive/My Drive/competition/ProbSpace/real_estate/train_data.csv"
train_data = pd.read_csv(path)
また、DataFrame
には.head()
というメソッドが定義されています。このコードを呼び出すとDataFrame
先頭の数行を確認可能です。
train_data.head()
id |
種類 |
地域 |
市区町村コード |
都道府県名 |
市区町村名 |
地区名 |
最寄駅:名称 |
最寄駅:距離(分) |
間取り |
1 |
中古マンション等 |
NaN |
13101 |
東京都 |
千代田区 |
飯田橋 |
飯田橋 |
1 |
2LDK |
2 |
中古マンション等 |
NaN |
13101 |
東京都 |
千代田区 |
飯田橋 |
飯田橋 |
5 |
1K |
3 |
中古マンション等 |
NaN |
13101 |
東京都 |
千代田区 |
飯田橋 |
飯田橋 |
3 |
1LDK |
4 |
中古マンション等 |
NaN |
13101 |
東京都 |
千代田区 |
飯田橋 |
飯田橋 |
5 |
1R |
5 |
宅地(土地と建物) |
NaN |
13101 |
東京都 |
千代田区 |
飯田橋 |
飯田橋 |
3 |
NaN |
データの確認と前処理
訓練データにおける取引金額がどのような分布になっているか見てみましょう。各階級ごとの度数をヒストグラムを用いてプロットするのが以下のコードです。
%matplotlib inline
import matplotlib.pyplot as plt
plt.hist(train_data['y'], bins=20)
ヒストグラムとしては、グラフエリアの左側に1本だけ棒状グラフが出力されました。
ほぼ全てのレコードの取引金額が10,000未満であり、一部の少数のレコードが60,000近い値を取っているために、上記のように表示されました。
極端に他のレコードと値の異なるものは'外れ値'と言われ、回帰モデルに悪影響を及ぼす場合もあります。この'外れ値'をどのように扱うかは場合により異なります。
今回は、四分位法と呼ばれる外れ値の除外手法にもとづき取引金額90以上のレコードを除外しました。
train_data = train_data[train_data["y"] < (90)]
plt.hist(train_data['y'], bins=20)
外れ値を除外したヒストグラムでは20,000,000辺りが最も多く、よ
り低い金額側に偏った分布が見られました。
20,000,000より高価格帯の取引金額は、比較的高い金額まで広範囲に分布していることがわかりました。
続いて、下記のコードを使用して使用するデータ全体の概観を確認します。
train_data.info()
各項目について欠損値以外('non-null'
)の値がどの程度含まれているか把握できます。また、データの形式についてもここで判別可能です。
'地域'
は欠損値が多数含まれていることがわかりました。欠損値が多い列を説明変数として採用すると、作成したモデルの汎用性が低くなる可能性があります。
データの形式を見ると、数値についてはfloat64/int64
型になっていることが確認できます。
不動産の取引金額との関係性が強いと考えられる'最寄駅:距離(分)'
、'建築年'
のデータ形式を見るとobject型となっていました。
回帰モデルの説明変数として扱うためには、数値に変換する必要がありそうです。
'最寄駅:距離(分)'
、'建築年'
について、.value_counts()
を使用して、中に含まれている値とその個数を確認します。
pd.set_option('display.max_rows', 100)
print(train_data['最寄駅:距離(分)'].value_counts())
print(train_data['建築年'].value_counts)
'最寄駅:距離(分)'
では、'30分?60分'
などの数値以外の値が一部含まれていましたので適当な値に置換して数値として処理できるようにします。
train_data['最寄駅:距離(分)'] = train_data['最寄駅:距離(分)'].replace('30分?60分','45')
train_data['最寄駅:距離(分)'] = train_data['最寄駅:距離(分)'].replace('1H?1H30','75')
train_data['最寄駅:距離(分)'] = train_data['最寄駅:距離(分)'].replace('1H30?2H','105')
train_data['最寄駅:距離(分)'] = train_data['最寄駅:距離(分)'].replace('2H?','120')
train_data['最寄駅:距離(分)'] = pd.to_numeric(train_data['最寄駅:距離(分)'], errors='coerce')
続いて、'建築年'
の出力結果についても確認します。
'建築年'
は、'平成''昭和'
などの和暦表記になっている他、'戦前'
という表記も見られました。こちらも適当な西暦4桁に置換します。
train_data['建築年'] = train_data['建築年'].dropna()
train_data['建築年'] = train_data['建築年'].str.replace('戦前','昭和20年')
train_data['年号'] = train_data['建築年'].str[:2]
train_data['和暦年数'] = train_data['建築年'].str[2:].str.strip('年').fillna(0).astype(int)
train_data.loc[train_data['年号']=='昭和','建築年(西暦)'] = train_data['和暦年数'] + 1925
train_data.loc[train_data['年号']=='平成','建築年(西暦)'] = train_data['和暦年数'] + 1988
続いて、数値データの正規化を行い、平均0、標準偏差1になるように変換を行います。
今回は、'容積率(%)'
、'建築年(西暦)'
について正規化をしました。
import numpy as np
l = ['最寄駅:距離(分)','容積率(%)','建築年(西暦)']
for name in l:
mean = np.nanmean(train_data[name], axis=0)
std = np.nanstd(train_data[name], axis=0)
train_data[name] = (train_data[name] - mean)/std
train_data.describe()
.describe()
を使用して、基本統計量を確認できます。平均0、標準偏差1に正規化できていることを確認します。
予測モデルの作成,学習
モデルに投入する目的変数(y),説明変数(X)を作成します。予測モデルを学習させる上でnull値の含まれたレコードを除外します。
今回は、'最寄駅:名称'
、'最寄駅:距離(分)'
、'容積率(%)'
、'建築年(西暦)'
を説明変数として用いました。
model_input = train_data[['最寄駅:名称','最寄駅:距離(分)','容積率(%)','建築年(西暦)','y']]
model_input = model_input.dropna(how='any', axis=0)
X = model_input[['最寄駅:名称', '最寄駅:距離(分)', '容積率(%)', '建築年(西暦)']]
y = model_input['y']
このデータにはいくつかのカテゴリカルデータが含まれているので今のままでは機械学習モデルで扱いづらいです。そこで、カテゴリカルデータに対してget_dummies
関数を使いone-hot表現に変換することができます。
今回は、'最寄駅:名称'
のみone- hot表現に変換しました。drop_first=True
を使用することで、多重共線性を弱めることができます。
X = pd.get_dummies(X, drop_first=True)
いよいよ予測モデルの作成に入ります。
model.fit('説明変数','目的変数')
と記述することでモデルの学習が可能となります。目的変数を説明変数の組み合わせで説明可能な回帰モデルを作成できます。
import sklearn
from sklearn.linear_model import LinearRegression as LR
model = LR()
model.fit(X, y)
作成したモデルの係数と切片を見てみましょう。
model.coef_
で係数の一覧、.intercept_
で切片を見ることができます。
coeff = pd.DataFrame(X.columns)
coeff.columns = ['説明変数']
coeff['係数推定'] = model.coef_
print(coeff)
[647 rows x 2 columns]
model.intercept_
各説明変数の係数を比較することで、どの説明変数がどれほどの影響を与えていたか把握することができます。
各説明変数は、係数の値が正に大きいほど取引金額に対して正の寄与をしており、反対に負に大きいほど取引金額に対して負の寄与をしていると言えます。
上記の出力結果を見ると、'建築年(西暦)'
の係数の値が正に大きく、比較的近年に建てられた不動産であるほど取引金額が高いことがわかります。
'容積率(%)'
では係数が負に大きく、敷地面積に対する延べ床面積の割合が大きいほど、取引金額は低くなることがわかります。
モデルの評価
テストデータに対する回帰モデルの当てはまりの良さの指標としては'決定係数'などの指標が用いられます。
決定係数の値は、.score
で確認が可能です。
1に近いほど回帰式で予測された値が実際のデータに当てはまることを表します。
model.score(X,y)
今回作成したモデルの決定係数は0.31程度でした。
また、コンペの評価指標となるRMSLEについても出力が可能です。
今回は、sklearn.metrics
に含まれるmean_squared_log_error
メソッドを使用します。
RMSLEは平均化された誤差の対数差を示す指標であり、0に近いほど見積もられる予測誤差が小さく予測精度が高いことを表します。
モデルを元にした予測値は、model.predict('説明変数')
で表します。
mean_squared_log_error
メソッドでは、変数に負の値が含まれる場合エラーが発生するため、今回は0より小さい予測値については、0に変換しています。
import numpy as np
from sklearn.metrics import mean_squared_log_error
y_true = model_input['y']
y_pred = model.predict(X)
y_pred = np.where(y_pred < 0, 0, y_pred)
print(np.sqrt(mean_squared_log_error(y_true, y_pred)))
今回作成したモデルのRMSLEは0.541程度でした。
テストデータに対する出力
最後に、作成したモデルを使用してテストデータでの取引金額を予測します。
テストデータの説明変数については予め、上記でモデルに学習させたデータの説明変数と同様の前処理をする必要があります。
path_test = "/content/drive/My Drive/competition/ProbSpace/real_estate/test_data.csv"
test_data = pd.read_csv(path_test)
print(test_data['最寄駅:距離(分)'].value_counts())
test_data['最寄駅:距離(分)'] = test_data['最寄駅:距離(分)'].replace('30分?60分','45')
test_data['最寄駅:距離(分)'] = test_data['最寄駅:距離(分)'].replace('1H?1H30','75')
test_data['最寄駅:距離(分)'] = test_data['最寄駅:距離(分)'].replace('1H30?2H','105')
test_data['最寄駅:距離(分)'] = test_data['最寄駅:距離(分)'].replace('2H?','120')
test_data['最寄駅:距離(分)'] = pd.to_numeric(test_data['最寄駅:距離(分)'], errors='coerce')
test_data['建築年'] = test_data['建築年'].dropna()
test_data['建築年'] = test_data['建築年'].str.replace('戦前','昭和20年')
test_data['年号'] = test_data['建築年'].str[:2]
test_data['和暦年数'] = test_data['建築年'].str[2:].str.strip('年').fillna(0).astype(int)
test_data.loc[test_data['年号']=='昭和','建築年(西暦)'] = test_data['和暦年数'] + 1925
test_data.loc[test_data['年号']=='平成','建築年(西暦)'] = test_data['和暦年数'] + 1988
l2 = ['最寄駅:距離(分)','容積率(%)','建築年(西暦)']
for name in l2:
mean = np.nanmean(test_data[name], axis=0)
std = np.nanstd(test_data[name], axis=0)
test_data[name] = (test_data[name] - mean)/std
model_input_test = test_data[['最寄駅:名称','最寄駅:距離(分)','容積率(%)','建築年(西暦)']]
model_input_test = model_input_test.dropna(how='any', axis=0)
X_test = model_input_test
X_test = pd.get_dummies(X_test, drop_first=True)
モデルで使うデータとテストデータのカラム数が違うとエラーが発生するので、モデルの説明変数について事前に工夫が必要です。
今回は、学習データとテストデータで重複する説明変数のみを利用して学習と予測を実施します。
また、下記のコードでは訓練データで学習させた説明変数の中で、テストデータに含まれないについても列名を出力しています。
col_list = X.columns.tolist()
col_test_list = X_test.columns.tolist()
col_joint = col_list + col_test_list
col_dup = [x for x in set(col_joint) if col_joint.count(x) > 1]
col_list_p = [x for x in set(col_joint) if col_list.count(x) == 0]
print(col_list_p)
'最寄駅:名称_沢井'
は、取引金額に影響を及ぼす可能性がありますが、テストデータには含まれません。そのため、今回の予測の上ではモデルに学習させてもあまり意味はないと考え、学習させるデータから該当するレコードを除外しました。
その上で訓練データについて再度学習させて、モデルを作成します。
X_test=X_test[X_test['最寄駅:名称_沢井']!=1]
X_train_fix = X[col_dup]
X_test_fix = X_test[col_dup]
model.fit(X_train_fix, y)
先ほど使用したmodel.predict('説明変数')
の'説明変数'
にテストデータの値を代入することで、テストデータの予測値を算出することができます。
下記のようにして、提出用のsubmission.csv
を出力することが可能です。
test_predicted = model.predict(X_test_fix)
submit_df = pd.DataFrame({'y': test_predicted})
submit_df.index.name = 'id'
submit_df.index = submit_df.index + 1
submit_df.to_csv('submission.csv')