日本の不動産市場情報はもっと開放すべきだ!

Published: Oct. 10, 2023, 2:50 a.m. (UTC) / Updated: Oct. 29, 2024, 9:09 a.m. (UTC) 🔖 1 Bookmarks
👍 0 👎 0
日本語

1. 不動産取引の価格透明性は十分か?

1) REINS ~不動産業界に囲われた知識~

REINSというサイトがある。REINSは不動産業者のみが閲覧可能な情報ソースであり、不動産業者がその不動産取引情報を共有することで互いの事業運営を行いやすくするための情報基盤だ。不動産屋の前に多くのチラシが貼られているのを見たことがあるだろうが、この情報の源となっているデータベースと言ってもいい。この情報にアクセスすることが出来れば多くの人々にとって、不動産の仲介業者を通じて物件を紹介してもらう手間は大分さがられる。実際にそうした不動産情報検索サイトは多くの人が利用しているだろう。
 しかし、そのデータを集めてきて集計処理して本当に特定の物件が妥当な価格設定なのか知ろうとしたらどうだろう?それをするにはなんと不動産業者の免許が無ければならないというのが現在の日本の制度となっている。為替や証券取引所の取引データなど多少お金が出せば(場合によっては全く出さなくても)大量の情報が得られて、妥当な価格を知るための統計分析など容易に出来る時代に不動産業界はこうした基本情報を広く共有することを必死に避けてきている。
 不動産業者側の言い訳としてはおよそ次のようなものではないか、と予想している。
 不動産は極めて個別性が高いためい説明をして初めて意味があるため、そうしたデータベースで勝手に分析してもいたずら誤った予見をユーザーに与えてしまうか時間の無駄になるだろうから、そうした情報のアクセス権は専門家に限定するべきだ。
それを言ったら株式投資の分析など、個別性の塊でしかなくそうした情報は多くの人々がアクセス可能な状態であるが故に投資家が安心して取引が出来るため、市場が活性化され市場が拡大しているという点についてどう説明するのだろうか?不動産業界人が知識を囲っていたところで取引したい人に必要な情報が簡単にアクセス出来るようになっていなければ取引は発生しないのだ。こうした業界慣習こそが日本の不動産取引の活性化を阻害していると考えるべきだろう。

2) ユーザー目線で欲しい情報は何か?

当然ながら買い手としては、自分の購入する物件の妥当な購入価格がどれくらいなのか、実勢ベースで知りたいし、売り手にしても自分の物件の妥当な売却価格がどれくらいなのか実勢ベースで知りたい。
しかし、不動産屋なりが示してくるのは多分、過去の近所の物件のチラシ情報(REINSの情報)を数枚見せてきて、「この物件に近い物件だと最近だとこんな感じの価格感ですね」という提示のされ方をして、「ここから考えて、大体これ位が妥当な価格じゃないでしょうか?」と言った提案を受けることだろう。
感じのいい不動産の営業マンがそう言うのであれば「この人のいう事は信用出来そうだ」程度の理由で、その営業マンの言う通りの価格で取引をしてしまう人も多いのではないだろうか。
しかし、当然ながら不動産の営業マンも達成すべきノルマのために必死でやっている。
REINSから引っ張ってくるチラシ情報に手心を加えようと思えば「この物件の情報は都合が悪いから気が付かなかったことにしよう」ということが容易に出来てしまう。更に、そもそもそんないくつかの近所の取引実績データを見ただけで妥当な金額に落ち着く保証もない。
エンドユーザーは、もう少し視野を広げて見た際に、統計的に妥当な価格設定はどれくらいなのか?を知ることが出来れば意思決定をより効率的に、確信をもって行うことが出来るだろう。

3) 土地総合情報システムとは

土地総合情報システムというものがある。
「不動産取引情報提供システムの改善に関する検討委員会」とりまとめ という国土交通省の文書の冒頭にはいみじくも以下のように記載されている。

不動産は国民にとって重要な資産であり、地域経済の活性化や国民生活の向上を図る上で、不動産流通市場の整備は極めて重要な政策課題である。しかしながら、不動産流通市場においては、消費者と不動産流通業者に大きな情報格差が存在しており、特に、既存住宅等の価格情報を入手することは難しいことから、実際に取引する価格の妥当性等に不安感を抱く消費者は多い。
このような不動産流通市場の実態を踏まえ、平成19年4月より、業者間の取引情報交換システムである指定流通機構(レインズ)が保有するマンション及び一戸建ての不動産取引価格(成約価格)情報を活用し、RMI(レインズマーケットインフォメーション)として、消費者向けの情報提供サービスが行われている。

国土交通省も土地総合情報システムについては多大な時間をかけ、コストをかけて構築され運用されてけた経緯がある。
仮にもこれが骨抜きになっているとあっては国土交通省の名折れである。しかし、これがどれだけユーザビリティた高いのか?について碌な検証文書も無い。
よって本稿ではそれをざっと確認してみようと思う。

上記の文書を読むと骨抜きにされてしまった経緯が細かく記載されている気がしている。つまり、ユーザーへアンケートを取って取引物件の詳細情報を載せて欲しくないという方向で整理されている。取引が終わった人に聞いたら、自分の取引情報が誰かに見られたくないのは当たり前だ。しかし、自分が買い手や売り手だったちょっと前の状態の時には他人の取引実績を知りたくてしかたなかったはずだ。そして今時インターネットで物件検索すれば過去にその物件が幾らで取引されたかを知ることは容易な時代になっている。
こういう状況であるにもかかわらず、特定の物件が特定されてないように、情報が相当丸められてあいまいになっているような土地総合情報システムは役に立つのだろうか?
一応データはあるので、本来の趣旨に照らしてどの程度利用可能か検証する必要があるだろう。

2. 土地総合情報システムのデータで妥当なプライシングが出来るか?

1) データの中身の確認

それでは実際のデータの中身を見てみよう。東京の過去のデータを見てみると例えば次のようになる。

一見して分かるが、歯抜けが多い。また取引価格もかなり丸められていることが分かる。面積は5平米刻みで都心の小さい物件にとっては絶望的な精度の悪さだ。住所についても千代田区飯田橋まで位しか分からないとしたら残念ながらいくら何でも個人情報保護の観点を理由に骨抜きにされてしまった感が否めない。
 そして後に示すが、かなりダーティーなデータであり、データベースに乗せる前にデータクリーニングをする程度の基礎的なことすら行われていないため、ユーザーがその都度データのクリーニングを行う必要があるという非常にユーザビリティが低い作りになっているのも残念なところだ。

2) モデル作成の際の対象の考え方

今回の分析対象は、取引件数の多さから中古物件に絞った。また東京都の中でも下の表の通り都心の区に特化した。一つの地域に含まれる件数が多いことが理由だが2018年第1四半期から2022年第4四半期までの4年間のデータとしてはかなり少ない件数と思われ、そもそも開示されない情報が非常に多い印象を受ける。

地域 件数
港区 4561
渋谷区 3364
新宿区 5651
千代田区 1592
品川区 5645
文京区 3748
目黒区 3061
合計 27622

3) ソースコード

まずはデータの読込からクリーニングを行うまでが以下のようになっている。
特にconfig.xlsxというファイルには一つのカテゴリ変数ごとに一つのワークシートで、カテゴリの値を数値に変換するテーブルを持っており、これによって、定性的な情報を定量値に置き換えている。
例えば以下のテーブルのように土地の種類をidに持っている数字に置き換えることを行っている。

id 種類
1 宅地(土地)
2 宅地(土地と建物)
3 中古マンション等
4 農地
5 林地

それ以外の個別の修正事項は以下のソースコードに詳細が記載されているので、必要に応じてごらんいただきたい。

import pandas as pd
FILE = "./data/All_20053_20224/13_Tokyo_20053_202242.csv"  # 土地総合情報システムのデータ

df_data = pd.read_csv(FILE, encoding= "cp932")

import re
col = df_data.columns.values

for c in range(len(col)):
    col[c] = re.sub(":","", col[c])

# 対象を中古マンションに制限 
target = "中古マンション等"
df_data = df_data[df_data["種類"]== target].reset_index(drop=True)

# カテゴリ変数 
Category = ["種類","地区名", "地域","間取り", "土地の形状","建物の構造", "今後の利用目的", \
            "前面道路方位", "前面道路種類","都市計画", "改装", "取引の事情等"]  #  "用途"は不使用

COL2 = ["最寄駅:距離(分)", "取引価格(総額)", "面積(㎡)", "間口", \
        "建築年", "前面道路幅員(m)", "建ぺい率(%)", "容積率(%)",\
        "取引時点"]
       
import glob
ITEM = "./data/config/config.xlsx" # カテゴリ変数を整数値に変換するためのテーブルを定義しているファイルを読み込む
file = glob.glob(ITEM)
item = []
for c in Category:
    df_item = pd.read_excel(file[0], sheet_name=None)

df_data2 = df_data.copy()

def get_change(df_data2):
    L = len(df_data2)
    for c in Category:
        for i in range(L):
            M = len(df_item[c])
            for m in range(M):
                hit = 0

                if df_data2.at[i, c] == df_item[c].at[m, c]:
                    df_data2.at[i, c] = m
                    hit =1
                    break
            if hit == 0:
                df_data2.at[i, c] = M
                #print("!")
    return df_data2

# ここからはデータのクリーニングを行う
# 取引時点の修正 大小関係を指定して抽出出来るようにdatetimeに変換する
import unicodedata

Q = ["1", "2", "3", "4"]
for i in range(L):
    c = df_data2.at[i, "取引時点"]
    for q in Q:
        if q in c:
            nq = int(unicodedata.normalize('NFKC', q) ) * 3 
            if nq < 10:
                nqc = "/0"+ str(nq) + "/01"
            else :
                nqc = "/" + str(nq) + "/01"
     
            string = "年第" + q + "四半期"
            df_data2.at[i, "取引時点"] = c.replace(string,  nqc)


# 建築年の修正 西暦に変換することで大小関係を評価出来るようにする
def get_chikunen(df_data2):
    wareki = {"昭和":1925, "平成":1988 , "令和":2018}
    L = len(df_data2)
    today = 2023
    for i in range(L):
        s = df_data2.at[i, "建築年"]
        y = 0
        if type(s)==float:
            y = 1950
        else:
            for w in wareki.keys():
                dum = re.findall(w, s)
                if 0<len(dum) :
                    y =int( re.search(r'\d+', s).group() )
                    y = y+ wareki[w]
                    break
        if y == 0:
            y = 1950
        df_data2.at[i, "築年数"] = today - y
    return df_data2
    
# 面積の修正 記法が統一されていないものを修正
def get_m2(df_data2):
    s = "㎡以上"
    L = len(df_data2)
    for i in range(L):
        c = str(df_data2.at[i, "面積(㎡)"])
        if s in c:
            df_data2.at[i, "面積(㎡)"] = c.replace(s,"")
        elif "m" in c:
            d = re.findall('([1-9]\d{0,2}(,\d{3})*)m', c) ## カンマ付きの数字で、数字の後にmがあるものを抽出
            df_data2.at[i, "面積(㎡)"] = int(d[0][0].replace(",", ""))
            
    return df_data2
    
def ifin(s, c):
    for ss in s:
        # print(ss, str(c)
        if ss in str(c):
            return True
    return False

# 最寄駅距離(分)
s = ["分", "時", "H", "M", "?"]  # この文字列が含まれていたら60分に統一してしまう
max_minutes = 30
for i in range(L):
    c = df_data2.at[i,"最寄駅距離(分)"]
    if type(c) == float:
        df_data2.at[i,"最寄駅距離(分)"] = max_minutes
    elif ifin(s, c):
        df_data2.at[i,"最寄駅距離(分)"] = max_minutes
    else:
        pass
    
# 間口の修正 余計な文字列が含まれているものを除去
s = "m以上"
for i in range(L):
    c = df_data2.at[i, "間口"]
    if type(c) == float: ## nan と 15.5みたいな正しい間口はスルー
        pass
    elif s in c:
        df_data2.at[i, "間口"] = c.replace(s,"")

#  機械学習用のデータセットの保存と読み込み
df_data2.to_csv("./data/temp/df_data2.csv", encoding="cp932",index=False)

ここまでが分析のためのデータの前処理だった。
ここからが分析タスクとなる。


df = pd.read_csv("./data/temp/df_data2.csv", encoding="cp932")

# 取引時点の絞り込み

L = len(df)
from datetime import datetime as dt
for i in range(L):
    date = df.at[i, "取引時点"]
    df.at[i, "取引時点"] = dt.strptime(date, '%Y/%m/%d')

th = "取引時点"
th_date = "2019/1/1"

date = dt.strptime(th_date, '%Y/%m/%d')
df = df[date < df[th]].reset_index(drop=True)

# 分析に利用する変数の選択
col_apart = ["市区町村コード","地区名", "間取り",  "建物の構造", "今後の利用目的", \
          "改装","最寄駅距離(分)", "面積(㎡)", "築年数"]   # "建ぺい率(%)", "容積率(%)"都市計画", "取引の事情等",

col_X = ["種類","地域", "間取り", "土地の形状", "建物の構造", "今後の利用目的", \
         "前面道路方位", "前面道路種類", "改装",\
         "最寄駅距離(分)", "面積(㎡)", "間口", "前面道路幅員(m)", "築年数"]   #  , "取引時点"   #,"用途" "取引の事情等",,"都市計画""建ぺい率(%)", "容積率(%)"
col_y = ["取引価格(総額)"]

# 説明変数の作成
X = df.loc[: , col_apart]
X["label"] = df.loc[:, col_y]
 
# 地域絞り

th = "市区町村コード"
X = X[13101 <= X[th]]
X = X[X[th] <= 13113]

# 面積絞り
th = "面積(㎡)"
X = X[20<X[th]]
X = X[X[th] < 40]

dfx = X.reset_index(drop=True) #.dropna(axis=0).reset_index(drop=True)
L = len(dfx)
M = int(0.95*L)

X_train, X_test = dfx.loc[0: M, col_apart], dfx.loc[M: , col_apart]
y_train, y_test = dfx.loc[0: M, "label"], dfx.loc[M: , "label"]

from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
import lightgbm as lgb
import numpy as np

import matplotlib.pyplot as plt
from matplotlib import ticker

def str2time(s):
    if type(s) == str:
        return datetime.datetime.strptime(s, "%Y-%m-%d HH:%M:%S")

def make_fig(df, title):
    df['Time'] = df['放送日'].apply(str2time)
    fig = plt.figure(figsize =(8,4), facecolor = 'gray' )
    ax = fig.add_subplot(111, xlabel = 'Time', ylabel = title, fc = 'gray' )
    ax.xaxis.set_major_locator(ticker.MultipleLocator(100))  #n ko tobashi
    ax.xaxis.label.set_color('w')
    ax.yaxis.label.set_color('w')
    ax.spines['top'].set_color('w')
    ax.spines['left'].set_color('w')
    ax.spines['right'].set_color('w')
    ax.spines['bottom'].set_color('w')
    ax.tick_params(axis ='x', colors='w')
    ax.tick_params(axis ='y', colors='w')
    ax.plot(df['Time'], df[title], color = 'orange')
    ax.set_title(title)
    fig.autofmt_xdate(rotation=45)
    plt.grid(linestyle='--', color = 'white')
    plt.show()
    file = './'+title+'.png'
    fig.savefig(file)

def get_pic(x, y, label):
    fig = plt.figure(figsize=(7, 5))
    
    x_max = max(x)
    x_min = min(x)
    print('min',x_min, 'max',x_max, int((x_max-x_min)/10) )
    pitch = int((x_max-x_min)/10)
    if pitch ==0:
        pitch = 1
    x1 = np.arange(int(x_min), int(x_max), pitch)
    #y1 = a[0]*x1 + b
    y2 = x1
    # Figure内にAxesを追加()
    ax = fig.add_subplot(111) 
    ax.plot(x1, y2, color = "brown", label='perfect')
    ax.scatter(x, y, label=label, color = "red", s=10 ) #...3
    
    plt.xlabel("prediction")
    plt.ylabel("observation")
    plt.legend()
    plt.show() 

from sklearn import linear_model
CLF = linear_model.LinearRegression()
def tankaiki(X, Y):
    CLF.fit(X, Y)
    return CLF.coef_, CLF.intercept_ , CLF.score(X, Y)

#XX_test, y_pred = predictionGBM(X_test, num_iteration=gbm.best_iteration)
def important():
    y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)
    lgb_score = mean_squared_error(y_test, y_pred)
    a, b, r2 = tankaiki(np.array(y_test).reshape(-1, 1), y_pred)
    print('model: Y = {}X + {}: R2:{}'.format(a[0], b, r2))

    lgb.plot_importance(gbm, height=0.5, figsize=(8,16), importance_type='gain')

    #df_performance = r
    importance = pd.DataFrame(gbm.feature_importance(importance_type='gain'), index=X_train.columns, columns=['importance'])
    importance = importance.sort_values('importance', ascending=False)
    display(importance)

X_eval = X_train
y_eval = y_train

# 学習用
lgb_train = lgb.Dataset(X_train, y_train,
                        #categorical_feature=categorical_features,
                        free_raw_data=False)
# 検証用
lgb_eval = lgb.Dataset(X_eval, y_eval, reference=lgb_train,
                       #categorical_feature=categorical_features,
                       free_raw_data=False)

# パラメータを設定
params = {
        'objective':  'mean_squared_error',
        'metric': 'l1',
        'verbosity': -1,
        'boosting_type': 'gbdt',
        'learning_rate': 0.05,
        'num_leaves': 21,
        #'max_depth' : 10,
        #'min_data_in_leaf': 15,
        'seed': 42,
        #'num_iteration': 150
    }

# 学習
evaluation_results = {}                                     # 学習の経過を保存する箱
model = lgb.train(params,                                   # 上記で設定したパラメータ
                  lgb_train,                                # 使用するデータセット
                  num_boost_round=1000,                     # 学習の回数
                  valid_names=['train', 'valid'],           # 学習経過で表示する名称
                  valid_sets=[lgb_train, lgb_eval],         # モデル検証のデータセット
                  evals_result=evaluation_results,          # 学習の経過を保存
                  #categorical_feature=categorical_features, # カテゴリー変数を設定
                  early_stopping_rounds=20,                 # アーリーストッピング# 学習
                  verbose_eval=-1)                          # 学習の経過の非表示

# テストデータで予測する
y_pred = model.predict(X_test, num_iteration=model.best_iteration)
lgb_score = mean_squared_error(y_test, y_pred)
importance = pd.DataFrame(model.feature_importance(importance_type='gain'), index=X_train.columns, columns=['importance'])
importance = importance.sort_values('importance', ascending=False)
display(importance)
get_pic(y_pred, y_test, 'prediction')

4) 分析結果

まず、予測モデルにとって変数の重要度のランキングを行うと以下のようになる。
上位3つに「築年数」、「面積」、「地区名」が来るのは直感に非常に合致する。
次いで「市区町村コード」、「最寄駅距離(分)」、「間取り」、「改装」、「建物の構造」というのもなんとなくそのような気がするので、モデルとしては定性的に違和感のないものが出来たと考えられる。

次にモデルの予測結果と正解を比較した線形回帰図は以下の通り。軸の単位は1千万円なので、バラつきの大きさが甚大であることがうかがえる。3000万円くらいの予想された物件であれば、プラスマイナス1000万円位の誤差はあるだろう。

だがしかし、現実の不動産取引においても物件の個別性は非常に高いので、上記の結果が一概に特別モデルの精度が悪いと結論付けることは出来ないだろう。
もちろん、丸められた価格情報や面積情報、その他に価格に影響を与えそうな情報が十分に含まれているか?、学習するためのデータは十分か?という点では相当な改善余地があることは否めない。

少なくとも、これだけ少量で雑なデータであってもそれなりの結果を得ることは出来るということは言えるのではないかと考えている。
情報さえ充実すればかなりのパフォーマンスが期待出来る可能性があるだろう。

3. 総括

  • 土地総合情報システムは機械学習などの統計処理を行い集合知として構築されたモデルは一定程度妥当な価格の指針を与えるのに役立つ可能性はある
  • しかし個別の物件の価格予測として使えるほどデータは充実していない
  • データ不足が重大な足かせになっている
  • 土地総合情報システムは情報の充実を伴うアップグレードできないのか?このままでは有用性は極めて低く、莫大な国民の税金や日々の関係者の入力作業が無駄に費やされてしまっており、国益に適わないシステムとなっている
  • 一般ユーザーがREINSに有料でもいいからアクセス出来るように出来ないのか?

仕事の依頼はこちらまで(info@garnetstar.jp)。

Comments