2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[不動産情報ライブラリ]Pythonを使用した不動産情報の可視化

Last updated at Posted at 2024-09-19

はじめに

近年不動産の高騰が問題となっておりますが、
個人的にも不動産テックは気になる分野でしたのでPythonとJupyterNotebookを使用して、物件情報の可視化をしてみます。
情報のソースには、不動産情報ライブラリを使用します。

目次

  1. 不動産情報ライブラリについて
  2. 可視化ライブラリ
  3. ジオコーディング
  4. APIからデータの取得
  5. テーブルデータの可視化
  6. 地図上へのプロット
  7. 最後に

不動産情報ライブラリについて

不動産情報ライブラリとは、不動産の取引価格、地価公示等の価格情報や防災情報、都市計画情報、周辺施設情報等、不動産に関する情報をご覧になることができる国土交通省のWEBサイトです。

2024年4月から運用が開始され、主に以下の不動産情報がWeb及びAPIで提供されております。

情報の種類 掲載情報
価格 地価公示、都道府県地価調査、不動産取引価格情報、成約価格情報
周辺施設等 学校、小・中学校区、市町村役場等、医療機関、福祉施設 など
防災 洪水浸水想定区域、土砂災害警戒区域等、津波浸水想定 など
都市計画 用途地域、防火・準防火地域、立地適正化区域 など
その他 将来推計人口(500mメッシュ;2050年まで(5年間隔))など

APIを利用するには、下記から申請をしてAPIキーを発行していただく必要があります。
発行には条件があり、法人のみ申請可能となっております。
必要な情報をフォームから入力して申請すると、翌営業日くらいにはメールでAPIキーの連絡が届きます。
https://www.reinfolib.mlit.go.jp/api/request/

現在APIとして提供されているデータは以下となります。

  1. 不動産価格(取引価格・成約価格)情報取得API
  2. 都道府県内市区町村一覧取得API
  3. 鑑定評価書情報API
  4. 不動産価格(取引価格・成約価格)情報のポイント (点) API
  5. 地価公示・地価調査のポイント(点)API
  6. 都市計画決定GISデータ(都市計画区域/区域区分)API
  7. 都市計画決定GISデータ(用途地域)API
  8. 都市計画決定GISデータ(立地適正化計画)API
  9. 国土数値情報(小学校区)API
  10. 国土数値情報(中学校区)API
  11. 国土数値情報(学校)API
  12. 国土数値情報(保育園・幼稚園等)API
  13. 国土数値情報(医療機関)API
  14. 国土数値情報(福祉施設)API
  15. 国土数値情報(将来推計人口500mメッシュ)API
  16. 都市計画決定GISデータ(防火・準防火地域)API
  17. 国土数値情報(駅別乗降客数)API
  18. 国土数値情報(災害危険区域)API
  19. 国土数値情報(図書館)API
  20. 国土数値情報(市区町村村役場及び集会施設等)API
  21. 国土数値情報(自然公園地域)API
  22. 国土数値情報(大規模盛土造成地マップ)API
  23. 国土数値情報(地すべり防止地区)API
  24. 国土数値情報(急傾斜地崩壊危険区域)API
  25. 都市計画決定GISデータ(地区計画)API
  26. 都市計画決定GISデータ(高度利用地区)API

可視化ライブラリ

地理空間データの処理に、geopandasとshapelyを使用します。
グラフの作成にはseaborn、地図の作成にはFoliumを使用します。

FoliumはJavascriptのLeafletをラップしているライブラリで、
JupyterNotebook上でインタラクティブにOSMベースの地図を操作をすることができます。
image.png
https://python-visualization.github.io/folium/latest/

Seabronがデフォルトでは日本語対応していないため、japanize_matplotlibを追加しています。
https://seaborn.pydata.org/

import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString
import seaborn as sns
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
import folium

ジオコーディング

jageocoderを使用することで、フリー入力の住所から都道府県と市区町村に分割し、
更に緯度経度を取得することができます。
https://github.com/t-sagara/jageocoder/blob/main/README_ja.md

>>> import jageocoder
>>> jageocoder.init(url='https://jageocoder.info-proto.com/jsonrpc')
>>> jageocoder.search('新宿区西新宿2-8-1')
{'matched': '新宿区西新宿2-8-', 'candidates': [{'id': 5961406, 'name': '8番', 'x': 139.691778, 'y': 35.689627, 'level': 7, 'note': None, 'fullname': ['東京都', '新宿区', '西新宿', '二丁目', '8番']}]}

Geoデータを返すAPIを呼び出すときには、座標情報をタイルで指定する必要があるため
ズームレベルを指定して、取得した緯度経度をタイルに変換しておきます。

def latlon2tile(lon, lat, z):
	"""
	緯度経度をタイル座標に変換
	"""
	x = int((lon / 180 + 1) * 2**z / 2) # x座標
	y = int(((-log(tan((45 + lat / 2) * pi / 180)) + pi) * 2**z / (2 * pi))) # y座標
	return [y,x]
import jageocoder
jageocoder.init(url='https://jageocoder.info-proto.com/jsonrpc')

target = dict(address = "東京都足立区綾瀬")

# 住所情報取得
address_data = jageocoder.search(target['address'].replace(' ', ''))
if address_data['candidates'][0]['fullname'][1] in DESIGNATED_CITY: #政令指定都市の場合、区をWardに代入
    target['pref'] = address_data['candidates'][0]['fullname'][0]
    target['ward'] = address_data['candidates'][0]['fullname'][2]
    target['city'] = address_data['candidates'][0]['fullname'][3]
else:
    target['pref'] = address_data['candidates'][0]['fullname'][0]
    target['ward'] = address_data['candidates'][0]['fullname'][1]
    target['city'] = address_data['candidates'][0]['fullname'][2]

# タイル情報取得
zoom_level = 14 # 14:約2.45キロ 15:約1.22キロ
target['lat_lon'] = [address_data['candidates'][0]['y'], address_data['candidates'][0]['x']]
tile = latlon2tile(target['lat_lon'][1], target['lat_lon'][0], zoom_level)

# ポイント情報作成
target['point'] = Point(target['lat_lon'][1], target['lat_lon'][0])
target['point_utm'] = gpd.GeoSeries([target['point']], crs="EPSG:4326").to_crs(epsg=32654).iloc[0]

APIからデータの取得

requestsを使用してAPIの結果を取得します。
配列でデータが格納されているため、分析の使い勝手がよいデータフレームに変換する関数を作成します。

def api2df(url, params):
    """
    不動産APIの情報をデータフレームに変換
    """
    response = requests.get(url, headers=HEADERS, params=params)
    user_data = response.json()
    if "message" in user_data:
        return user_data
    elif "data" in user_data:
        return pd.DataFrame(user_data["data"])
    elif "features" in user_data :
        if len(user_data['features']) > 0:
            return gpd.GeoDataFrame.from_features(user_data['features'], crs="EPSG:4326")
        else:
            return pd.DataFrame()
    else:
        return pd.DataFrame()

試しに市区町村コードを取得してみます。

url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XIT002"
params = {"area": PREF_CODE.index(target['pref'])}
df_city = api2df(url, params)

image.png

テーブルデータの可視化

  • 人口統計情報
    APIにタイルとズームレベルを指定して、データを取得します。
    指定したタイルの周りのタイルを取得する関数を作成しておきます。
def get_9tiles_data(url, params):
    """
    該当タイルの周りまで取得
    """
    gdf_list = []
    for i in [-1, 0, 1]:
        for j in [-1, 0, 1]:
           new_params = params.copy()
           new_params['x'] = params['x'] + i
           new_params['y'] = params['y'] + j
           gdf = api2df(url, new_params) 
           if len(gdf) > 0:
               gdf_list.append(gdf)
    if len(gdf_list) > 0 :
        return pd.concat(gdf_list)
    else:
        return pd.DataFrame()

データを取得します。

url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XKT013"
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0]}
gdf_population = get_9tiles_data(url, params)

取得したデータを前処理して、年代別の統計情報を集計します。

# 20XX年男女計xx歳以上人口のカラムのみ抽出
age_range_dict = {'PTA': '0-14歳', 'PTB': '15-64歳', 'PTC':'65-歳'}
summary_columns = [c for c in gdf_population.columns if c.split('_')[0] in age_range_dict.keys()] 

# カラムの整形
df_target_population = target_population[summary_columns].reset_index()
df_target_population.columns = ['index','population']
df_target_population['year'] = df_target_population['index'].apply(lambda x: x.split('_')[1])
df_target_population['age'] = df_target_population['index'].apply(lambda x: age_range_dict[x.split('_')[0]])

# 年代がカラムになるよう整形
df_target_population = df_target_population[['year', 'age', 'population']].sort_values(['age','year'])
df_target_population = df_target_population.pivot_table(['population'], index='year', columns='age')
df_target_population.columns = [c[1] for c in df_target_population.columns]
df_target_population

image.png

seabornの積み上げ棒グラフを使用して可視化します。

bottom = 0
for c in df_target_population.columns: 
    bar_plot = plt.bar(df_target_population.index, df_target_population[c].astype(int), bottom=bottom, label=c)
    plt.bar_label(bar_plot, label_type='center')
    bottom = df_target_population[c]
plt.title('将来推計人口の推移:500mメッシュ')
plt.legend()
plt.show()

image.png

  • 駅情報
    データを取得します。
url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XKT015"
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0]}
gdf_station = get_9tiles_data(url, params)

対象地点と駅の直線距離を計算して、3キロ圏内にデータを絞ります。

# 3km圏内のレコード
range_distance = 3000
gdf_station['distance'] = gdf_station.to_crs(epsg=32654)['geometry'].distance(target['point_utm']) # 距離をメートル換算するためにCRSを32654に変換
gdf_station[['S12_003_ja', 'S12_001_ja','distance']][gdf_station['distance'] <= range_distance].sort_values('distance').drop_duplicates('S12_001_ja')

image.png

一番最寄りの駅情報を集計します。

# 対象となる駅を抽出
from_year = 2011 # 乗降客数のデータの開始年
station_user_info_columns = [f'S12_0{str(i*4+9).zfill(2)}' for i in range(0, datetime.now().year - from_year -1)] # 乗降数のカラムを取得
station_user = gdf_station[gdf_station['S12_001_ja'] == target['station']].iloc[0][station_user_info_columns]

# データフレームに整形
df_station_user = pd.DataFrame(station_user).reset_index()
df_station_user.columns = ['year', 'users']
df_station_user['year'] = [i for i in range(from_year, from_year + len(df_station_user))]
df_station_user

image.png

棒グラフにします。おそらくコロナの影響で2020年に乗降客数がガクッと減ってそこから利用者が再び戻ってきています。

sns.barplot(data=df_station_user, x = "year", y = "users")
plt.title(f'乗降者数の推移:{target["station"]}')
plt.show()

image.png

  • 不動産価格(取引価格・成約価格)情報
    こちらの情報はレインズをもとに個人情報をマスクした実際の取引データになります。
    市区町村を指定して直近5年分取得します。
url = 'https://www.reinfolib.mlit.go.jp/ex-api/external/XIT001'

# 直近5年間のデータを取得
past_year = 10
df_deal_price_list = []
for i in range(-past_year, 0):
    params = {"year": datetime.now().year+i, "city": ward_code}
    df = api2df(url, params)
    if type(df) == dict:
        continue
    df['DealYear'] = datetime.now().year+i
    df_deal_price_list.append(df)

if len(df_deal_price_list) > 0:
    df_deal_price = pd.concat(df_deal_price_list)
else:
    print('結果が見つかりません')

カラムを前処理してエリア内の成約サンプルを抽出します。

# 値の前処理
df_deal_price = df_deal_price[df_deal_price['TotalFloorArea'].apply(lambda x: x.replace(' ', '') != '')] # ㎡数不明を除外
df_deal_price = df_deal_price[df_deal_price['BuildingYear'].apply(lambda x: '' in x)] # 建築年が不明を除外
df_deal_price['TotalFloorArea'] = df_deal_price['TotalFloorArea'].astype(float)
df_deal_price['BuildingYearInt'] = df_deal_price['BuildingYear'].apply(lambda x: int(x.replace('', '')))
df_deal_price['BuildingAge'] = df_deal_price['DealYear'] - df_deal_price['BuildingYearInt']
df_deal_price['PricePerUnit'] =  (df_deal_price['TradePrice'].astype(int) / df_deal_price['TotalFloorArea'].astype(int)).astype(int) # ㎡単価 = 金額 / 延べ床面積
df_deal_price['UnitPrice'] =  (df_deal_price['PricePerUnit'] * 3.3).astype(int) # 坪単価 = ㎡単価 * 3.3㎡

image.png

坪単価の統計情報を出します。

df_deal_price_mean = df_deal_price_city[['DealYear','UnitPrice']].groupby('DealYear').mean()
df_deal_price_mean

image.png

推移をグラフ化します。

sns.barplot(data=df_deal_price_city, x = "DealYear", y = "UnitPrice",)
plt.title('周辺物件の成約価格の坪単価の推移')
plt.show()

image.png

  • 鑑定価格
    こちらの情報は、国土交通省での土地の鑑定価格(路線価、公示価格)となり、相続税や固定資産税の計算などに利用されます。

直近5年のデータを取得します。

# 直近5年間のデータを取得
url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XCT001"
division = '00' # 住宅地
from_year = 5

df_eval_price_list = []
for i in range(-from_year, 0):
    params = {"area": PREF_CODE.index(target['pref']), "year":datetime.now().year+i, "division":division}
    df = api2df(url, params)
    df_eval_price_list.append(df)
df_eval_price = pd.concat(df_eval_price_list)

最寄りの駅付近にデータを絞ります

gdf_eval_price = gpd.GeoDataFrame(
    df_eval_price, geometry=gpd.points_from_xy(df_eval_price['位置座標 経度'], df_eval_price['位置座標 緯度']), crs="EPSG:4326"
)
# カラムの前処理
df_eval_price['1㎡当たりの価格'] = df_eval_price['1㎡当たりの価格'].astype(int)
df_eval_price['路線価 相続税路線価'] = df_eval_price['路線価 相続税路線価'].astype(int)

# 対象となる駅に絞る
df_eval_price_city = df_eval_price[df_eval_price['標準地 交通施設の状況 交通施設'].str.contains(target['station'])]
df_eval_price_city = df_eval_price_city[['価格時点','標準地 所在地 住居表示','1㎡当たりの価格', '路線価 年', '路線価 相続税路線価', '標準地 交通施設の状況 距離']]

image.png

推移を計算します。

df_eval_price_city['坪単価'] = df_eval_price_city['1㎡当たりの価格'] * 3.3
df_eval_price_city[['1㎡当たりの価格', '坪単価', '路線価 相続税路線価','価格時点']].groupby('価格時点').mean()

image.png

プロットします。路線価は公示価格の凡そ0.7倍の値になります。
image.png

sns.barplot(data=df_eval_price_city, x = "価格時点", y = "1㎡当たりの価格", label="公示価格")
sns.barplot(data=df_eval_price_city, x = "価格時点", y = "路線価 相続税路線価", label="路線価")
plt.title('公示価格と路線価の推移')
plt.legend(loc='lower right')
plt.show()

地図上へプロット

  • 鑑定価格
    データを取得して、前処理します。
url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XPT002"
this_year = datetime.now().year
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0], "year":this_year-1}
gdf_eval_price = get_9tiles_data(url, params)

# Pointから緯度経度に変換
gdf_eval_price['lat_lon'] = gdf_eval_price['geometry'].apply(lambda x: x.__geo_interface__['coordinates'][::-1])

# 徒歩分数の計算
gdf_eval_price['walking_distance_min'] = gdf_eval_price["u_road_distance_to_nearest_station_name_ja"].apply(
   lambda x: (x.replace('m','').replace(',','')))
gdf_eval_price['walking_distance_min'] = gdf_eval_price["walking_distance_min"].apply(
   lambda x: int(int(x) / 80) if x != '' else np.nan)

# 坪単価の計算
gdf_eval_price["unit_price"] = gdf_eval_price["u_current_years_price_ja"].apply(
   lambda x: x.replace('(円/㎡)', '').replace(',', ''))
gdf_eval_price["unit_price"] = gdf_eval_price["unit_price"].apply(lambda x: int(int(x) * 3.3) if x != '' else np.nan)

Foliumを使用して鑑定ポイントを地図にプロットします。

m = folium.Map(location=[target['lat_lon'][0], target['lat_lon'][1]], zoom_start=15)
folium.Marker([target['lat_lon'][0], target['lat_lon'][1]], icon=folium.Icon(color='red', icon="home")).add_to(m) # 物件の場所

for idx, row in gdf_eval_price.iterrows():
    lat_lon = row['geometry'].__geo_interface__['coordinates'][::-1]
    text = f'''
        住所: {row["residence_display_name_ja"]}<br>
        徒歩距離: {row["walking_distance_min"]}分({row["u_road_distance_to_nearest_station_name_ja"]})<br>
        坪単価: {row["unit_price"]:,}円<br>
        '''
    popup = folium.Popup(text, max_width=300)
    folium.Marker([lat_lon[0], lat_lon[1]], popup=popup).add_to(m)
m

image.png

  • 周辺施設
    物件近辺の学校、保育施設、医療施設を取得してプロットします。

学校

url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XKT006"
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0]}
gdf_school = get_9tiles_data(url, params)
# 1km圏内のレコード
gdf_school['distance'] = gdf_school.to_crs(epsg=32654)['geometry'].distance(target['point_utm'])
gdf_school[['P29_004_ja','distance']][gdf_school['distance'] <= 1000].sort_values('distance')

image.png

保育施設

url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XKT007"
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0]}
gdf_pre_school = get_9tiles_data(url, params)

# 1km圏内のレコード
range_distance = 1000
gdf_pre_school['distance'] = gdf_pre_school.to_crs(epsg=32654)['geometry'].distance(target['point_utm'])
gdf_pre_school[['preSchoolName_ja','distance']][gdf_pre_school['distance'] <= range_distance].sort_values('distance').drop_duplicates('preSchoolName_ja')

image.png

医療施設

url = "https://www.reinfolib.mlit.go.jp/ex-api/external/XKT010"
params = {"response_format":"geojson", "z":zoom_level, 'x':tile[1], 'y':tile[0]}
gdf_medical = get_9tiles_data(url, params)
# 1km圏内のレコード
range_distance = 1000
gdf_medical['distance'] = gdf_medical.to_crs(epsg=32654)['geometry'].distance(target['point_utm']) # 距離をメートルに換算するためCRSを変換
gdf_medical[['P04_002_ja','distance']][gdf_medical['distance'] <= range_distance].sort_values('distance').head(20)

image.png

上記を地図上にプロットします。レイヤーを使用することでグループで表示することができます。

m = folium.Map(location=[target['lat_lon'][0], target['lat_lon'][1]], zoom_start=15)
folium.Marker([target['lat_lon'][0], target['lat_lon'][1]], icon=folium.Icon(color='red', icon="home")).add_to(m) # 物件の場所


# 学校
fg = folium.FeatureGroup(name="学校", show=True).add_to(m)
gdf_school['lat_lon'] = gdf_school['geometry'].apply(lambda x: x.__geo_interface__['coordinates'][::-1])
for idx, row in gdf_school.iterrows():
    lat_lon = row['geometry'].__geo_interface__['coordinates'][::-1]
    text = row['P29_004_ja']
    popup = folium.Popup(text, max_width=300)
    folium.Marker([lat_lon[0], lat_lon[1]], popup=popup, icon=folium.Icon(icon="school", prefix='fa')).add_to(fg)

# 保育園
fg = folium.FeatureGroup(name="保育園", show=True).add_to(m)
for idx, row in gdf_pre_school.iterrows():
    lat_lon = row['geometry'].__geo_interface__['coordinates'][::-1]
    text = row['preSchoolName_ja']
    popup = folium.Popup(text, max_width=300)
    folium.Marker([lat_lon[0], lat_lon[1]], popup=popup, icon=folium.Icon(icon="baby", prefix='fa')).add_to(fg)

# 医療機関
fg = folium.FeatureGroup(name="医療機関", show=True).add_to(m)
for idx, row in gdf_medical.iterrows():
    lat_lon = row['geometry'].__geo_interface__['coordinates'][::-1]
    text = row['P04_002_ja']
    popup = folium.Popup(text, max_width=300)
    folium.Marker([lat_lon[0], lat_lon[1]], popup=popup, icon=folium.Icon(icon="hospital", prefix='fa', color='pink')).add_to(fg)

folium.LayerControl().add_to(m)
m

image.png

最後に

これらの集計をWeb上で行えるサービスを作成しましたので、
お試しいただければ幸いです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?