この記事ではPythonを使って家探しサイトSUUMOからデータを爬取して可視化して機械学習で予測モデルを作成することについて解説します。
コードも分析の結果も全部ここに載せます。
扱うデータは福岡市内の物件ですが、方法としてはどこの物件探しでも通用のはずなので、データ分析を勉強したい人のために役に立つかと思います。
はじめに
就活して内定が決まって転職することになったので、次は引っ越しです。転職先は福岡県福岡市博多区。
(これについては以前の記事で書いて経験をシェアしたので興味があれば)
仕事探しが終わって、これでもう楽になると思ったら次は「家探し」ですね。人生色々あって大変です。家探しってなんか色々就活と似ていてデジャヴュって感じもします。どれも自分の居場所探しだから。
私はまだ一度も福岡(というより九州)に行ったことないので、あまり何も分からなかったです。住むならどこがいいか他の人に相談しながら情報を収集してきました。
そして家探しでよく使われるSUUMOやLIFULL HOME’Sなどのウェブサイトで検索してみたら賃貸物件のデータがいっぱい出てきてその中をどう選んで決めたらいいか、あまり簡単ではない。
そこで私もデータ科学者の端くれなので、こういう時こそ自分の持っている技術を駆使できる場面になりそうですね。そう考えて今回の家探しのためにSUUMOを爬取してデータを分析することになりました。
今回やるのは
- 爬取(ウェブサイトからデータ取得)
- 可視化(グラフを描いたり、表に纏めたり)
- 機械学習による予測モデル
どれもよくやっていることで、手段も使い慣れているpandas、matplotlib、sklearnですが、お金に関わるデータを扱うのは今回初めてです。こんな技術は色んな用途があるから習得したら使い道がいくらでもあってとても有用ですね。
qiitaでもSUUMOや物件探し関連の記事が多くて私はこれらの記事も読んで色々参考になりました。
爬取を行うとなると、どうしてもウェブサイトに悪影響を与えるかと心配してしまうけど、SUUMOの爬取に関する記事はqiitaで検索しても沢山出てくるので、やっぱり誰もするようで全然問題ないと判断しました。この人気なサイトはきっといつもアクセスされて、爬取にも慣れているでしょう。
目的
今回のデータ分析の目的は色々ですね。大体こんな感じ:
- 日本の家探しに関する勉強
- どんな要因が家賃に関係あるか探求する
- 福岡で一人暮らしの家賃の相場を把握する
- どこに住めばいいか決める
- 隠れたわりといい物件を見つけ出す
- データ分析の練習
qiitaで何か面白い記事を書いて沢山「いいね」頂きたい!
そう考えて色々行いましたので、自分のやった手段と結果をできるだけ纏めてここで載せていきます。
使うツール
今回のデータの扱いでPythonの色んなライブラリーが使われます。
requests | ウェブサイトへの接続 |
bs4 (beautiful soup) | HTMLデータの解析 |
numpy | 配列での数学計算 |
pandas | データ処理 |
matplotlib | 可視化 |
sklearn (scikit-learn) | 機械学習 |
どれも簡単にpipでインストールできるのでインストール方法などの詳細は省略します。
データ取得
今回の対象は福岡市にある全ての物件です。勤務地は博多駅の近くだがその辺りに住むのは得策とは限らない。できるだけ博多駅に通いやすい駅の近くに住んで電車で通勤したいと思います。
どの辺りがいいかまだ漠然としていたので、とりあえず福岡市全体は対象としました。
実際に隣の新宮町や春日市や大野城市に住んでいて福岡市に通勤する人も多いらしくて、これも後で調べますが、今回はまず対象外にしています。
このURLにアクセスすることで公開されている福岡市内の賃貸物件データを全部表示する画面に入ります。
https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=090&bs=040&ta=40&sa=02&sngz=&po1=25&pc=50
その中で各物件の情報が羅列されています。この頁を爬取したら物件のデータを手に入れます。
ただし各頁では50件しか表示されないので、全部のデータを取得するためには次の頁にアクセスする必要があります。
そしてそうするためにはURLに&page=(頁番号)
を追加すればいいです。
ということで、まずは各頁にアクセスして爬取してファイルに保存していきます。
import requests,time,os
from bs4 import BeautifulSoup
t0 = time.time()
# 最初の頁のURL
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=090&bs=040&ta=40&sa=02&sngz=&po1=25&pc=50'
# サイトに接続する
req = requests.get(url)
# 取得したhtmlを解析する
bisu = BeautifulSoup(req.content,'lxml')
# 何頁あるか調べる
peejisuu = int(bisu.find(class_='pagination-parts').find_all('li')[-1].text)
# 取得したhtmlをファイルに保存
open('html/suumo001.html','wb').write(req.content)
print(' 全部%d頁ある\n1頁完了。%.3f秒過ぎた'%(peejisuu,time.time()-t0))
# 保存するフォルダがまだなければまず新しく作る
if(not os.path.exists('html')):
os.mkdir('html')
# 第2頁からのデータを取得していく
for p in range(2,peejisuu+1):
# 次々の頁のURL
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=090&bs=040&ta=40&sa=02&sngz=&po1=25&pc=50&page=%d'%p
try: # 各頁に接続する
req = requests.get(url)
except: # 接続に問題が起きた場合、暫く待ってもう一度接続する
print('問題が起きた。3秒後に再度試す')
time.sleep(3) # まずは3秒待つ
req = requests.get(url)
# ファイルに保存
open('html/suumo%03d.html'%p,'wb').write(req.content)
print('%d頁完了。%.3f秒過ぎた'%(p,time.time()-t0))
各頁は大体1-2秒かかります。私がデータを取得した時点で312頁あるから大体6分かかりました。
今回はただhtmlデータをそのまま取得して保存するだけで、データを纏めて処理するのは次に段階でします。そのため保存するスペースが必要で、全部で200MBくらいかかりました。
実は各頁にアクセスしたらすぐ解析して必要なデータだけ取るという手もありますが、そうしたらデータ解析方法を帰るたびにまた取得する必要があるということになります。爬取行為はできるだけ回数少ない方がいいので、とりあえず全部保存しておく方が妥当だと思います。
それにSUUMOのデータは更新されていくものなので、その時点のデータを獲得できるように全部のデータを一気に取得した方がいい。もしデータを取得している間にデータ更新があったら頁の並びも変わって重複のデータを取ることになるという可能性もあるでしょう。
拘り条件のデータ
検索する時に所在地や専有面積などの他にも、候補を更に絞るために拘りの条件を指定することも大事です。SUUMOでは色んな条件で検索することができます。今回の分析も色んな拘り条件を考慮したいです。
ただし物件一覧の頁には拘り条件に関する情報が載っていないから、このまま全部の頁のデータを解析しても拘り条件の情報は手に入りません。
各物件の詳細の頁にアクセスしたら可能ですが、数万頁あるのであまり現実的ではありません。
となると思いついた一番簡単な手段は、各拘り条件で検索して、その条件を満たす物件のリンクのリストを作るということです。リンクは物件ごと違うので、それぞれを区別するためのIDとなります。これを前処理の段階で使って他の情報と一緒に加えます。
設定できる拘り条件は多すぎるので、今回は気になる10個だけ選びました。
- バス・トイレ別
- 温水洗浄便座
- 室内洗濯機置場
- 洗面所独立
- 家具家電付き
- 宅配ボックス
- 敷地内ゴミ置場
- バルコニー付
- 都市ガス
- ロフト
この部分の爬取を行うコードは以下。
import requests,time,json
from bs4 import BeautifulSoup
# 各条件とその検索番号の辞書
tc_dic = {'バス・トイレ別': '0400301',
'温水洗浄便座': '0400302',
'室内洗濯機置場': '0400501',
'洗面所独立': '0400502',
'ロフト': '0400505',
'家具家電付き': '0400510',
'宅配ボックス': '0400905',
'敷地内ゴミ置場': '0400906',
'バルコニー付': '0400907',
'都市ガス': '0400912'}
t0 = time.time()
link_id_dic = {} # 各条件のリンクIDのリストを入れる辞書
for jouken in tc_dic:
tc = tc_dic[jouken] # この条件の検索番号
link_id_lis = [] # 条件のリンクIDを入れるリスト
p = 1
while('最後の頁まで'):
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=090&bs=040&ta=40&sa=02&sngz=&po1=25&pc=50&page=%d&tc=%s'%(p,tc)
try: # 各頁に接続する
req = requests.get(url)
except: # 接続失敗の場合
print('問題が起きた。3秒後に再度試す')
time.sleep(3)
req = requests.get(url)
# htmlを解析する
bisu = BeautifulSoup(req.content,'lxml')
if(p==1): # 第1頁に入ったら頁数を調べておく
peejisuu = int(bisu.find(class_='pagination-parts').find_all('li')[-1].text)
print(' 「%s」条件で全部%s頁ある'%(jouken,peejisuu))
# 全部のリンクIDを取得する
for link in bisu.find_all(class_='js-cassette_link_href cassetteitem_other-linktext'):
link_id_lis.append(link['href'])
print('%d頁完了。%.3f秒過ぎた'%(p,time.time()-t0))
# 最後の頁になったらここで終わり
if(p==peejisuu):
break
p += 1
# 収集したリストを辞書に入れる
link_id_dic[jouken] = link_id_lis
# jsonで保存する
json.dump(link_id_dic,open('link_jouken.json','w',encoding='utf-8'),ensure_ascii=False,indent=1)
前処理をして.csvに保続する
爬取の段階が終わったら次は前処理です。取得したデータのhtmlから欲しい部分を切り取って纏めてpandasのデータフレームにして、.csvファイルに保存します。
各頁では50つの建物のデータが羅列されますが、各建物の中で部屋のデータが入っています。一つの建物の中で複数部屋がある場合もあるので、各頁で得られた部屋の情報は50以上となります。
ここで取得するデータは12あります。置かれた場所の順で:
データ | 説明 |
---|---|
建物種類 | アパート、マンション、一戸建てなど |
建物名 | 建物の名前。あまり当てにならないから分析に使わないけど一応 |
区 | 福岡市のどの区にあるか |
最寄り駅 | 一番近くにある駅。ただし無い場合もある |
歩 | (最寄り駅があれば)駅から歩く分単位の時間 |
築年数 | 建てられてから経った年数 |
階数 | 建物は何階ある |
==== 以下は部屋別のデータ ==== | |
階 | 何階にある |
家賃 | 賃料+管理費 |
間取り | ワンルーム、1K、1DK、1LDKなど |
専有面積 | 平方メートル単位の専有面積 |
リンク | 物件の頁へのリンク |
以上7つは建物ごとに共通ですが、5つはその中の部屋それぞれ違うデータです。
そしてそれに加えて10の拘り条件の有夢(0と1で表記)も合わせて22列の表ができます。
全ては以下のコードで纏められます。
import re,glob,json
from bs4 import BeautifulSoup
import pandas as pd
# 各条件のリンクを収める辞書
jouken_dic_lis = json.load(open('link_jouken.json',encoding='utf-8'))
heya_dic_lis = []
for html in glob.glob('html/*.html'):
bisu = BeautifulSoup(open(html,'rb').read(),'lxml')
for tatemono in bisu.find_all(class_='cassetteitem'):
# 建物の情報を入れる辞書
tatemono_dic = {}
tatemono_dic['建物種類'] = tatemono.find(class_='ui-pct ui-pct--util1').text.replace('賃貸','')
tatemono_dic['建物名'] = tatemono.find(class_='cassetteitem_content-title').text
# 住所の情報から区だけ取得
juusho = tatemono.find(class_='cassetteitem_detail-col1').text
tatemono_dic['区'] = re.search(r'市(\w+)区',juusho).group(1)
# 最寄り駅は最大3つ並んでいるからその中一番近いものだけ取る
for moyori in tatemono.find(class_='cassetteitem_detail-col2').text.strip().split('\n'):
# 駅から歩く時間が表示されたものだけ取る
m = re.search(r'/(\S+)駅 +?歩(\d+)分',moyori)
if(m):
aruku = float(m.group(2)) # 歩く時間
# 30以内歩けるものだけ取る
if(aruku<=30 and (not tatemono_dic.get('歩') or aruku<tatemono_dic['歩'])):
tatemono_dic['最寄り駅'] = m.group(1).replace('JR','')
tatemono_dic['歩'] = aruku
# 築年数は0の場合は「新築」と表記される
chiku,kaisuu = tatemono.find(class_='cassetteitem_detail-col3').text.strip().split('\n')
if('新築' in chiku):
tatemono_dic['築年数'] = 0
else:
tatemono_dic['築年数'] = int(re.search(r'(\d+)年',chiku).group(1))
# 階数は平屋の場合1階
if('平屋' in kaisuu):
tatemono_dic['階数'] = 1
else:
tatemono_dic['階数'] = int(re.search(r'(\d+)階建',kaisuu).group(1))
# 部屋別でデータを取得する
for heya in tatemono.find_all(class_='js-cassette_link'):
heya_dic = tatemono_dic.copy() # 建物のデータを全ての部屋のデータにする
kai = heya.find_all('td')[2].text.strip()
if(kai=='-'): # 一階建ては「-」と表記
heya_dic['階'] = 1
else:
heya_dic['階'] = int(re.search(r'\d+',kai).group(0))
# 賃料
chinryou = heya.find(class_='cassetteitem_other-emphasis ui-text--bold').text
chinryou = int(float(re.search(r'(\d+)(\.\d+)?',chinryou).group(0))*10000)
# 管理費
kanrihi = heya.find(class_='cassetteitem_price cassetteitem_price--administration').text
if(kanrihi=='-'): # 管理費がない場合もある
kanrihi = 0
else:
kanrihi = int(re.search(r'\d+',kanrihi).group(0))
#賃料と管理費を合わせて家賃
heya_dic['家賃'] = chinryou+kanrihi
heya_dic['間取り'] = heya.find(class_='cassetteitem_madori').text
heya_dic['専有面積'] = float(heya.find(class_='cassetteitem_menseki').text.split('m')[0])
heya_dic['リンク'] = heya.find(class_='js-cassette_link_href cassetteitem_other-linktext')['href']
# 各条件のリストにあるかどうかチェック
for k in jouken_dic_lis:
heya_dic[k] = int(heya_dic['リンク'] in jouken_dic_lis[k])
# 部屋の情報が入っている辞書をリストに入れる
heya_dic_lis.append(heya_dic)
# 辞書のリストからデータフレームへ
df = pd.DataFrame(heya_dic_lis)
# 家賃の順で並び替える
df = df.sort_values('家賃')
# .csvファイルに保存
df.to_csv('suumo_df.csv',index=False)
最寄り駅に関しては、実は普段最大3駅が載っているが、ここでは一番徒歩時間が短い一つだけ取ります。駅ではなくバス停が載っているものもありますが、この場合は最寄り駅がないと判断します。また、もし徒歩時間は30分以上なら流石にあまり歩ける距離ではないので、この場合も最寄り駅がないとします。
普段家賃は賃料と管理費に分けられているけど、殆どの場合この2つを合わせて考慮すればよくて、別々として考える必要はないので、ここでは単に『賃料+管理費』を意味する『家賃』だけ考慮します。
こうして今回取得でき物件数は全部 33,572 件あります。
数字データ
データの準備ができたら次は可視化と分析の段階に入ります。まず以上取得できたデータの中で数字であるものは6つあります。
- 家賃
- 専有面積
- 築年数
- 階数
- 階
- 歩
その中で「歩」は「最寄り駅」によって違うし、NaNであるものも含まれるので、ここではまず触れないでおきます。
まず保存された.csvファイルからデータを読み込んで、最大値、最小値、平均値、中央値を纏めてデータフレームにしてみます。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('suumo_df.csv')
df_ymck = df[['家賃','専有面積','築年数','階数','階']]
mmmm = pd.DataFrame({'最小': df_ymck.min(),
'最大': df_ymck.max(),
'平均': df_ymck.mean(),
'中央値': df_ymck.median()})
わかりやすいように、ここではできたデータフレームを表にしてます。
最小 | 最大 | 平均 | 中央値 | |
---|---|---|---|---|
家賃 | 15000 | 2300000 | 70911.55 | 64000 |
専有面積 | 3.6 | 837.88 | 34.43 | 28.98 |
築年数 | 0 | 99 | 18.41 | 18 |
階数 | 1 | 48 | 7.21 | 6 |
階 | 1 | 39 | 4.44 | 3 |
福岡で人が住めるようなビルは48階まであるようです。でも今実際に空室があるのは39階までらしい。
ちなみにこの「階数」は地下が含まれません。地下の部屋も稀にSUUMOに載っているようですが、調べた結果福岡ではそのような部屋は0です。福岡で地下に住んでいる人がいないと言えるかどうかわかりませんが、たとえあったとしても珍しいでしょう。
単身の暮らし
今回は一人暮らしの単身生活のための部屋が目的なので、単身に向いている「ワンルーム、1D、1DK」のみ絞ったデータも纏めてみます。
(実際に1LDKで1人住む人もいるが、2人住む場合も多くて、一人で住むなら豪華で高いのでここで含みません)
では「ワンルーム/1K/1DK」だけ絞って同じような表を作ります。
df_bocchi = df[df['間取り'].str.match(r'(ワンルーム|1K|1DK)')]
df_ymck_bocchi = df_bocchi[['家賃','専有面積','築年数','階数','階']]
mmmm_bocchi = pd.DataFrame({'最小': df_ymck_bocchi.min(),
'最大': df_ymck_bocchi.max(),
'平均': df_ymck_bocchi.mean(),
'中央値': df_ymck_bocchi.median()})
最小 | 最大 | 平均 | 中央値 | |
---|---|---|---|---|
家賃 | 15000 | 754000 | 49717.28 | 47000 |
専有面積 | 3.6 | 67.04 | 23.56 | 23.44 |
築年数 | 0 | 99 | 22.59 | 24 |
階数 | 1 | 30 | 6.57 | 5 |
階 | 1 | 28 | 4.10 | 3 |
これだけ絞ったら相場が低くなってきましたね。
ざっくりいうと福岡市で一人暮らしするなら家賃5万以下は簡単に見つかりそうです。面積も20平方メートル以上。
分布のヒストグラム
次はヒストグラムで各値の分布を調べてみます。全体の場合と一人暮らし用の間取りの場合両方一緒に表記します。
plt.figure(figsize=[6,9],dpi=100)
plt.subplot(511,xlim=[15000,200000])
plt.xlabel('家賃(円)',family='MS Gothic')
ybin = np.linspace(15000,200000,51)
plt.hist(df['家賃'],bins=ybin,ec='k')
plt.hist(df_bocchi['家賃'],bins=ybin,ec='k')
plt.legend(['全部','ワンルーム/1K/1DKだけ'],prop={'family':'MS Gothic'})
plt.subplot(512,xlim=[10,100])
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
mbin = np.linspace(10,100,46)
plt.hist(df['専有面積'],bins=mbin,ec='k')
plt.hist(df_bocchi['専有面積'],bins=mbin,ec='k')
plt.subplot(513,xlim=[-0.5,60.5])
plt.xlabel('築年数',family='MS Gothic')
cbin = np.arange(-0.5,60.5)
plt.hist(df['築年数'],bins=cbin,ec='k')
plt.hist(df_bocchi['築年数'],bins=cbin,ec='k')
plt.subplot(514,xlim=[0.5,df['階数'].max()+0.5])
plt.xlabel('階数',family='MS Gothic')
kbin = np.arange(0.5,df['階数'].max()+0.5)
plt.hist(df['階数'],bins=kbin,ec='k')
plt.hist(df_bocchi['階数'],bins=kbin,ec='k')
plt.subplot(515,xlim=[0.5,df['階'].max()+0.5])
plt.xlabel('階',family='MS Gothic')
kbin = np.arange(0.5,df['階'].max()+0.5)
plt.hist(df['階'],bins=kbin,ec='k')
plt.hist(df_bocchi['階'],bins=kbin,ec='k')
plt.tight_layout()
plt.savefig('ymck_hist.png')
plt.close()
結果から見ると、新築(築年数が0)が多いのは目立ちます。建てられたばかりの建物はいっぱいありますね。福岡は人口が急速に増えていく傾向にあるので、新しい建物はどんどん出てくるでしょうね。
階数は1階建てが殆どなく、2階建ては一番多いです。だから2階には部屋も一番多いでいしょう。1階の部屋も多いのは、多数ある2階建てや3階建てで1階住む部屋も多いからでしょう。
データ分析と可視化
次はデータ分析をして色々気になるところを調べて可視化して行きます
数字でないデータ
「建物種類」別
SUUMOでは建物種類が5つに分けられているらしいです。
- アパート
- マンション
- 一戸建て
- テラス・タウンハウス
- その他
それぞれ違う特徴を持っているので、個別として数字データの分布を見てみたいです。
ではpandasの便利なgroupby
メソッドを使って関数を作って建物種類別で数字データの中央値を纏めるデータフレームを作ります。
def f_ymck(r):
sr = pd.Series()
sr['数'] = len(r)
sr['家賃'] = int(r['家賃'].median())
sr['専有面積'] = int(r['専有面積'].median())
sr['築年数'] = int(r['築年数'].median())
sr['階'] = int(r['階'].median())
return sr
df_ymck_shurui = df.groupby('建物種類').apply(f_ymck)
df_ymck_shurui_bocchi = df_bocchi.groupby('建物種類').apply(f_ymck)
この纏めのデータフレームを表に纏めてみます。
全部の部屋
建物種類 | 数 | 家賃 | 専有面積 | 築年数 | 階 |
---|---|---|---|---|---|
その他 | 32 | 60000 | 34 | 23 | 2 |
アパート | 8359 | 48000 | 24 | 20 | 2 |
テラス・タウンハウス | 79 | 109000 | 65 | 1 | 1 |
マンション | 24887 | 69000 | 30 | 18 | 4 |
一戸建て | 215 | 140000 | 83 | 18 | 1 |
単身の部屋
建物種類 | 数 | 家賃 | 専有面積 | 築年数 | 階 |
---|---|---|---|---|---|
その他 | 10 | 35500 | 19 | 31 | 2 |
アパート | 5131 | 40000 | 21 | 23 | 2 |
テラス・タウンハウス | 3 | 46000 | 21 | 17 | 2 |
マンション | 12021 | 52000 | 24 | 25 | 4 |
一番多いのはアパートとマンションで、特に単身の場合は殆どアパートとマンションしかない。
アパートとマンションと一戸建ての家賃の分布の違いも見てみましょう。
yachin_bin = np.arange(14000,400001,4000)
plt.figure(figsize=[6,7],dpi=100)
for i,shurui in enumerate(['アパート','マンション','一戸建て']):
plt.subplot(311+i)
plt.title(shurui,family='MS Gothic')
plt.hist(df.loc[df['建物種類']==shurui,'家賃'],bins=yachin_bin,ec='k')
plt.hist(df_bocchi.loc[df_bocchi['建物種類']==shurui,'家賃'],bins=yachin_bin,ec='k')
if(i==0):
plt.legend(['全部','ワンルーム/1K/1DKのみ'],prop={'family':'MS Gothic'})
plt.xlabel('家賃(円)',family='MS Gothic')
plt.tight_layout()
plt.savefig('yachin_shurui_hist.png')
plt.close()
マンションはアパートより高い傾向にありますが、単身の場合その差は少し小さくなりますね。どの場合でも安く住みたいならアパートを選んだ方がいいですね。
一戸建ては更に高いし、単身とは無縁のようなのでここでは触れないでおきましょう。
「区」別
福岡市には7区に分けられています。それぞれ特徴があって、住む家も違うでしょう。
一応福岡市の区の地図もここに載せておきましょう。
画像の来源と福岡市の区に関する説明はこのリンク。
市の中心は中央区ですが、一番重要な駅である博多駅は博多区にあって、私の勤務地も博多区にあるので、今回の家探しの中心は博多区です。
だから西にある西区と早良区に住むのはあまり難しいかもしれませんね。
建物種類別の時と同じように、区別で纏めてデータフレームを作ります。纏めるための関数も流用。家賃の安い順で並び替えておきます。
df_ymck_ku = df.groupby('区').apply(f_ymck).sort_values('家賃')
df_ymck_ku_bocchi = df_bocchi.groupby('区').apply(f_ymck).sort_values('家賃')
全体の部屋
区 | 数 | 家賃 | 専有面積 | 築年数 | 階 |
---|---|---|---|---|---|
城南 | 2120 | 45000 | 24 | 27 | 2 |
南 | 4416 | 55000 | 30 | 25 | 2 |
東 | 6389 | 56500 | 27 | 18 | 3 |
西 | 2396 | 58000 | 29 | 15 | 2 |
早良 | 2613 | 68000 | 33 | 23 | 2 |
中央 | 7016 | 69000 | 28 | 21 | 5 |
博多 | 8622 | 71000 | 30 | 12 | 5 |
単身の部屋
区 | 数 | 家賃 | 専有面積 | 築年数 | 階 |
---|---|---|---|---|---|
城南 | 1456 | 38000 | 21 | 28 | 2 |
東 | 3500 | 40000 | 23 | 27 | 2 |
南 | 2138 | 42000 | 22 | 29 | 2 |
早良 | 1125 | 45000 | 23 | 27 | 2 |
西 | 1283 | 48500 | 23 | 16 | 2 |
中央 | 3479 | 53000 | 23 | 25 | 4 |
博多 | 4184 | 59000 | 24 | 18 | 5 |
やはり博多区と中央区は高いです。都市の中心だから当然ですね。この辺りに住んだら便利そうですが、家賃はきついかも。
もっと詳しくわかるように分布の図も描きます。ただし7つもあって、全部別々で描くよりも纏めて描いた方がいいでしょう。一緒に描く場合棒グラフは無理なので線グラフにします。ただしmatplotlibのhist
を使ったら棒グラフになるので、ここでは代わりにnumpyのhistogram
関数を使います。
yachin_bin = np.arange(14000,114001,2000)
plt.figure(figsize=[6,6],dpi=100)
plt.subplot(211)
plt.title('全部',family='MS Gothic')
for ku,df_ku in df.groupby('区'):
hist = np.histogram(df_ku['家賃'],bins=yachin_bin)[0]
plt.plot((yachin_bin[1:]+yachin_bin[:-1])/2,hist,'.-',label=ku+'区')
plt.legend(prop={'family':'MS Gothic'})
plt.subplot(212)
plt.title('ワンルーム/1K/1DKのみ',family='MS Gothic')
for ku,df_ku in df_bocchi.groupby('区'):
hist = np.histogram(df_ku['家賃'],bins=yachin_bin)[0]
plt.plot((yachin_bin[1:]+yachin_bin[:-1])/2,hist,'.-')
plt.xlabel('家賃(円)',family='MS Gothic')
plt.tight_layout()
plt.savefig('yachin_ku_plot.png')
plt.close()
線が重なってわかりにくいところもありますが、これを見て明らかなのは東区にいっぱい安い部屋があるということ!
東区は博多区の隣で、電車で博多駅へ通いやすいです。やはり東区を狙った方が一番いいと思いますね。相場は4万くらいで、3万も見つかりそう。
その他に南城区も安くていけそうですが、数が少なく、東区と比べたら博多駅へ通いやすくない。
「最寄り駅」別
次は最寄り駅のことを分析します。ただし福岡市内は駅いっぱいあるから全部持ち出すのは難しいです。試しに全部の駅を羅列してみます。
print(' '.join(sorted(df['最寄り駅'].dropna().unique())))
結果
七隈 三苫 下山門 中洲川端 九大学研都市 九産大前 井尻 今宿 伊賀 六本松 別府 千代県庁口 千早 南福岡 博多 博多南 吉塚 名島 呉服町 周船寺 和白 唐の原 唐人町 土井 大橋 大濠公園 天神 天神南 奈多 姪浜 室見 春日 春日原 東比恵 柚須 桜坂 梅林 橋本 櫛田神社前 次郎丸 渡辺通 祇園 福大前 福岡空港 福工大前 竹下 笹原 箱崎 箱崎九大前 箱崎宮前 舞松原 茶山 薬院 薬院大通 藤崎 西戸崎 西新 西鉄三苫 西鉄千早 西鉄平尾 西鉄福岡(天神) 西鉄香椎 西鉄高宮 貝塚 賀茂 赤坂 野芥 金山 雁ノ巣 雑餉隈 香椎 香椎宮前 香椎神宮 香椎花園前 馬出九大病院前 高宮
このように76駅もあります。やはり多い。
ちなみにその中で福岡市内ではない駅も含まれます。春日駅と春日原駅は隣町である春日市にあるが、福岡市との境界線に近いです。
その中でここでは一番重要な鹿児島本線だけ見ていきます。
鹿児島本線は福岡県から熊本県を通して鹿児島県までの長い路線ですが、福岡市内には10駅あります。
駅名 | 博多駅から | 快速 | 区 |
---|---|---|---|
福工大前駅 | -13.1 | ● | 東区 |
九産大前駅 | -9.9 | | | |
香椎駅 | -8.4 | ● | |
千早駅 | -7.2 | ● | |
箱崎駅 | -3.2 | | | |
吉塚駅 | -1.8 | ● | 博多区 |
博多駅 | 0 | ● | |
竹下駅 | +2.7 | ▲ | |
笹原駅 | +5.1 | ▲ | 南区 |
南福岡駅 | +6.7 | ● | 博多区 |
私の目的は博多駅に通うことだからここでは博多駅との結びを重視します。
実際に春日市にある春日駅や大野城市にある大野城駅と水城駅や太宰府市にある都府楼南駅も博多駅から遠くなくて通いやすいらしいけど、福岡市内ではないのでここでは割愛します。
では駅別の纏めのデータフレームを作ります。
eki_lis = ['福工大前','九産大前','香椎','千早','箱崎','吉塚','博多','竹下','笹原','南福岡']
retsu_lis = ['数','家賃','専有面積','築年数','階数','階']
eki_betsu_dic = {x: [] for x in retsu_lis}
eki_betsu_bocchi_dic = {x: [] for x in retsu_lis}
for eki in eki_lis:
df_eki = df[df['最寄り駅']==eki]
df_eki_bocchi = df_bocchi[df_bocchi['最寄り駅']==eki]
eki_betsu_dic['数'].append(len(df_eki))
eki_betsu_bocchi_dic['数'].append(len(df_eki_bocchi))
for retsu in retsu_lis[1:]:
eki_betsu_dic[retsu].append(int(df_eki[retsu].median()))
eki_betsu_bocchi_dic[retsu].append(int(df_eki_bocchi[retsu].median()))
df_ensen = pd.DataFrame(eki_betsu_dic,index=eki_lis)
df_ensen_bocchi = pd.DataFrame(eki_betsu_bocchi_dic,index=eki_lis)
全部の部屋
駅 | 数 | 家賃 | 専有面積 | 築年数 | 階数 | 階 |
---|---|---|---|---|---|---|
福工大前 | 366 | 38000 | 22 | 31 | 2 | 2 |
九産大前 | 775 | 38000 | 24 | 29 | 3 | 2 |
香椎 | 410 | 53000 | 24 | 20 | 3 | 2 |
千早 | 128 | 49500 | 27 | 19 | 4 | 3 |
箱崎 | 702 | 61000 | 27 | 16 | 6 | 3 |
吉塚 | 694 | 70750 | 33 | 8 | 8 | 4 |
博多 | 1744 | 73250 | 30 | 17 | 11 | 6 |
竹下 | 1085 | 70600 | 34 | 8 | 5 | 3 |
笹原 | 571 | 64000 | 27 | 18 | 3 | 2 |
南福岡 | 232 | 64000 | 27 | 4 | 4 | 2 |
単身の部屋
駅 | 数 | 家賃 | 専有面積 | 築年数 | 階数 | 階 |
---|---|---|---|---|---|---|
福工大前 | 291 | 35000 | 21 | 31 | 2 | 2 |
九産大前 | 650 | 35000 | 23 | 30 | 3 | 2 |
香椎 | 281 | 37000 | 22 | 28 | 2 | 2 |
千早 | 65 | 37000 | 22 | 28 | 3 | 3 |
箱崎 | 353 | 46000 | 23 | 26 | 5 | 3 |
吉塚 | 284 | 52000 | 23 | 17 | 5 | 3 |
博多 | 951 | 64000 | 24 | 21 | 11 | 5 |
竹下 | 400 | 48000 | 23 | 22 | 3 | 2 |
笹原 | 343 | 45800 | 25 | 17 | 3 | 2 |
南福岡 | 135 | 38000 | 23 | 25 | 3 | 2 |
わかりやすいように全部グラフにします。
xlim = [-0.5,9.5]
xtick = range(len(eki_lis))
plt.figure(figsize=[5,7],dpi=120)
plt.subplot(311,xlim=xlim,xticks=xtick,xticklabels=[])
plt.ylabel('数',family='MS Gothic')
plt.bar(xtick,eki_betsu_dic['数'],1,ec='k')
plt.bar(xtick,eki_betsu_bocchi_dic['数'],1,ec='k')
plt.legend(['全部','ワンルーム/1K/1DKのみ'],prop={'family':'MS Gothic'})
plt.grid(ls='--')
plt.subplot(312,xlim=xlim,xticks=xtick,xticklabels=[])
plt.ylabel('家賃(円)',family='MS Gothic')
plt.plot(eki_betsu_dic['家賃'],'.-')
plt.plot(eki_betsu_bocchi_dic['家賃'],'.-')
plt.grid(ls='--')
ax = plt.subplot(313,xlim=xlim)
plt.ylabel('専有面積($m^2$)',family='MS Gothic')
ax.set_xticks(xtick,['\n'.join(list(e)) for e in eki_lis],family='MS Gothic')
plt.plot(eki_betsu_dic['専有面積'],'.-')
plt.plot(eki_betsu_bocchi_dic['専有面積'],'.-')
plt.grid(ls='--')
plt.tight_layout()
plt.savefig('ensen_plot1.png')
plt.close()
plt.figure(figsize=[5,7],dpi=120)
c_lis = ['築年数','階数','階']
for i in range(3):
ax = plt.subplot(311+i,xlim=xlim,xticks=xtick,xticklabels=[])
plt.ylabel(c_lis[i],family='MS Gothic')
plt.plot(eki_betsu_dic[c_lis[i]],'.-')
plt.plot(eki_betsu_bocchi_dic[c_lis[i]],'.-')
plt.grid(ls='--')
ax.set_xticks(xtick,['\n'.join(list(e)) for e in eki_lis],family='MS Gothic')
plt.legend(['全部','ワンルーム/1K/1DKのみ'],prop={'family':'MS Gothic'})
plt.tight_layout()
plt.savefig('ensen_plot2.png')
plt.close()
思った通り博多駅は一番家賃が高くて、離れるほど安くなります。
階数もやはり博多駅なら高いビルばかりですね。
築年数はなぜか南の方に行くと新しくなります。
博多駅の近くの駅なら便利で通勤の時間と費用も省けるけど、家賃は高い。決断をする時にちゃんとメリットとデメリットを考えなければならないですね。
家賃の分布を見てみましょう。
yachin_bin = np.arange(14000,114001,2000)
plt.figure(figsize=[6,9],dpi=100)
ax1 = plt.subplot(211)
ax2 = plt.subplot(212)
ax1.set_title('全部',family='MS Gothic',size=10)
ax2.set_title('ワンルーム/1K/1DKのみ',family='MS Gothic',size=10)
for eki in eki_lis:
hist1 = np.histogram(df.loc[df['最寄り駅']==eki,'家賃'],bins=yachin_bin)[0]
ax1.plot((yachin_bin[1:]+yachin_bin[:-1])/2,hist1,'.-',label=eki+'駅')
hist2 = np.histogram(df_bocchi.loc[df_bocchi['最寄り駅']==eki,'家賃'],bins=yachin_bin)[0]
ax2.plot((yachin_bin[1:]+yachin_bin[:-1])/2,hist2,'.-',label=eki+'駅')
ax2.legend(prop={'family':'MS Gothic'})
ax2.set_xlabel('家賃(円)',family='MS Gothic')
ax1.set_ylabel('数',family='MS Gothic',rotation=0)
ax2.set_ylabel('数',family='MS Gothic',rotation=0)
plt.tight_layout()
plt.savefig('ensen_yachin_plot.png')
plt.close()
線が多くてわかりにくいが、どうやら香椎駅の辺りでは3万円より安い部屋が沢山あるらしいです。しかもこの駅は鹿児島本線と香椎線の乗り換え駅でかなり便利です。ただし4万円辺りまでなら福工大前駅と九産大前駅の方が多いから、この2つの駅の方が探しやすいかもしれません。
線グラフよりも、駅を並べて家賃を縦軸にして数を色で示した方がわかりやすいかも
plt.figure(figsize=[6,7],dpi=100)
ax1 = plt.subplot(211,xticks=xtick,xticklabels=[])
ax2 = plt.subplot(212)
clis1,clis2 = [],[]
for eki in eki_lis:
clis1.append(np.histogram(df.loc[df['最寄り駅']==eki,'家賃'],bins=yachin_bin)[0])
clis2.append(np.histogram(df_bocchi.loc[df_bocchi['最寄り駅']==eki,'家賃'],bins=yachin_bin)[0])
for ax,clis,title in zip([ax1,ax2],[clis1,clis2],['全部','ワンルーム/1K/1DKのみ']):
pcm = ax.pcolormesh(xtick,(yachin_bin[1:]+yachin_bin[:-1])/2,np.array(clis).T,cmap='inferno')
cb = plt.colorbar(pcm,pad=0.01,aspect=40)
cb.set_label('数',fontdict={'family':'MS Gothic'},rotation=0)
ax.set_title(title,family='MS Gothic',size=10)
ax.set_ylabel('家賃(円)',family='MS Gothic')
ax2.set_xticks(xtick,['\n'.join(list(e)) for e in eki_lis],family='MS Gothic')
plt.tight_layout()
plt.savefig('ensen_yachin_pcolor.png')
plt.close()
九産大前駅の辺りにいっぱい3-4万円くらいの部屋があるというところは目立ちます。
駅から歩く時間と家賃の関係も見てみます。今回は単身の場合だけ描きます。線グラフと色プロット両方を同時に描きます。
plt.figure(figsize=[6,9],dpi=100)
plt.subplot2grid((3,1),(0,0),rowspan=2)
df_ho = pd.DataFrame(index=range(1,31),columns=eki_lis)
for eki in eki_lis:
sr_ho = df_bocchi.groupby('歩').apply(lambda r: r.loc[r['最寄り駅']==eki,'家賃'].median()).dropna()
plt.plot(sr_ho,'.-',label=eki+'駅')
df_ho[eki] = sr_ho
plt.legend(prop={'family':'MS Gothic'},ncol=2)
plt.xlabel('駅から歩く時間(分)',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
plt.subplot2grid((3,1),(2,0),facecolor='#CCCCCC')
plt.pcolormesh(xtick,range(1,31),df_ho,cmap='inferno')
cb = plt.colorbar(pad=0.01,aspect=40)
cb.set_label('家賃(円)',fontdict={'family':'MS Gothic'})
plt.xticks(xtick,['\n'.join(list(e)) for e in eki_lis],family='MS Gothic')
plt.ylabel('駅から歩く時間(分)',family='MS Gothic')
plt.tight_layout()
plt.savefig('ensen_yachin_aruku_plot.png')
plt.close()
駅から遠ければ遠くほど家賃が安くなると思っていたが、実際にそこまではっきりとその傾向が見えないようです。
「間取り」別
次は間取り別で家賃の分布を見てみたいです。ここで取得間取りの情報は実は意外と多様です。試しにデータフレームの中の間取りの全種類とその数を羅列してみます。
print(' '.join(['%s(%d)'%(g,len(r)) for g,r in df.groupby('間取り')]))
10SLDK(1) 12LDK(2) 1DK(1853) 1K(10733) 1LDK(8567) 1LK(2) 1SDK(14) 1SK(182) 1SLDK(115) 1SLK(1) 2DK(1036) 2K(846) 2LDK(3081) 2SDK(20) 2SK(1) 2SLDK(121) 2SLK(1) 3DK(390) 3K(14) 3LDK(1636) 3SDK(1) 3SLDK(51) 4DK(21) 4K(2) 4LDK(237) 4SDK(1) 4SLDK(12) 5DK(6) 5LDK(26) 5SK(1) 5SLDK(2) 6DK(2) 6LDK(4) 6SLDK(6) 7K(2) 7LDK(2) 7SLDK(1) ワンルーム(4579)
全部38ありますね。その中で一個しかないものも多いですが、最も多いのは1K>1LDK>ワンルームです。
このように間取りの種類は色々ありますが、ここでは単身用のワンルームと1Kと1DK、そして一応1LDKも比べてみましょう。ついでに今回のヒストグラムは区で色を付けて分けます。
yachin_bin = np.arange(14000,114001,2000)
madori_lis = ['ワンルーム','1K','1DK','1LDK']
plt.figure(figsize=[5,10],dpi=120)
for i,madori in enumerate(madori_lis):
plt.subplot(411+i)
plt.title(madori,family='MS Gothic')
h = plt.hist([r.loc[r['間取り']==madori,'家賃'] for _,r in df.groupby('区')],bins=yachin_bin,stacked=1,ec='k')
if(i==0):
plt.legend([ku+'区' for ku,_ in df.groupby('区')],prop={'family':'MS Gothic'})
med = df.loc[df['間取り']==madori,'家賃'].median()
plt.text(med,h[0].max(),' %d'%med,va='bottom')
plt.axvline(med,ls='--',color='k')
plt.ylim([0,h[0].max()*1.11])
plt.xlabel('家賃(円)',family='MS Gothic')
plt.tight_layout()
plt.savefig('yachin_madori_hist.png')
plt.close()
破線と数字は中央値を示します。
ワンルームと1Kはあまり大差ないが、1K、1DK、1LDKの順で相場は高くなっていくらしい。これは面積の違いが大きな原因だと思えます。
では面積の分布も見てみます。
plt.figure(figsize=[5,10],dpi=120)
for i,madori in enumerate(madori_lis):
plt.subplot(411+i)
plt.title(madori,family='MS Gothic')
h = plt.hist([r.loc[r['間取り']==madori,'専有面積'] for _,r in df.groupby('区')],bins=np.linspace(10,50,41),stacked=1,ec='k')
ku_lis = [ku+'区' for ku,_ in df.groupby('区')]
if(i==0):
plt.legend(ku_lis,prop={'family':'MS Gothic'})
med = df.loc[df['間取り']==madori,'専有面積'].median()
plt.ylim([0,h[0].max()*1.11])
plt.text(med,h[0].max(),' %d'%med,va='bottom')
plt.axvline(med,ls='--',color='k')
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
plt.tight_layout()
plt.savefig('menseki_madori_hist.png')
plt.close()
思った通り面積の分布も家賃の分布と似ています。
でももし同じくらの面積で考えたら家賃はどう違うのかこれも気になるので、pandasのcutで面積を分けて各段の家賃のグラフを描いてみます。比較のため分布のグラフも一緒に描きます。
q_menseki = pd.cut(df_bocchi['専有面積'],np.arange(15,41,1))
x = q_menseki.values.categories.mid
g = df_bocchi.groupby(q_menseki,observed=True)
plt.figure(figsize=[6,6],dpi=120)
plt.subplot(211)
plt.ylabel('数',family='MS Gothic')
for i,madori in enumerate(madori_lis):
plt.plot(x,g.apply(lambda r: (r['間取り']==madori).sum()),'.-')
plt.legend(madori_lis,prop={'family':'MS Gothic'})
plt.grid(ls=':')
plt.subplot(212)
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
for i,madori in enumerate(madori_lis):
plt.plot(x,g.apply(lambda r: r.loc[r['間取り']==madori,'家賃'].median()),'.-')
plt.grid(ls=':')
plt.tight_layout()
plt.savefig('yachin_menseki_madori_plot.png')
plt.close()
ワンルームと1Kはあまり変わらないが、1DKは少し安く見えます。つまり1DKの方が高い理由は広い傾向にあるからだけで、実際に同じ専有面積で比較したら1DKの方が安いらしいです。
同じ専有面積でも間取りによってサイズ感は違ってワンルームは区切りが少なくて一番広く感じるでしょう。
各数字データの関係
面積が大きくなると当然家賃は高くなるでしょう。それをはっきり見えるように専有面積と家賃の関係を説明する散布図を描いてみます。おまけに点の色は築年数にします。
plt.figure(figsize=[6,5],dpi=100)
plt.axes(xlim=[10,50],ylim=[0,150000])
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
plt.scatter(df_bocchi['専有面積'],df_bocchi['家賃'],c=df_bocchi['築年数'],s=3,cmap='rainbow')
cb = plt.colorbar(pad=0.01,aspect=40)
cb.set_label('築\n年\n数',fontdict={'family':'MS Gothic'},rotation=0)
plt.grid(ls=':')
plt.tight_layout()
plt.savefig('yachin_menseki_chiku_bunpu.png')
plt.close()
思った通りの結果ですね。それに築年数が低い方は家賃が高いという傾向も見られます。これも当然のことですね。
次は建物の階数と家賃の関係も見ましょう。色は部屋の階にします。
plt.figure(figsize=[5,5],dpi=120)
plt.axes(ylim=[0,150000])
plt.xlabel('階数',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
plt.scatter(df_bocchi['階数'],df_bocchi['家賃'],c=df_bocchi['階'],s=25,alpha=0.3,marker='_',cmap='rainbow')
yachin_kaisuu = df_bocchi.groupby('階数').apply(lambda r:r['家賃'].median())
plt.plot(yachin_kaisuu.index,yachin_kaisuu.values,'.-',lw=0.5,color='k',ms=3)
cb = plt.colorbar(pad=0.01,aspect=40)
cb.set_label('階',fontdict={'family':'MS Gothic'},rotation=0)
plt.tight_layout()
plt.savefig('yachin_kaisuu_bunpu.png')
plt.close()
黒い線は各階数での中央値です。建物が高くなると家賃も高くなるという傾向が見られますね。これは高いビルが都心の近くに集中しているからでしょう。ただし15階以上になると数が少なすぎてあまり当てにならないみたいです。
ところで普段に考えると部屋の階は建物の階数より高いのはあり得ないはずですが、念のためにちょっと関係の散布図も描いてみましょう。
from matplotlib.colors import LogNorm
plt.figure(figsize=[6,5],dpi=100)
plt.axes(aspect=1)
plt.xlabel('階数',family='MS Gothic')
plt.ylabel('階',family='MS Gothic')
df_kai_kaisuu_n = df_bocchi.groupby(['階','階数']).apply(len).reset_index()
plt.scatter(df_kai_kaisuu_n['階数'],df_kai_kaisuu_n['階'],c=df_kai_kaisuu_n[0],s=8,cmap='winter',norm=LogNorm())
cb = plt.colorbar(pad=0.01,aspect=40)
cb.set_label('数',fontdict={'family':'MS Gothic'},rotation=0)
plt.plot([0,30],[0,30],'--k')
plt.tight_layout()
plt.savefig('kaisuu_kai_bunpu.png')
plt.close()
意外なことに、階数より高い階にある部屋が稀だけど本当にあります!これはどういうことでしょう?単に入力ミスかもしれません。
階と家賃の関係も見てみます。今回は色でアパートとマンションを区別します。
plt.figure(figsize=[5,5],dpi=120)
plt.axes(ylim=[0,150000],xticks=range(1,21,2))
plt.xlabel('階',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
for shurui in ['アパート','マンション']:
jouken = (df_bocchi['階']<21)&(df_bocchi['建物種類']==shurui)
plt.scatter(df_bocchi[jouken]['階'],df_bocchi[jouken]['家賃'],s=25,alpha=0.3,marker=int(shurui=='アパート'))
yachin_kai = df_bocchi[df_bocchi['階']<21].groupby('階').apply(lambda r:r['家賃'].median())
plt.plot(yachin_kai.index,yachin_kai.values,'.-',lw=0.2,color='k',ms=2)
plt.legend(['アパート','マンション','中央値'],prop={'family':'MS Gothic'})
plt.tight_layout()
plt.savefig('yachin_kai_bunpu.png')
plt.close()
やはり高いところにある部屋の方は家賃が高いですね。そして高いのは殆どマンション。
階数を分けてアパートとマンションでの家賃の分布のヒストグラムも描いてみます。
dfx = df_bocchi.copy()
dfx.loc[dfx['階']>10,'階'] = 10
plt.figure(figsize=[6,6],dpi=100)
for shurui in ['アパート','マンション']:
plt.subplot(211+(shurui=='マンション'))
plt.title(shurui,family='MS Gothic')
plt.hist([r[r['建物種類']==shurui]['家賃'] for _,r in dfx.groupby('階')],bins=yachin_bin,stacked=1,ec='k')
plt.legend(['%d階'%i for i in range(1,10)]+['≥10階'],prop={'family':'MS Gothic'})
plt.xlabel('家賃(円)',family='MS Gothic')
plt.tight_layout()
plt.savefig('yachin_kai_hist.png')
plt.close()
拘り条件別の家賃分布
次は各拘り条件がどのように家賃に影響を与えるか調べたいです。
このコードで全部の拘り上品の有無別で分布のヒストグラムを描きます。今回は単身の場合だけ見て、色は3種の間取りで分けます。
madori_lis = list(df_bocchi.groupby('間取り').groups.keys())
jouken_lis = ['バス・トイレ別','温水洗浄便座','室内洗濯機置場','洗面所独立','ロフト','家具家電付き','宅配ボックス','敷地内ゴミ置場','バルコニー付','都市ガス']
for jouken in jouken_lis:
plt.figure(figsize=[5,5],dpi=120)
plt.subplot(211)
plt.title(jouken,family='MS Gothic')
h = plt.hist([r[r[jouken]==1]['家賃'] for _,r in df_bocchi.groupby('間取り')],bins=yachin_bin,stacked=1,ec='k')
plt.legend(madori_lis,prop={'family':'MS Gothic'})
med = df_bocchi['家賃'][df_bocchi[jouken]==1].median()
plt.text(med,h[0].max(),med,va='bottom')
plt.axvline(med,ls='--',color='k')
plt.ylim([0,h[0].max()*1.11])
plt.subplot(212)
plt.title('無い',family='MS Gothic')
h = plt.hist([r[r[jouken]==0]['家賃'] for _,r in df_bocchi.groupby('間取り')],bins=yachin_bin,stacked=1,ec='k')
med = df_bocchi['家賃'][df_bocchi[jouken]==0].median()
plt.text(med,h[0].max(),med,va='bottom')
plt.axvline(med,ls='--',color='k')
plt.xlabel('家賃(円)',family='MS Gothic')
plt.ylim([0,h[0].max()*1.11])
plt.tight_layout()
plt.savefig('yachin_%s_hist.png'%jouken)
plt.close()
次はできた結果を見ていきます。
バス・トイレ別
バスとトイレが別だとやはり家賃が高い傾向にありますね。
一人暮らしだとバスとトイレが一緒の方が便利のはずで、しかも区切りが少ないと広く見えて、その上に家賃も安いなんて……。
ということでやはりバスとトイレを分けた部屋を選ぶ理由はあまりないですね。できればバスとトイレは一緒がいい。
ただしバスとトイレを別けた部屋の方が圧倒的に多いので、バスとトイレが一緒の部屋はあまり期待しにくくて残念です。
温水洗浄便座
温水洗浄便座があると家賃が高くなるのは明らかのようです。
私は温水洗浄便座が欲しいのでこれはかなり痛い事実です。これがないとやはりきつい。
どうしても欲しいと思ったら高い家賃を覚悟しておいた方がいいかもしれません。
室内洗濯機置場
室内に洗濯機置場があるというのも重要な条件の一つです。
洗濯機置場がないと洗濯機を使えない。そして洗濯機がないとコインランドリーを使うしかなくて、毎月の費用に大きく影響を与えます。しかも外に服を持たなければならないのはとても不便になります。
結果から見るとやはり室内に洗濯機置場があると家賃が高くなりますね。でもその分洗濯の費用を節約できるから、やはりあった方がいい気もします。
ただし「室内洗濯機置場」の条件が付いていない場合でも洗濯機置場がバルコニーにある場合もあるし、または施設内に共有の洗濯機がある場合もあるから、室内洗濯機置場がないと不便だとは限らないので、これも別として調べる必要があります。
洗面所独立
洗面所独立は洗面所がバスやトイレの中にないということです。そうなると洗面所が居室にあって便利なところもあるけど、居住スペースが狭くなります。それにトイレに洗面所があったら用を足した後すぐ洗えて、その方は便利な気がします。
何より、今回の結果から見ると明らかに洗面所独立の方は家賃が高いです。
ということで特に理由がない限りできれば洗面所独立でない方を選んだ方がいいと思います。
家具家電付き
普段の賃貸物件は殆どの家具と家電が付いていないので、自分で買うしかないですが、一部家具と家電が揃っている部屋もあるらしいです。
結果から見ると家具家電付きの部屋でも家賃が高くなるわけでもないらしくてこれはいいですね。安く家具家電を借りられるって感じ。
とはいってもデメリットもありますね。
それに数を見ると家具家電付きの部屋はたったほんの僅かなので、探すのは大変だからあまり期待しない方がいいですね。
ちなみになぜか家具家電付きの部屋は殆ど1Kばかりらしいです。
宅配ボックス
宅配ボックスがあれば不在の時でも郵便が届くので便利です。リモートワークの人ならあまり必要ないかもしれませんが、平日毎日通勤する普通の人にとってやはりあった方がいいでしょう。
でも宅配ボックスを持つ物件の方は明らかに家賃が高いのでこれもよく検討しなければなりませんね。
敷地内ゴミ置場
敷地内にゴミ置場があると便利ですね。しかも結果から見れば、あっても家賃が高いわけではないようです。
だからできれば敷地内ゴミ置場がある物件を選びたいですね。
バルコニー付
SUUMOではバルコニーとベランダは一概にして「バルコニー」と称するらしいです。実際にSUUMOで「バルコニー付」という条件の部屋は殆ど「ベランダ付」という意味で、本当にバルコニーであるものは少ないらしいです。
それでヒストグラムから見ると意外なことにバルコニー付の方は少し家賃が安いらしいです。
私もバルコニー(ベランダ含む)はあった方がいいと思いますし、バルコニー付の物件の数の方が圧倒的に多くて探しやすいから、特別な理由がない限りわざわざバルコニーのない部屋を選ぶ理由はあまりないと思いますね。
都市ガス
ガス代は、家賃に加えて毎月払わなければならない費用の一つです。そして主に都市ガスとプロパンガスに分けられていますが、一般にいうと都市ガスの方が安いらしいです。
そして結果から見ると都市ガスかどうかは殆ど家賃に影響はないらしいです。
家賃が同じなら都市ガスの方が有利なので、都市ガスを選ばない理由はないでしょう。
ロフト
福岡ではロフト付きの部屋が多いらしいです。
検索したらこういう統計的纏めも見つけました。
ロフトはバルコニーと同様に専有面積に含まれないので、あればスペースが増えるとも言えるようです。
ただしデメリットとも多いらしいです。
ただし分析の結果、ロフト付きの方は家賃が安いようです。
私は主に家賃が安いところで検索しているから、見つけた半分くらいの部屋はロフト付きです。
ロフトは要らないと思いますが、安いのであれば別にロフト付きでもいいかもしれません。
ロフトと専有面積と家賃
なぜロフト付きは家賃が安いのか気になるので、もっと分析したいです。
まず考えられるのは面積のことですね。ロフトの方が専有面積が少ない傾向にあるはずだから、そう考えてロフトの有無別で専有面積と家賃の散布図を描いてみます。
plt.figure(figsize=[5,5],dpi=120)
plt.axes(xlim=[10,40],ylim=[0,120000])
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
rofuto = df_bocchi['ロフト']==1
plt.scatter(df_bocchi.loc[~rofuto,'専有面積'],df_bocchi.loc[~rofuto,'家賃'],c='b',s=3,alpha=0.2)
plt.scatter(df_bocchi.loc[rofuto,'専有面積'],df_bocchi.loc[rofuto,'家賃'],c='r',s=3,alpha=0.2)
plt.legend(['ロフト無し','ロフト付き'],prop={'family':'MS Gothic'})
plt.grid(ls=':')
plt.tight_layout()
plt.savefig('yachin_menseki_rofuto_bunpu.png')
plt.close()
予想通りロフト付き部屋は面積少ない方に集まっています。
ただもし面積が同じくらいならロフト付きの方は安いのか、この散布図からははっきりわからないので、わかりやすいように専有面積を数段に分けて線グラフを描きます。
q_menseki = pd.cut(df_bocchi['専有面積'],np.arange(15,40,1))
x = q_menseki.values.categories.mid
g = df_bocchi.groupby(q_menseki,observed=True)
y_rofuto = g.apply(lambda r: r.loc[r['ロフト']==1,'家賃'].median())
y_nashi = g.apply(lambda r: r.loc[r['ロフト']==0,'家賃'].median())
rofuto_wariai = g.apply(lambda r: (r['ロフト']==1).mean())
plt.figure(figsize=[6,6],dpi=120)
plt.subplot(211)
plt.ylabel('ロフト付き割合',family='MS Gothic')
plt.plot(x,rofuto_wariai,'.-g')
plt.grid(ls=':')
plt.subplot(212)
plt.xlabel('専有面積($m^2$)',family='MS Gothic')
plt.ylabel('家賃(円)',family='MS Gothic')
plt.plot(x,y_nashi,'.-b')
plt.plot(x,y_rofuto,'.-r')
plt.legend(['ロフト無し','ロフト付き'],prop={'family':'MS Gothic'})
plt.grid(ls=':')
plt.tight_layout()
plt.savefig('rofuto_plot.png')
plt.close()
結果から見ると、同じくらいの専有面積で見てもやはりロフト付きの方が安いらしいです。
機械学習による予測モデル
最後にやっと機械学習の出番です。
以上説明した通り、家賃に影響を与える要因は沢山あります。それらの要因を分析することで家賃を予測できます。
ここでは機械学習でそのための予測モデルを作ってみます。
ランダムフォレスト
予測モデルを作る方法は色々ありますが、今回は簡単で高速のランダムフォレストという方法を選びました。
ランダムフォレストに関してここでは説明しませんが、参考のリンクを読んでください。
私も昔ランダムフォレストに関する記事も書いたことがあります。これも読んで欲しいです。
今回は一番簡単にランダムフォレストを実装できるsklearnを使います。
特徴量の準備
まずは分析する特徴量を準備する必要があります。
家賃を除き、数字のデータは5つ
- 専有面積
- 築年数
- 階数
- 階
- 歩
これらはそのまま特徴量に使えます。
ただし「歩」(駅から歩く時間)は一部NaNになっている(最寄り駅がない場合)のでこの場合100という大きい数字に入れ替えます。
また拘り条件のデータも0と1という値になっているので、そのまま使うこともできます。
問題は残り4つの数字でないデータです。
- 間取り
- 建物種類
- 最寄り駅
- 区
これらを使うためにこのように書き換えます
- 間取りは「〇〇」かどうか
- 建物種類は「〇〇」かどうか
- 最寄り駅は「〇〇」かどうか
- 「〇〇」区にあるかどうか
これも拘り条件と同じように0と1の数字にします。
建物種類は「5」、最寄り駅は「77」(NaNも含む)、区は「7」なので、「38+5+77+7 = 126」も特徴量の数が増えます。最初の「5+10」に合わせて、特徴量の数は全部「142」になります。
なお、実はハイパーパラメータ調整やトレイン・テストの分裂もやるべきことですが、ここでは割愛します。
学習と予測
次は実装します。まずモデルを作って学習させて、その学習データで予測してみましょう。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from sklearn.ensemble import RandomForestRegressor as Rafore
df = pd.read_csv('suumo_df.csv')
# 近くに駅がない場合、歩く時間が100分にする
df.loc[df['歩'].isnull(),'歩'] = 100
# 追加の特徴量を入れる辞書
f_dic = {}
madori_lis = list(df['間取り'].unique())
for madori in madori_lis:
f_dic[madori] = (df['間取り']==madori).astype(int)
shurui_lis = list(df['建物種類'].unique())
for shurui in shurui_lis:
f_dic[shurui] = (df['建物種類']==shurui).astype(int)
ku_lis = [ku+'区' for ku in df['区'].unique()]
for ku in ku_lis:
f_dic[ku] = (df['区']+'区'==ku).astype(int)
eki_lis = [eki+'駅' for eki in df['最寄り駅'].dropna().unique()]
for eki in eki_lis:
f_dic[eki] = (df['最寄り駅']+'駅'==eki).astype(int)
# 最寄り駅がな場合
eki_lis += ['駅から遠い']
f_dic['駅から遠い'] = df['最寄り駅'].isnull().astype(int)
# 追加の特徴量をデータフレームに入れる
df = pd.concat([df,pd.DataFrame(f_dic)],axis=1)
jouken_lis = ['バス・トイレ別','温水洗浄便座','室内洗濯機置場','洗面所独立','ロフト','家具家電付き','宅配ボックス','敷地内ゴミ置場','バルコニー付','都市ガス']
# 特徴量に使う全部の列のリスト
f_lis = ['専有面積','築年数','階数','階','歩']+jouken_lis+shurui_lis+madori_lis+ku_lis+eki_lis
y = df['家賃'].values # 予測したい場合
X = df[f_lis].values # 分析に使う特徴量
# 予測のためのランダムフォレストのモデル
rafore = Rafore(n_estimators=100,n_jobs=-1)
# 学習開始
rafore.fit(X,y)
# 学習終了後、学習テータで予測してみる
yy = rafore.predict(X)
# 予測と本当の値を比較する散布図
plt.figure(figsize=[6,6.5],dpi=100)
plt.axes(xlim=[0,400000],ylim=[0,400000],aspect=1)
plt.xlabel('本当の家賃',family='MS Gothic')
plt.ylabel('予測の家賃',family='MS Gothic')
plt.plot([0,400000],[0,400000],'--k')
plt.scatter(y,yy,c=yy/y,s=2,cmap='winter_r',norm=LogNorm())
cb = plt.colorbar(pad=0.01,aspect=40,location='top')
cb.set_label('予測の家賃/本当の家賃',fontdict={'family':'MS Gothic'})
plt.tight_layout()
plt.savefig('yosoku_bunpu.png')
plt.close()
ここでは全部学習データをそのまま予測したから、殆どは予測値し実際の値が近いですね。それでも少し外れ値があります。
左上にあるのは家賃を実際よりも高く予測されたものです。つまりこのモデルなりに考えて高いはずなのに実際に安い。ということで注目すべきはこの辺りの物件ですね。値段のわりにいい物件である可能性が高いです。
ただし勿論ここではまだ考慮していない要因もあるので、その思いがけない要因のせいで安くなって実際にあまりよくないという可能性もあるので、後でちゃんとその物件の詳細を調べる必要があります。
外れ値の分布
どれくらい外れ値があるのか散布図だけではわかりにくいので、ヒストグラムを描きます。
plt.figure(figsize=[6,4.5],dpi=100)
plt.hist(np.log10(yy/y),bins=50,ec='k')
plt.xlabel('予測の家賃/本当の家賃',family='MS Gothic')
ax = plt.gca()
xtick = ax.get_xticks()
ax.set_xticks(xtick,['%.2f'%(10**t) for t in xtick])
plt.semilogy()
plt.grid(axis='y',ls='--')
plt.tight_layout()
plt.savefig('yosoku_err_hist.png')
plt.close()
特徴量の順位
ランダムフォレストの長所の一つとして、「特徴量の重要さを判断できる」ということです。
sklearnの場合、その特徴量の重要さはモデルオブジェクトの.feature_importances_
に入っています。ここで順番に並べて10番まで表示してみます。
f_sr = pd.Series(rafore.feature_importances_,index=f_lis).sort_values(ascending=0)
print(f_sr[:10])
結果
専有面積 0.637072
築年数 0.149224
階数 0.053593
中央区 0.033448
階 0.025710
12LDK 0.022197
歩 0.020340
薬院駅 0.003648
早良区 0.002695
敷地内ゴミ置場 0.002691
重要さの数字は全部合わせたら1となります。高いほど重要ということです。
やはり一番重要なのは「専有面積」ですね。これは当然ですね。次は「築年数」と「階数」で、どれも数字のデータです。
そして次は数字でない「中央区」、つまり中央区にあるかどうによって予測の様子は大きく変わるということです。
12LDKがランキングに出てきたのは意外ですが、これも必ずわけあるはずです。とりあえず調べられます。
print(df[df['間取り']=='12LDK']['家賃'])
そして結果から見ると、12LDKの部屋は2つしかなくて、どれも家賃は2,300,000円です。そして実はこの数字は家賃の最大値です。だから「12LDKだととりあえず高い」という判断基準になって大きく影響を与えるでしょう。
ちなみに12LDKってどんな物件か気になって調べてみたらこうです。
大家族で暮らす家ですね。どれにせよ自分とは無縁そう。
纏め
分析の結果から色々わかってきたからここに纏めます。
- 福岡市全体では一人暮らしの部屋の相場は大体47000-50000円くらいで専有面積は大体23m2程度
- 東区は一番安い部屋を探しやすい
- 鹿児島本線で探すなら香椎駅と九産大前駅と福工大前駅の辺りがいい。相場は35000円くらい
- マンションはアパートとより家賃が高いし、高いビルにあるものも多い
- バス・トイレ一緒の方が安いが、数が少ないので探しにくい
- 温水洗浄便座と室内洗濯機置場と宅配ボックスが欲しいなら安いものを探しにくくなる
- 家具家電付きは稀で、殆どは1K
- 都市ガスの有夢は家賃に影響ないのであった方が有利
- 狭くて安い部屋はロフト付きである傾向がある
- 予測モデルの結果を見て、実際の家賃より高く予測された物件を狙えば得になるかも
私の家探しはまだ終わっていないので結局どんな部屋に住むことになるのかまだわからないのですが、少なくとも以上のデータ分析は多少参考になって役に立つでしょう。
# 2024/5/2更新:実際に引っ越しできた話はこの記事に纏めました。
参考
今回物件探しに関して色んな記事を参考にしたのでここでリンクを載せておきます。
物件探しに関して
- 引っ越しすることになったので機械学習を使って全力で自分の住む家を決めようとした話
- 【初めてのデータサイエンス⑤】友達の初物件探しをデータ分析で手伝ってみた
- スクレイピングで理想の物件探し!物件から目的地まで徒歩何分か自動取得するの巻
- スクレイピングで物件探し!その2
- scikit-learnではじめる物件探し〜その2
- SUUMOの物件情報を自動取得(スクレイピング)したのでコードを解説する。
- 機械学習初心者(?)がSUUMO賃貸データを使って行った機械学習モデルによる分析とアプリ作成でやったことをまとめた。
- 10万件以上の物件データを学習したのにクソ失礼にも家賃69万の物件に対して28万だと査定した機械学習モデルは何を考えているのか。
- 【Python】東京23区の中古マンション販売価格予測をやってみた
- 戸建て住宅マーケティングAI 開発 (5) 不動産情報サイトからの物件情報収集
- スクレイピングで不動産情報取得(SUUMO)(1)
- Kaggleに挫折したのでスクレイピング&機械学習でお得な賃貸物件探してみた
- 機械学習を使って一番安い家賃の家に住む。〜スクレイピング編〜
- 【超初心者向け】コピペで動かして楽しむPython環境構築&スクレイピング&機械学習&実用化【SUUMOでお得賃貸物件を探そう!】
- 【個人開発】爆速な賃貸物件の検索サービスを作った
決定木とランダムフォレストに関して
また、技術とは関係ないが、物件探しの経験のことで参考になったqiita記事。