はじめに
大学院時代に文京区の中心部に住み、教育の環境に気に入りました。
実際、文京区の教育水準は都内トップで、東京都23区の中でも、1番教育環境が充実していると言われているのが「文京区」です。日本を代表する大学である東京大学が所在する区であり、文教地区に指定されている地域が多いため、落ち着いた住環境が魅力です。
文京区の教育レベルの高さは23区の中で国・私立中学校への進学率が1番高いというデータに表れています。令和2年度の文京区の都内中学校進学者のうち、46%の生徒が国・私立中学校へと進学しています。(https://journal.kawlu.com/58821)
そのため、将来子供教育のために、また文京区に住みたいと思い、不動産の価格を予測するモデルを作ってみたいです。
しかし、全体的の値段を見ると、近いのうちに中古マンション以外無理そうなので、中古マンションにフォーカスしました。
環境
MacOS python 3.9 VSCode
手順
1.環境準備(MacOSにて、苦労しました。)
2.データ収取:不動産情報ライブラリからデータを収集
3.データ前処理:データを読み、整形や損値、異常値の処理
4.可視化によって、特徴量を選び
5.予測モデルを構築
参考文献
【Python】東京23区の中古マンション販売価格予測をやってみた
https://qiita.com/hidemiya666/items/a65d7fed65ff6443947c
埼玉県の中古マンション価格予測
https://qiita.com/aoikk45/private/e1cb48a20d5226800ec5
Pythonで不動産価格予測解析
https://note.com/maytakesao/n/n126165676d30
1.環境準備
一般的な処理用:numpy、pandas
可視化用:matplotlib、seaborn
回帰分析用:LinearRegression、Ridge、Lasso、ElasticNet、RandomForest、LightGBMと全結合のディープラーニングのライブラリ
# 必要なライブラリのインポート
import sys
print(sys.executable)
import os
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns
sns.set(font="IPAexGothic")
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import sklearn
from sklearn.model_selection import train_test_split
# 回帰分析用 from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Ridge
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.ensemble import RandomForestRegressor
import lightgbm as lgb
# 全結合ディープラーニング用
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Activation,Dense,Dropout
2.データ収集
来源
(元国土交通省土地総合情報システム、4月1日改版。。)
新しいシステムにて、データ大量ダウンロードできません。
文京区2018年1月から2023年3月の中古マンションに関するデータを取りました。
df_data = pd.read_csv(f'/Users/chikakochou/Downloads/Tokyo_Bunkyo Ward_20181_20233 - Tokyo_Bunkyo Ward_20181_20233.csv',encoding="utf-8")
データ確認
# 特徴量を確認
df_data.columns
# ランダムに1%のデータを抽出し、先頭から10件データを表示
df_data.sample(frac=0.01).head(10)
print(df_data.nunique())
print(df_data['市区町村名'].unique())
print(df_data['改装'].unique())
print(df_data['種類'].unique())
print(df_data['建築年'].unique())
print(df_data['最寄駅:距離(分)'].unique())
print(df_data['最寄駅:名称'].unique())
print(df_data['面積(�u)'].unique())
#各項目に対し、欠損数を確認
df_data.isnull().sum()
3.データ前処理
# 原始データのコピーを作る
df_data2= df_data
df_data2.sample(frac=0.01).head(10)
# 最寄駅:距離(分)が欠損している行を削除
df_data2.dropna(subset=['最寄駅:距離(分)'], inplace=True)
# 最寄駅:距離(分)を修正
df_data2=df_data2.replace( '1H30〜2H','90')
df_data2["最寄駅:距離(分)"]=df_data2["最寄駅:距離(分)"].astype(float)
df_data2["最寄駅:距離(分)"].info()
# 建築年を修正
df_data2['建築年'] = df_data2['建築年'].str.replace('年', '').astype(float)
# 面積(㎡)を修正
df_data2=df_data2.rename(columns={'面積(�u)': '面積(㎡)'})
df_data2["面積(㎡)"]=df_data2["面積(㎡)"].astype(float)
df_data2["面積(㎡)"].info()
4.可視化によって、特徴量を選び
#'最寄駅:距離(分)', '取引価格(総額)', '建築年','面積(㎡)'の統計量を確認
display(df_data2[['取引価格(総額)','面積(㎡)','建築年','最寄駅:距離(分)']].describe())
# 取引価格のヒストグラムの表示
sns.displot(df_data2['取引価格(総額)'])
#取引価格の外れ値の確認
sns.boxplot(y=df_data2['取引価格(総額)'])
# 面積の外れ値の確認
sns.boxplot(y=df_data2['面積(㎡)'])
# 建築年の外れ値の確認
sns.boxplot(y=df_data2['建築年'])
# 駅距離の外れ値の確認
sns.boxplot(y=df_data2['最寄駅:距離(分)'])
# 外れ値範囲の算出
# 四分位数 最小/最大值
data_summary = {
'取引価格(総額)': {'25%': 23000000, '75%': 67000000, 'min': 800000, 'max': 220000000},
'面積(㎡)': {'25%': 25, '75%': 60, 'min': 10, 'max': 450},
'建築年': {'25%': 1994, '75%': 2009, 'min': 1962, 'max': 2023},
'最寄駅:距離(分)': {'25%': 3, '75%': 7.75, 'min': 0, 'max': 90},
}
# IQRと外れ値範囲
extreme_value_ranges = {}
for key, value in data_summary.items():
IQR = value['75%'] - value['25%']
lower_bound = max(value['25%'] - 1.5 * IQR, value['min'])
upper_bound = min(value['75%'] + 1.5 * IQR, value['max'])
extreme_value_ranges[key] = (lower_bound, upper_bound)
extreme_value_ranges
# 外れ値の除去
df_data3 = df_data2.loc[df_data2['取引価格(総額)'] < 133000000.0]
df_data3 = df_data3.loc[df_data3['取引価格(総額)'] > 800000]
df_data3 = df_data3.loc[df_data3['面積(㎡)'] < 112.5]
df_data3 = df_data3.loc[df_data3['建築年'] > 1971.5]
df_data3 = df_data3.loc[df_data3['最寄駅:距離(分)'] < 14.875]
display(df_data3[['取引価格(総額)','面積(㎡)','建築年','最寄駅:距離(分)']].describe())
# 各特徴量の関係性を確認します。
# 散布図で取引価格、面積(㎡)、建築年、最寄駅:距離(分)の関係性を確認
cols = ['取引価格(総額)', '面積(㎡)','建築年', '最寄駅:距離(分)']
sns.pairplot(df_data3[cols], height = 2.5)
取引価格(総額)と面積(㎡)の間には正の相関が見られるようです。これは、面積が広いほど取引価格総額が高くなる傾向があることを意味しています。
建築年の分布には複数のピークが見られ、特定の年に建設された建物の数が多かったことを示している可能性があります。
最寄駅:距離(分)のヒストグラムを見ると、ほとんどのデータが低い分数に集中していることがわかります。これは、ほとんどの建物が最寄りの駅からそれほど遠くないことを示しています。
取引価格(総額)と最寄駅:距離(分)の間の散布図を見ても、強い関連性は明らかではありません。これは、駅からの距離が全体的に近いため、その差が限られている可能性があります。
建築年が新しいほど取引価格が高いが、傾向性がそれほど明白ではありません。それは、文京区は新しい部屋の他の特性(面積など)が落ちていると示しているかもしれません。
# relplotで取引価格、築年数、面積の関係性を可視化
sns.relplot(data=df_data3, x='面積(㎡)', y='取引価格(総額)', size='建築年', hue='建築年')
同じ面積内では、新しい建物(濃い色の点)の取引価格が高いようです。
取引価格は家の面積が大きくなるにつれて上昇し、これは全ての建築年代で見られる傾向です。
時間が経つにつれて、同じ面積の不動産でも取引価格が上昇しているようで、これは市場価格が一般的に上昇している傾向を反映している可能性があります。
大きな家(100以上)小さいな家(20以下)においては、建築年が価格に与える影響は他の面積の家屋ほど顕著ではないかもしれません。
言い換えれば、広いや狭い面積の家屋の場合、購入者は建築年よりも他の要因、例えば地理的な位置、設計の品質、または家のユニークな特徴などをより重視するかもしれません。
予測モデルの説明変数として、「面積(㎡)」「建築年(西暦)」 「最寄駅:距離(分)」を使用してモデルを作成します。
5.予測モデルを構築
線形回帰、リッジ回帰、ラッソ回帰、エラスティックネット回帰、ランダムフォレスト、LightGBMを使って予測をしていきます。各モデルの二乗平均平方根誤差を比較し、値が最も小さくなるモデルをベストモデルとします。また、各モデルの訓練データと検証データの相関係数も確認しました。
X = df_data3[['面積(㎡)', '最寄駅:距離(分)','建築年']]
y = df_data3['取引価格(総額)']
# 訓練データ検証データに分ける(9:1)
train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.1, random_state=0)
best_model = ""
pred_model = []
# 線形回帰
model = LinearRegression() # 線形回帰モデル
model.fit(train_X,train_y) # 学習
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("LinearRegression RMSE : %.2f" % (mse** 0.5))
min_mse = mse
best_model = "線形"
pred_model = pred_model.append(model)
# 訓練データと検証データでの決定係数を確認
print('LinearRegression train score : {}'.format(model.score(train_X, train_y)))
print('LinearRegression test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
# リッジ回帰
model = Ridge() # リッジ回帰モデル
model.fit(train_X, train_y) # 学習
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("Ridge RMSE : %.2f" % (mse** 0.5))
if min_mse > mse:
min_mse = mse
best_model = "リッジ"
pred_model = model
# 訓練データと検証データでの決定係数を確認
print('Ridge train score : {}'.format(model.score(train_X, train_y)))
print('Ridge test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
# ラッソ回帰
model = Lasso() # ラッソ回帰モデル
model.fit(train_X, train_y) # 学習
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("Lasso RMSE : %.2f" % (mse** 0.5))
if min_mse > mse:
min_mse = mse
best_model = "ラッソ"
pred_model = model
# 訓練データと検証データでの決定係数を確認
print('Lasso train score : {}'.format(model.score(train_X, train_y)))
print('Lasso test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
# ElasticNet回帰
model = ElasticNet(l1_ratio=0.5) # エラスティックネット回帰モデル
model.fit(train_X, train_y) # 学習
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("ElasticNet RMSE : %.2f" % (mse** 0.5))
if min_mse > mse:
min_mse = mse
best_model = "エラスティックネット"
pred_model = model
# 訓練データと検証データでの決定係数を確認
print('ElasticNet train score : {}'.format(model.score(train_X, train_y)))
print('ElasticNet test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
# RandomForest回帰
model = RandomForestRegressor(100) # ランダムフォレスト回帰モデル
model.fit(train_X,train_y) # 学習
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("RandomForestRegressor RMSE : %.2f" % (mse** 0.5))
if min_mse > mse:
min_mse = mse
best_model = "ランダムフォレスト回帰"
pred_model = model
# 訓練データと検証データでの決定係数を確認
print('RandomForestRegressor train score : {}'.format(model.score(train_X, train_y)))
print('RandomForestRegressor test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
# LightGBM
gbm_reg = lgb.LGBMRegressor(objective='regression', n_estimators=500)
model = gbm_reg.fit(train_X, train_y, eval_set=[(test_X, test_y)], eval_metric='l2')
pred_y = model.predict(test_X) # 予測
mse= mean_squared_error(test_y, pred_y)# 評価
print("LightGBM RMSE : %.2f" % (mse** 0.5))
if min_mse > mse:
min_mse = mse
best_model = "LightGBM"
pred_model = model
# 訓練データと検証データでの決定係数を確認
print('LightGBM train score : {}'.format(model.score(train_X, train_y)))
print('LightGBM test score : {}'.format(model.score(test_X, test_y)))
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
print("ベストモデル:{}".format(best_model))
LinearRegression RMSE : 10109137.66
LinearRegression train score : 0.8692490895553495
LinearRegression test score : 0.8679481888659117
Ridge RMSE : 10109136.26
Ridge train score : 0.8692490895533355
Ridge test score : 0.8679482251997575
Lasso RMSE : 10109137.65
Lasso train score : 0.8692490895553494
Lasso test score : 0.8679481890724409
ElasticNet RMSE : 10106698.25
ElasticNet train score : 0.8692415179846276
ElasticNet test score : 0.8680119112523018
RandomForestRegressor RMSE : 9399184.81
RandomForestRegressor train score : 0.9643552469205566
RandomForestRegressor test score : 0.8858445845106652
LightGBM RMSE : 8855018.24
LightGBM train score : 0.9473471599236066
LightGBM test score : 0.8986800278462523
予測の結果、回帰の中にLightGBMが最も良いモデルとなりました。
ディープニューラルネットワーク
# DNN回帰
model = Sequential()
model.add(Dense(128, input_dim=train_X.shape[1]))
model.add(Activation("relu"))
model.add(Dense(256))
model.add(Activation("relu"))
model.add(Dropout(rate=0.1))
model.add(Dense(128))
model.add(Activation("relu"))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam')
history = model.fit(train_X, train_y, epochs=100 , batch_size=32 , verbose=0, validation_data=(test_X, test_y) ) # 学習
pred_y = model.predict(test_X) # 予測
train_pred_y = model.predict(train_X)
mse= mean_squared_error(test_y, pred_y)# 評価
print("DeepNuralNet RMSE : %.2f" % (mse** 0.5))
#訓練データと検証データでの決定係数を確認
from sklearn.metrics import r2_score
r2_train = r2_score(train_y, train_pred_y)
r2_test = r2_score(test_y, pred_y)
print('DeepNuralNet train score : ', r2_train)
print('DeepNuralNet test score :', r2_test)
plot_data = pd.DataFrame()
plot_data['observed'] = test_y[:]
plot_data['predict'] = pred_y[:]
plt.figure(figsize=(10,10))
sns.jointplot(x='observed', y='predict', data=plot_data, kind='reg')
plt.show()
DeepNuralNet RMSE : 13021421.57
DeepNuralNet train score : 0.7748416584229538
DeepNuralNet test score : 0.7809047610707098
精度が比較的に低い結果となりました。
チューニングが必要なようです。
標準化または正規化:すべての特徴が同じスケールになるようにします。訓練プロセスの収束を速め、数値範囲が大きい特徴がモデル訓練に不均衡な影響を与えるのを防ぎます。
ネットワーク構造の調整:ディープニューラルネットワークの構造(例えば、層の数、各層のニューロンの数)の調整は、モデルの性能にとって非常に重要です。データ中の複雑なパターンや関係をより良く捉えることができます。
正則化:Dropoutなどの正則化技術を使用すると、過学習を防ぎ、訓練データで良好な性能を発揮するとともに、見たことのないデータでの一般化能力も向上します。
from sklearn.preprocessing import StandardScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation, Dropout
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt
import seaborn as sns
# 特征缩放
scaler = StandardScaler()
train_X_scaled = scaler.fit_transform(train_X)
test_X_scaled = scaler.transform(test_X)
# DNN回帰模型
model = Sequential()
model.add(Dense(128, input_dim=train_X_scaled.shape[1], activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam')
# 模型训练
history = model.fit(train_X_scaled, train_y, epochs=100, batch_size=32, verbose=0, validation_data=(test_X_scaled, test_y))
# 预测
pred_y = model.predict(test_X_scaled).flatten()
train_pred_y = model.predict(train_X_scaled).flatten()
# 性能评估
mse = mean_squared_error(test_y, pred_y)
rmse = mse ** 0.5
r2_train = r2_score(train_y, train_pred_y)
r2_test = r2_score(test_y, pred_y)
print(f"DeepNeuralNet RMSE: {rmse:.2f}")
print(f"DeepNeuralNet train score: {r2_train:.3f}")
print(f"DeepNeuralNet test score: {r2_test:.3f}")
# 绘制观察值与预测值的关系
plot_data = pd.DataFrame({'Observed': test_y, 'Predicted': pred_y})
sns.jointplot(x='Observed', y='Predicted', data=plot_data, kind='reg', height=10)
plt.show()
DeepNeuralNet RMSE: 9579290.33
DeepNeuralNet train score: 0.882
DeepNeuralNet test score: 0.881
改善できました!
考察
これらの要素の中で、標準化または正規化そしてネットワーク構造の調整は、モデルの性能を向上させるために最も重要かもしれません。
標準化または正規化はモデル訓練の効率と安定性に直接影響し、ネットワーク構造の調整はモデルの学習能力と複雑さを決定しました。
正則化は、モデルが過学習を避けるのに役立ち、テストデータでのパフォーマンスを向上させました。