はじめに
近年、人口減少、少子高齢化、東京一極集中等から、特に地方都市を中心に空き家問題が顕在化している。
この問題を解決するために、住宅の価値を構成する要因を明らかにすることを目的として分析を行う。
解決したい社会課題
上述した通り空き家問題であり、住宅の価値を構成する要因を把握できれば、今後価値が少なくなる住宅を減らすことができることや、現状価値の少ない住宅の価値向上を検討でき、また今後恒久的に価値の向上が難しい住宅の取り壊し等も検討できる。
分析するデータ
今回は、サンプルデータてしてNishikaが提供する住宅価格予測コンペのデータを使用した。
実行環境
パソコン:
開発環境:Spyder
言語:Python
ライブラリ:Pandas,Glob,Numpy,Matplotlib
分析の流れ
- 基礎集計をする
- ロジスティック回帰モデルで評価する
分析の過程
実行したコード
import os
import pandas as pd
import glob
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
#pandasライブラリのDataframe型をprintする際に省略させずに表示
#データ変数の項目確認に使用
pd.set_option('display.max_columns', 1000)
# ディレクトリを変更
os.chdir(r"C:\Users\z574z\OneDrive\デスクトップ\samurai課題\20_回帰タスク\data\train")
#空のDataFrameを作成・定義
df = pd.DataFrame()
# .csvを含むファイルをpd.read_csv()で読み込み
for i in glob.glob("*.csv"):
tmp_df = pd.read_csv(i)
# DataFrameを連結する
df = pd.concat([df, tmp_df])
#testデータを読み込み
test_df = pd.read_csv(r"C:\Users\z574z\OneDrive\デスクトップ\samurai課題\20_回帰タスク\data\test.csv")
test_df.head()
# 欠損セルにTrueを入力して、欠損値の個数を表示
df.isnull().sum()
test_df.isnull().sum()
#データの列・行数を表示
df.shape
test_df.shape
# "取引価格(総額)" 列を追加
df["取引価格(総額)"] = df["取引価格(総額)_log"]
# 数値の列を常用対数から整数に変換する
df["取引価格(総額)"] = np.power(10, df["取引価格(総額)"])
# 新しい列 "取引価格(総額)(万円)" を追加し、取引総額を10,000で割った値を入力する
df["取引価格(総額)(万円)"] = df["取引価格(総額)"] / 10000
#renameメソッドを使用して、カラム名を英数字とアンダースコアに変更
df.rename(columns={
'取引価格(総額)': 'Total_Price',
}, inplace=True)
###最寄り駅までの距離(分)
# 特定の列のデータの種類を抽出して表示する
column_name = "最寄駅:距離(分)"
data_types = test_df[column_name].unique()
data_types
# 1H?1H30 を 90 に変更
df.loc[df["最寄駅:距離(分)"] == "1H?1H30", "最寄駅:距離(分)"] = "90"
# 30分?60分 を 60 に変更
df.loc[df["最寄駅:距離(分)"] == "30分?60分", "最寄駅:距離(分)"] = "60"
# 2H? を 120 に変更
df.loc[df["最寄駅:距離(分)"] == "2H?", "最寄駅:距離(分)"] = "120"
# 1H30?2H を 120 に変更
df.loc[df["最寄駅:距離(分)"] == "1H30?2H", "最寄駅:距離(分)"] = "120"
#テストデータ
# 1H30?2H を 120 に変更
test_df.loc[test_df["最寄駅:距離(分)"] == "1H30?2H", "最寄駅:距離(分)"] = "120"
# 1H?1H30 を 90 に変更
test_df.loc[test_df["最寄駅:距離(分)"] == "1H?1H30", "最寄駅:距離(分)"] = "90"
# 2H? を 120 に変更
test_df.loc[test_df["最寄駅:距離(分)"] == "2H?", "最寄駅:距離(分)"] = "120"
# 30分?60分 を 60 に変更
test_df.loc[test_df["最寄駅:距離(分)"] == "30分?60分", "最寄駅:距離(分)"] = "60"
## 最寄駅:距離(分)2列を追加し、中央値で欠損値を補う
# trainデータ
# '最寄駅:距離(分)2' 列を作成
df["最寄駅:距離(分)2"] = df["最寄駅:距離(分)"]
# 欠損値を中央値で補完
df["最寄駅:距離(分)2"] = pd.to_numeric(df["最寄駅:距離(分)2"], errors='coerce')
median_value = df["最寄駅:距離(分)2"].median()
df["最寄駅:距離(分)2"].fillna(median_value, inplace=True)
# 数値へ変換
df['最寄駅:距離(分)2'] = pd.to_numeric(df['最寄駅:距離(分)2'], errors='coerce')
# inf を最大値に置換
max_value = df[df['最寄駅:距離(分)2'] != float('inf')]['最寄駅:距離(分)2'].max()
df['最寄駅:距離(分)2'] = df['最寄駅:距離(分)2'].replace([float('inf'), -float('inf')], max_value)
# NaN を 0 に置換(中央値補完後なので不要だが、念のため)
df['最寄駅:距離(分)2'] = df['最寄駅:距離(分)2'].fillna(0)
# 整数型に変換
df['最寄駅:距離(分)2'] = df['最寄駅:距離(分)2'].astype(int)
# testデータ
# '最寄駅:距離(分)2' 列を作成
test_df["最寄駅:距離(分)2"] = test_df["最寄駅:距離(分)"]
# 欠損値を中央値で補完
test_df["最寄駅:距離(分)2"] = pd.to_numeric(test_df["最寄駅:距離(分)2"], errors='coerce')
median_value = test_df["最寄駅:距離(分)2"].median()
test_df["最寄駅:距離(分)2"].fillna(median_value, inplace=True)
# 数値へ変換
test_df['最寄駅:距離(分)2'] = pd.to_numeric(test_df['最寄駅:距離(分)2'], errors='coerce')
# inf を最大値に置換
max_value = test_df[test_df['最寄駅:距離(分)2'] != float('inf')]['最寄駅:距離(分)2'].max()
test_df['最寄駅:距離(分)2'] = test_df['最寄駅:距離(分)2'].replace([float('inf'), -float('inf')], max_value)
# NaN を 0 に置換(中央値補完後なので不要だが、念のため)
test_df['最寄駅:距離(分)2'] = test_df['最寄駅:距離(分)2'].fillna(0)
# 整数型に変換
test_df['最寄駅:距離(分)2'] = test_df['最寄駅:距離(分)2'].astype(int)
#最寄り駅までの距離、29分まで右肩下がり、60,90,120分は同じ値として処理
# 30以上は一定値
def Moyori_dummy(i):
if i <= 29:
out = i
elif i > 30:
out = 30
return out
#築年数30年を基準にダミー変数化
df["Moyori_dummy"] = df["最寄駅:距離(分)2"].apply(Moyori_dummy)
test_df["Moyori_dummy"] = test_df["最寄駅:距離(分)2"].apply(Moyori_dummy)
# 駅までの距離を数値として扱うためにint型に変換
df['Moyori_dummy'] = df['Moyori_dummy'].astype(int)
test_df['Moyori_dummy'] = test_df['Moyori_dummy'].astype(int)
# 「面積(㎡)」の"2000㎡以上"を"2000"に変更
df.loc[df["面積(㎡)"] == "2000㎡以上", "面積(㎡)"] = "2000"
test_df.loc[test_df["面積(㎡)"] == "2000㎡以上", "面積(㎡)"] = "2000"
#「面積(㎡)」の列を数値型に変換し、変換できない値は、0に置き換える
df["面積(㎡)"] = pd.to_numeric(df["面積(㎡)"], errors='coerce').fillna(0)
test_df["面積(㎡)"] = pd.to_numeric(test_df["面積(㎡)"], errors='coerce').fillna(0)
# 面積(㎡)の列のデータの種類とその数を表示
column_name = "面積(㎡)"
value_counts = df[column_name].value_counts().sort_index()
print(value_counts)
#renameメソッドを使用して、カラム名を英数字とアンダースコアに変更
df.rename(columns={
'面積(㎡)': 'Area',
}, inplace=True)
test_df.rename(columns={
'面積(㎡)': 'Area',
}, inplace=True)
###建築年数
#欠損値の補完
# "建築年" 列の最頻値を計算
西暦建築年 = df["建築年"].mode()[0]
西暦建築年 = test_df["建築年"].mode()[0]
# "西暦建築年" 列を追加
df["西暦建築年"] = df["建築年"]
test_df["西暦建築年"] = test_df["建築年"]
# "西暦建築年" 列の欠損値を最頻値で補完
df["西暦建築年"] = df["西暦建築年"].fillna(西暦建築年)
test_df["西暦建築年"] = test_df["西暦建築年"].fillna(西暦建築年)
# 戦前を"1945"に変更
df.loc[df["西暦建築年"] == "戦前", "西暦建築年"] = "1945"
# 「西暦建築年」列を和暦から西暦に変換する
# 和暦と西暦の対応表を作成
era_to_year = {
'昭和': lambda x: int(x) + 1925,
'平成': lambda x: int(x) + 1988,
'令和': lambda x: int(x) + 2018
}
# 建築年を和暦から西暦に変換
# trainデータ
def convert_era_to_year(era_year):
era, year = era_year[:2], era_year[2:-1]
if era in era_to_year:
return era_to_year[era](year)
else:
return 1945 # 未知の和暦の場合は1945を返す
df['西暦建築年'] = df['西暦建築年'].apply(convert_era_to_year)
# testデータ
def convert_era_to_year(era_year):
era, year = era_year[:2], era_year[2:-1]
if era in era_to_year:
return era_to_year[era](year)
else:
return 0 # 0
test_df['西暦建築年'] = test_df['西暦建築年'].apply(convert_era_to_year)
# 西暦年から2024年までの年数を計算して新しい列 'Years_Since' を作成
current_year = 2024
df['建築年数'] = current_year - df['西暦建築年']
test_df['建築年数'] = current_year - test_df['西暦建築年']
# "建築年数" 列の各値の出現回数を取得
value_counts = df['建築年数'].value_counts()
value_counts2 = test_df['建築年数'].value_counts()
# 出現回数を昇順にソートして表示
value_counts_sorted = value_counts.sort_index()
value_counts2_sorted = value_counts2.sort_index()
# 全行を表示するように設定
pd.set_option('display.max_rows', None)
print(value_counts_sorted)
print(value_counts2_sorted)
# 関数を定義
def Nensu(i):
if i <= 60:
return i
else:
return 0
# 新しい列"Nensu"を作成し、"建築年数"列から値を変換
df["Nensu"] = df["建築年数"].apply(Nensu)
test_df["Nensu"] = test_df["建築年数"].apply(Nensu)
# 関数を定義
def Kaiso(i):
if i == "改装済":
return 1
else:
return 0
# 新しい列"Nensu"を作成し、"建築年数"列から値を変換
df["Kaiso"] = df["改装"].apply(Kaiso)
test_df["Kaiso"] = test_df["改装"].apply(Kaiso)
#欠損値の補完
# "Kaiso" 列の欠損値を最頻値で補完
df["Kaiso"] = df["Kaiso"].fillna(Kaiso)
test_df["Kaiso"] = test_df["Kaiso"].fillna(Kaiso)
from sklearn.preprocessing import LabelEncoder
# ラベルエンコーダのインスタンス作成
label_encoder = LabelEncoder()
# '間取り'列を数値に変換
df['Madori'] = label_encoder.fit_transform(df['間取り'])
test_df['Madori'] = label_encoder.fit_transform(test_df['間取り'])
print(df.head())
## statsmodelsでの検証
import statsmodels.formula.api as smf
##説明変数を減らす Toshikeikakuを削除
# 重回帰モデルの構築
formula = 'Total_Price ~ Nensu + Kaiso + Moyori_dummy + Area + Madori'
model = smf.ols(formula, data=df).fit()
# 結果の出力
print(model.summary())
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error
#データの分割
train_df, val_df = train_test_split(df, test_size=0.3, random_state=0)
# トレーニングデータの準備
X_train = df[['Nensu', 'Kaiso', 'Moyori_dummy', 'Area', 'Madori']]
y_train = df['Total_Price']
# テストデータからTotal_Priceを除く(実際のケースではテストデータにこの列が存在しないことを想定)
#val_df = val_df.drop(columns=['Total_Price'])
# モデルの構築
model = LinearRegression()
# フィッティング
model.fit(X_train, y_train)
# バリデータでの予測
X_test = val_df[['Nensu', 'Kaiso', 'Moyori_dummy', 'Area', 'Madori']]
predictions = model.predict(X_test)
# バリデータに予測値を追加
val_df['Predicted_Total_Price'] = predictions
# MAEの計算
y_true = val_df['Total_Price']
y_pred = val_df['Predicted_Total_Price']
mae = mean_absolute_error(y_true, y_pred)
print(f'Mean Absolute Error (MAE): {mae}')
#予測
# 必要な列のみ抽出
X_test_real = test_df[['Nensu', 'Kaiso', 'Moyori_dummy', 'Area', 'Madori']]
# テストデータでの予測
test_predictions = model.predict(X_test_real)
# テストデータに予測値を追加
test_df['Predicted_Total_Price'] = test_predictions
# MAEの計算
y_true_test = val_df['Total_Price'][:len(test_df)]
y_pred_test = test_df['Predicted_Total_Price']
mae = mean_absolute_error(y_true_test, y_pred_test)
print(f'Mean Absolute Error (MAE): {mae}')
# Predicted_Total_Priceの値を常用対数に変換して新しい列を作成
test_df['取引価格(総額)_log'] = np.log10(test_df['Predicted_Total_Price'])
# 予測結果の表示
print(test_df.head())
可視化したデータ(グラフなど)
分析したデータから得られた情報
傾向
- 駅から近くなれば住宅価格が高くなる。
- 築年数が長くなれば住宅価格が低くなる。
- 総面積が大きくなれば住宅価格が高くなる。
- 改装されていると住宅価格が高くなる。
分類
-
気を付けられる点
- 駅から離れた場所の新規の住宅を規制する
- 改装に補助金を出す
-
気を付けられない点
- 築年数が長くなるとどうしようもない
- 面積は変えられない
課題
改装すれば価値の向上が期待できるのにも関わらず、そういった点が放置され、空き家になっている中で新築の物件が建築されている。
考察
改装以外の要件について、住宅の価値が高くなる要件を満たした物件を優先的に改装を行っていくことで、空き家の価値が向上し空き家の発生が抑えられると考えられる。また、未改装の物件で、かつその他の条件を住宅の価値向上に対して不利な場合は、住宅を取り壊すといった対策も考えられる。
まとめ
近年、空き家発生率が高くなっているという状況に問題意識を持ち、住宅の価値に貢献する要因を分析した結果として、いくつかの要件が確認され、特にすでに空き家となった後にできる対策としては、改装は対策として有効であることが確認できた。このため、対策としては改装以外の条件が有利な物件を優先的に改装し空き家の価値を上げていくことと考えた。また、改装以外の要件があまりにも不利な物件については、取り壊すことも有限な財源を無駄に消費しないためには有効であると考えられる。
参考にした資料、文献
- Nishika 住宅価格コンペ
- 参考にしたページ
PythonのStatsModelsで重回帰分析:https://qiita.com/ground0state/items/529a76ce2a450754cbb0