akatak blog

プログラム初心者の50代ビジネスマンがセカンドキャリアを目指して働きながらPythonを中心に独学していましたが、昨年IT系企業に転職。新規事業開発の仕事をすることになりました。自らの覚え書きや成果物、感じたことなどを脈絡もなく書き連ねるブログです。

Pythonによる不動産情報のデータ取得&分析(6)【売却物件/分析編】

前回の記事から随分時間がたってしまいました。

昨年10月以降、仕事の関係で海外出張に暫く行っていたほか、現在行っている不動産分析結果を参考にもしながら年末には遂に引越しすることになりまして、片付けから新居の準備と非常に忙しく、そうこうしているうちに年明け。すっかりPythonから離れておりました。

前回記事では中途半端でありましたので、不動産売却物件編(中古マンション)について、今回で一旦完結したいと思っております。

データの基礎分析

前回の記事で前処理として加工したデータを取り込んで、基礎的な分析をしておきましょう。データは前回のものを利用しているので、半年ほど古いですが、ご容赦ください。

まずは、おまじないのごとく、いつものように必要なライブラリを取り込んでおきます。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

次に、前回加工したデータをpandas のDataFrameとして取り込みます。

df = pd.read_csv('data/suumo_used_mansion_mod_owner.csv', index_col=0)

データの概要を見ておきましょう。 f:id:akatak:20190302223111j:plain

14000件ほどのデータがあることが分かります。

前回、加工仕切れていませんでした。不動産価格や平米当りの単価が円単位だと、数値が大きくなってしまうので、万円単位に変更し、新たな列として保存しておきます。

df['価格_万円'] = df['価格']/10000
df['単価_万円'] = df['単価']/10000

さて、賃貸物件の場合と同様に、売却物件数を市区毎に比較してみましょう。 groupbyにて、市区別にデータをカウントし、件数の多い順にソートして表示してみます。

f:id:akatak:20190302225006j:plain

売却物件数と人口を散布図にプロットしたら、以下の通り。

f:id:akatak:20190302225314j:plain

賃貸物件と同様に、相関が見られますね。 次に、seabornをimportして、市区別に単価を比較してみましょう。

f:id:akatak:20190302225634j:plain

更に、築年数と平米単価をグラフ化してみます。

f:id:akatak:20190302230106j:plain

築年数毎に平均平米単価を算出してプロットしてみます。

f:id:akatak:20190302230424j:plain

築50年を超えるとデータ数の減少に伴い歪になりますが、それまでは傾向をよく表している感じですね。

次に、駅からの距離(徒歩何分か)と平均平米単価をプロットしてみましょう。 f:id:akatak:20190303075240p:plain

40分を超えた所に異常値がありそうです。 f:id:akatak:20190303085246p:plain

徒歩91分は流石に異常値なので、除外します。 f:id:akatak:20190303085636p:plain

重回帰分析

さて、ここからは重回帰分析を行っていきましょう。 まず、重回帰分析の説明変数に使うデータの種類を特定します。不動産価格に影響を与えると思われるものを取り敢えず、df_for_analysis_baseとして設定することにします。また、欠損値のあるデータを除外しておきます。

f:id:akatak:20190303091041p:plain

重回帰分析を行うためにscikit-learnライブラリをimportします。

import sklearn
from sklearn.linear_model import LinearRegression as LR

次に、予測モデル作成にあたり、

  1. LinearRegressionクラスを読み込みます。
  2. 説明変数Xに、df_for_analysis_baseから「単価万円」「部屋数」「総戸数」「路線」を除いたものを取り敢えず設定します。「単価万円」は被説明変数なので除外。「部屋数」「総戸数」は単価に殆ど影響がないため除外。「路線」を入れると「駅」と相関が高いため、多重共線性の影響により、回帰係数が異常値となっていますので、除外しています。
  3. 被説明変数yに「単価_万円」を設定します。
  4. 「市区」「駅」「手段」など、質的データをダミー化します。

f:id:akatak:20190303115115p:plain

そして、予測モデルを作成し、結果である回帰係数と切片を呼び出します。

f:id:akatak:20190303105140p:plain

決定係数は76%とまずまずのようです。この結果を見てみると、概略以下のことが言えるかと思います。

  • 築年数が1年経過すると、平米単価で約1.1万円安くなる
  • 駅からの距離が1分遠くなると、平米単価で約1.4万円安くなる
  • 物件階が1階高くなると、平米単価が約1万円高くなる
  • 市区だけでなく、最寄駅の銘柄によって結構、単価が変わる

ただし、方角については、必ずしも南向きが高くなっていないので、実感と異なる状況です。現在、統計については勉強している最中ですので、将来的にこのモデルの評価についてもう少し掘り下げて行いたい、必要があれば修正したいとは思っています。まだ、先になりそうですが。

さて、この結果をダウンロードして、Excel上で簡単に不動産価格を試算できるスプレッドシートを作成しましたので、ご参考までにアップします。ただし、モデルの検証もきちんと行っていないので、あくまで参考の域をでないこと、また、分析に利用したデータは半年以上前のものですので、ご了承ください。

drive.google.com

本日は以上です。

Pythonによる不動産情報のデータ取得&分析(5)【売却物件/データ前処理編】

akatak.hatenadiary.jp

さて、前回記載したスクリプトでSUUMOさんのサイトからスクレイピングしましたデータを、分析しやすいようにきれいにしていきましょう。このデータの前処理は、データの状況にもよりますので、なかなか完全な自動化は難しいですよね。自動化できる良い方法があれば、どなたか是非教えて下さい!

さて、例によって、Jupyter Notebookにてデータ前処理を行っていきましょう。データの状況を都度確認しながら、データ前処理を実行していくには、Jupyter Notebookは非常に有効だと思います。まだ、お使いでない方は是非試していただきたいと思います。

まず、データを取り込みましょう。pandas numpy reをインポートします。re正規表現を利用して文字列を操作するために必要なモジュールです。

前回保存しておいたcsvファイルをpandasのdataframeとしてdfに読み込みます。index_col=0として、ファイルの0列目(pythonでは0から始まる)をindex列として指定します。引数でこれを指定しないと、pandasが自動的に0から数値をindexとして割り付けてしまいます。

import pandas as pd
import numpy as np
import re

df = pd.read_csv('data/suumo_used_mansion.csv', index_col = 0)

info()にてdataframeの概要を見てみます。また、head()にて最初の5行もどんな感じか見ておきましょう。

f:id:akatak:20180915061142p:plain f:id:akatak:20180915061350p:plain

約23000行のデータ系列ですね。ところが、データ系列をExcel等に取り込んで眺めてみると分かるのですが、同じ物件のデータが場合によっては3個も4個もあるいはもっとあります。異なる業者が同じ物件をSUUMOさんのホームページに登録しているようです。

そこで重複行を削除してしまいましょう。pandasには「重複行を抽出」したり、「重複行を削除」するメソッドが準備されています。前者がduplicated()、後者がdrop_duplicates()です。

まず、duplicated()を利用して、どの位重複があるのかを確認しましょう。引数subsetで重複を確認する列を指定できます。また、keep=Falseと指定すると、重複行全てTrueを返します(keep='first'の指定で、重複行のうち最初に出てくる行がFalseとなり、それ以外がTrue。keep='last'とすると最後の行がFalseとなり、それ以外の行がTrueとなります)。sum()で合計すると重複行をカウントできます。

f:id:akatak:20180915075911p:plain

9216も重複していますね。 物件名が「オリエンタル大森」のもののみ抽出して確認すると、以下の通り。

f:id:akatak:20180915080122p:plain

重複行の最初の行のみ残して、後は削除してしまいましょう。

df.drop_duplicates(subset=['物件名', '価格', '専有面積', '間取り'], keep='first', inplace=True)

ただし、Excel等に取り込んだデータをよく見てみると物件名が「ザ・パークハウス小石川春日」と「◆ザ・パークハウス小石川春日◆2駅4路線利用可!」と異なっていますが、その他の列を見ると専有面積もバルコニー面積も価格も住所も一緒のものがあります。

完全ではないにしろ、ある程度きれいにしておきたいところですので、これらも除外しておきましょう。

f:id:akatak:20180915081308p:plain

さて、相当数を削除した結果、indexが歯抜け状態になっていますので、indexを付け直しましょう。

df.reset_index(drop=True, inplace=True)

ここで、データ系列の概要を見てみると、以下の通り、データ数は14631個になりました。

f:id:akatak:20180915081840p:plain

さて、ここから、賃貸物件と同様に、データを加工していきます。

# 文字列を複数文字で split。住所から「都県」列・「市区」列を新たに作成
df['都県'] = df['住所'].apply(lambda x: re.split('[都県]', x)[0]) 
df['市区'] = df['住所'].apply(lambda x: re.split('[都県市区]', x)[1]) 

次に、築年数を計算するために以下の加工・計算を行います。

from datetime import datetime

df['築年'] = df['築年月'].apply(lambda x: x.split('年')[0])
df['築月'] = df['築年月'].apply(lambda x: x.split('年')[1].strip('月'))
df['築月'] = df['築月'].apply(lambda x: x if x != '' else '6')

df['築年月'] = df['築年'] +'/'+ df['築月'] +'/15'  # 一律に日付を15日に設定
df['築年月'] = pd.to_datetime(df['築年月'])  #  stringからdatetimeオブジェクトに変換
df['築日数'] = df['築年月'].apply(lambda x:(datetime.today() - x).days) #今日までの日数計算
df['築年数'] = df['築日数']/365.25 # 年数(概算)を計算(うるう年も考慮)

計算の過程で作った「築年」「築月」「実日数」の列は分析でも不要なので、削除してしまいます。

df.drop(['築年','築月','築日数'], axis=1, inplace=True)

続きまして、「1億円」「1080万円」等のStringとなっているものを、数値に変換していきます。まずは、「4998万円※権利金含む」などの記載があるものの処理から。

df['価格'] = df['価格'].apply(lambda x: x.split('※')[0] if '※権利金' in x else x)

次に「億円」表示のものを「万円」に変換。また、「1億800万円」のような表示を「万円」に変換します。

df['価格'] = df['価格'].apply(lambda x: x.strip('億円') + '0000万円' if '億円' in x else x)
df['価格'] = df['価格'].apply(lambda x: str(int(x.split('億')[0]) *10000 + int(x.split('億')[1].split('万円')[0])) + '万円' if '億' in x else x)

念のため、下2文字が「万円」でないものがあるか確認すると、 f:id:akatak:20180915083628p:plain

一つだけだけありました。仕方が無いので、個別で調整します。

df.iloc[2761,2] = '2335.6万円'

後は、範囲で指定しているものもありますが、これも個別で上下の平均値でも入れておきましょう。 f:id:akatak:20180915083937p:plain f:id:akatak:20180915083947p:plain

価格の仕上げとして、数値化しておきます。

df['価格'] = df['価格'].apply(lambda x: int(float(x.strip('万円')) * 10000))

間取り、部屋数、路線、駅、手段、分は賃貸物件の場合と同様に処理します。

df['間取り'] = df['間取り'].str.replace('ワンルーム', '1RM')
df['部屋数'] = df['間取り'].apply(lambda x: int(re.search('[0-9]+', x).group(0)))
df['路線'] = df['最寄駅'].apply(lambda x: x.split('「')[0])
df['駅'] = df['最寄駅'].apply(lambda x: x.split('「')[1].split('」')[0])
df['手段'] = df['最寄駅'].apply(lambda x: x.split('「')[1].split('」')[1][0])
df['手段'] = df['手段'].apply(lambda x: x.replace('徒', '歩')) # 手段の「徒」を「歩」に変えます。
df['分'] = df['最寄駅'].apply(lambda x: re.search('[0-9]+',x.split('「')[1].split('」')[1]).group(0))

あと、今回は物件階の影響も調べたいので、加工していきましょう。 データを一覧すると f:id:akatak:20180915084551p:plain

うわー、仕方ないですね。一つ一つ変換していきましょう。

df['物件階'] = df['物件階'].str.replace('B2階','-2階')
df['物件階'] = df['物件階'].str.replace('B1階','-1階')
df['物件階'] = df['物件階'].str.replace('2-3階','2.5階')
df['物件階'] = df['物件階'].str.replace('5-6階','5.5階')
df['物件階'] = df['物件階'].str.replace('3-4階','3.5階')
df['物件階'] = df['物件階'].str.replace('B1-1階','-0.5階')
df['物件階'] = df['物件階'].str.replace('1-2階','1.5階')
df['物件階'] = df['物件階'].str.replace('4-5階','4.5階')
df['物件階'] = df['物件階'].str.replace('6-7階','6.5階')
df['物件階'] = df['物件階'].str.replace('1-3階','2階')
df['物件階'] = df['物件階'].str.replace('B1-2階','1階')
df['物件階'] = df['物件階'].str.replace('8-9階','8.5階')
df['物件階'] = df['物件階'].str.replace('1--1階','0.5階')
df['物件階'] = df['物件階'].str.replace('B1-3階','1階')
df['物件階'] = df['物件階'].str.replace('1-1階','1階')
df['物件階'] = df['物件階'].str.replace('46-47階','46.5階')
df['物件階'] = df['物件階'].str.replace('37-38階','37.5階')
df['物件階'] = df['物件階'].str.replace('B1.5階','-1.5階')
df['物件階'] = df['物件階'].str.replace('B2階','-2階')
df['物件階'] = df['物件階'].apply(lambda x: float(x.strip('階')) if x != '-' else x.replace('-', ''))

総戸数も数値に変換します。

df['総戸数'] = df['総戸数'].apply(lambda x: int(x.strip('戸')) if x !='-' else x.replace('-', ''))

リフォームは有無のみ「リフォームflag」に入れます。有り=True・無し=Falseです。

df['リフォームflag'] = df['リフォーム'] != "['-\\r\\n\\t\\t\\t']"

今回は、単価を分析の対象としたいので、追加します。

df['単価'] = df['価格'] / df['専有面積'] #単価(単位):円/㎡

権利形態としてとして各種「借地権」の物件もありますが、分析の対象は「所有権」のものに絞りたいと思います。最後に加工後のデータを保存します。

df_owner = df[df['権利形態'] == '所有権']
df_owner.to_csv('data/suumo_used_mansion_mod_owner.csv')

以上、駆け足でしたがいかがでしたか。結構骨が折れる作業かと思います。慣れてくると、あーでもないこーでもないと考えることを楽しみながら、加工できますので、作業自体はそんなに苦痛ではないですよ。

分析は次回にしましょう。それでは。

Pythonによる不動産情報のデータ取得&分析(4)【売却物件/Webスクレイピング編】

9月になりましたが、まだまだ、暑い日が続きますね。

Pythonによる不動産情報でデータ取得&分析については、賃貸物件編が一応終わりましたので、今回から売却物件編に進みたいと思います。

前回までの記事は、参考までにこちらにリンクを張っておきます。

akatak.hatenadiary.jp

akatak.hatenadiary.jp

akatak.hatenadiary.jp

売却物件編についても、Suumoさんからデータ取得を行いたいと思います。マンションの中古物件に特定して分析を行っていきますので、今回は目黒区の中古物件データを取得するために、以下のサイトの情報を取得しましょう。

# SUUMO 中古マンション 目黒区
url = 'https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=011&ta=13&jspIdFlg=patternShikugun&sc=13110&kb=1&kt=9999999&mb=0&mt=9999999&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1'

まずは、トップページの情報を取得します。

result = requests.get(url, timeout=10)
c = result.content
soup = BeautifulSoup(c, "html.parser")

続いて、ページ数を取得しましょう。ページ数に基づき、全てのページのURLのリストを作成します。

# ページ数を取得
s = soup.find("div", {"class": "pagination pagination_set-nav"})
num_pages = int(s.find_all("a")[-2].string)

# 全てのページのURLを作成
urls = []
urls.append(url)
for i in range(num_pages-1):
    pg = str(i+2)
    url_page = url + '&pn=' + pg
    urls.append(url_page)

さて、賃貸物件と同様にデータを取り込むリスト(data= [ ] )とエラーを取り込むリスト(errors = [ ])を作っておきましょう。

data = []
errors = []

ここで、各ページ毎にURLを取得し、必要なデータをスクレイピングしていきます。 エラーが起きた時のデータの位置を把握するために、kという箱を用意し、ナンバリングしていきます。また、エラーが発生した際にスクリプトを実行し続けるとともに、どこでどんなエラーが発生したのか把握するため、try〜exceptを使います。

for url in urls:
    k = 0
    try:

    # ここにエンジン部分を記載します。
        
    except Exception as e:
        errors.append([e, url, k, subtitle])
        pass

さて、ここからが本番です。エンジン部分を作っていきます。 各ページ毎にデータを取得して、物件毎のデータをunitsに保管します。

result = requests.get(url)
c = result.content
soup = BeautifulSoup(c, "html.parser")
summary = soup.find("div",{'id':'js-bukkenList'})
units = summary.find_all("div",{'class':'property_unit'})

次に、unitsに保管された物件データを一つ一つ取り出して、必要なデータを取得していきます。 賃貸物件の場合と異なり、今回は各物件のページだけですと、データが限られていますので、そのページから更に詳細なデータが記載されているページに移動してデータを取得します。ですので、以下の通り、詳細ページへのリンク、データ取得を行っておきます。

# 物件詳細情報へのリンク
h2 = unit.find('h2', {'class':'property_unit-title'})
a = h2.find('a')
href = a.get('href')
link = 'https://suumo.jp' + href + 'bukkengaiyo/'

result_child = requests.get(link, timeout=10)
c_child = result_child.content
soup_child = BeautifulSoup(c_child, "html.parser")
summary_child = soup_child.find_all('tbody', {'class':'vat tal'})

その上で、各変数にデータを取り込んでいきます。実は、ここが個別ではうまく取り込めなかったりして、テクニカルには大変なのですが、最終形だけ掲載いたします。リフォームの箇所のみうまくテキストが抽出できず、リスト形式にしていますが、ご了承ください。将来的には何とかしたいと思いますが、今はリストの中身よりも、リフォームの有無のみ取り上げたいので、中途半端ですが許容いただければと思います。

# 新着、価格更新等の情報
try:
    info = unit.find('span',{'class':'ui-label ui-label--cta1 ui-label--cta4'}).string
except:
    pass

# 物件名
subtitle = unit.find('dd',{'class':'dottable-vm'}).string

# 価格
price = unit.find('span',{'class':'dottable-value'}).string

# 住所
location = unit.find_all('dd')[2].string

# 最寄駅
station = unit.find_all('dd')[3].string

# 専有面積
area = unit.find_all('dd')[4].contents[0].split('m')[0]

# 間取り
floor_plan = unit.find_all('dd')[5].string

# バルコニー
balcony = unit.find_all('dd')[6].contents[0].split('m')[0]

# 築年月
yrs = unit.find_all('dd')[7].string

# link
link = unit.find('h2',{'class':'property_unit-title'}).a.get('href')

# 管理費
kanrihi = summary_child[0].find_all('td')[5].string.strip('\r\n\t')

# 修繕積立費
shuzenhi = summary_child[0].find_all('td')[6].string.strip('\r\n\t')

# 物件階
stair = summary_child[0].find_all('td')[14].string.strip('\r\n\t')

# 方角
direction = summary_child[0].find_all('td')[15].string.strip('\r\n\t')

# リフォーム
reform = summary_child[0].find_all('td')[16].contents

# 総戸数
total_units = summary_child[1].find_all('td')[2].string.strip('\r\n\t')

# 構造・階建て
structure = summary_child[1].find_all('td')[3].string.strip('\r\n\t')

# 権利形態
right_form = summary_child[1].find_all('td')[5].string.strip('\r\n\t')

# 用途地域
usage_district = summary_child[1].find_all('td')[6].string.strip('\r\n\t')

# 駐車場
parking = summary_child[1].find_all('td')[7].string.strip('\r\n\t')

1件ずつデータを取得して、都度リストに加えていきます。

data.append([info, subtitle, price, location, station, area, floor_plan, balcony, yrs, link, kanrihi, shuzenhi, stair, direction, reform, total_units, structure, right_form, usage_district, parking])

最終的に、dataリストをデータフレームに変換して、タイトルを付けて、csvファイルとして保存します。エラーリストも同じく保存しておきましょう。

# data listを DataFrameに変換
df = pd.DataFrame(data, columns=['情報','物件名','価格','住所','最寄駅','専有面積','間取り','バルコニー','築年月','リンク','管理費', '修繕積立費','物件階', '方角','リフォーム','総戸数', '構造・階建て','権利形態','用途地域','駐車場'])

# csvファイルとして保存
df.to_csv('Data/suumo_used_mansion.csv', sep = ',',encoding='utf-8')

# ついでに errors fileも保存
df_errors = pd.DataFrame(errors)
df_errors.to_csv('Data/errors_used_mansion.csv', sep = ',', encoding='utf-8')

どうでしょうか。無事データが取り込めましたでしょうか。 取り込んだデータを次回以降、加工・分析していきたいと思います。

本日はこれにて。

【珈琲たいむ】Pythonでコロプレス図を描く

不動産関連の分析の最中ですが、【珈琲たいむ】にしましょう。今回は「コロプレス図」なるものを描いてみます。 英語では"choropleth map"ですが、日本語では「階級区分図」というようです。余り聴いたことがありませんが、Wikipediaでは以下のように記載があります。

階級区分図 - Wikipedia

簡単に言うと、「統計地図の1つで,区域ごとに統計量を色分けした地図」のことをいうようです。米国大統領選挙の際によく利用されるような地図です。

f:id:akatak:20180825212958j:plain

この図表は、日経平均株価AI予想さんのページから引用させていただいております。
トランプ大統領誕生のリスク-株価暴落リスク要因 | 日経平均株価 AI予想

さて、これをPythonで描いてみましょう。海外ではplotlyというモジュールが良く使われているようです。ただ、米国や世界のコロプレス図には対応していますが、残念ながら日本を対象としたコロプレス図は準備されていません。そこで、ネットで検索して調べていると、Python環境で、しかもJupyter Notebook上に日本のコロプレス図を作成するには、どうやらfoliumというモジュールを使うのがよさそうです。

ohke.hateblo.jp

www.monotalk.xyz

等のブログを参考にさせて頂きました。

事前準備を行う

さて、コロプレス図を作成していきましょう。

まず、foliumほか、pandasをインポートしておきます。

import folium
import pandas as pd

そして、コロプレス図に表示する情報(例えば、賃貸物件数や平均家賃)の以下のようなデータを予め作っておき、tokyo23.csvとして保存しておきます。

f:id:akatak:20180825223439p:plain

pandasのread_csvメソッドにて情報データを取り込みます。そして、行政区分コード(例えば、千代田区の場合は「13101」)をstr型に変換しておきます。

tokyo_data = pd.read_csv('tokyo23.csv', index_col=0)
tokyo_data['行政区分コード'] = tokyo_data['行政区分コード'].astype('str')

また、先達が作成された以下のサイトから東京23区境界のGeoJSONデータtokyo23.jsonをダウンロードして保存しておきます。

github.com

GeoJSONデータを保存している場所(相対パス)を指定します。また、地図の中心点としたい場所の位置の緯度・経度データを指定します。今回は港区の場所を指定しています。

geojson = 'tokyo23.json'
tokyo23_location = [35.658593, 139.745441]   # 地図の中心点を指定。

ベースマップを作成する

コロプレス図を表示するためのベースマップは、以下の1行で作成されます。

m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10) 

locationで地図の中心を指定し、zoom_startで縮尺のレベルを指定します。デフォルト値は10です。デフォルトなので、記載しなくても良いですが、後から変更できるように記載しています。

tilesでベースマップの種類を指定します。コロプレス図が見やすいのはcartodbpositronだと思いますので、ここで指定します。

尚、foliumにビルトインされているベースマップのうち、API_keyがなくても利用できるのは以下の8つです(本ページの最後に表示)。

  • openstreetmap
  • mapboxbright
  • cartodbdark_matter
  • cartodbpositron
  • mapboxcontrolroom
  • stamenterrain
  • stamentoner
  • stamenwatercolor

後はWeb上で公開されているLeaflet用の URL をtilesで指定して利用可能なようです。その場合の書式は http://{s}.yourtiles.com/{z}/{x}/{y}.png

folium — Folium 0.9.1 documentation

ベースマップを表示させてみると、こんな感じ。

f:id:akatak:20180826090008p:plain

コロプレス図をベースマップに表示する

まず、表示させたいデータを確認しておきましょう。

f:id:akatak:20180826090207p:plain

先ほどのベースマップ上にコロプレス図を作成するのは簡単です。先ほど作成したベースマップmに対して、choroplethメソッドを適用するだけです。

m.choropleth(
    geo_data=geojson,       # GeoJSONデータ
    name='choropleth',
    data=tokyo_data,    # DataFrameまたはSeriesを指定
    columns=['行政区分コード', '物件数'],  # 行政区分コードと表示データ
    key_on='feature.id',    # GeoJSONのキー(行政区分コード)
    fill_color='YlGn',      # 色パレットを指定(※)
    threshold_scale=[0,4000, 8000, 12000, 16000, 20000], # 境界値を指定
    fill_opacity=0.7,       # 透明度(色塗り)
    line_opacity=0.2,     # 透明度(境界) 
    legend_name='物件数'    # 凡例表示名
)

(※)色パレットは、‘BuGn’, ‘BuPu’, ‘GnBu’, ‘OrRd’, ‘PuBu’, ‘PuBuGn’, ‘PuRd’, ‘RdPu’, ‘YlGn’, ‘YlGnBu’, ‘YlOrBr’, and ‘YlOrRd’から選びます。

さて、表示させると以下のような感じになります。 f:id:akatak:20180826092239p:plain

図をhtmlファイルに保存することも可能です。

m.save('choropleth_bukkensuu.html')

ついでに、平均家賃をコロプレス図にしてみました。

f:id:akatak:20180826092901p:plain

ベースマップの種類

f:id:akatak:20180826093031p:plain

f:id:akatak:20180826093107p:plain

f:id:akatak:20180826093139p:plain

f:id:akatak:20180826093209p:plain

f:id:akatak:20180826093253p:plain

f:id:akatak:20180826093345p:plain

f:id:akatak:20180826093418p:plain

f:id:akatak:20180826093453p:plain

f:id:akatak:20180826093530p:plain

foliumでは、他にもマーカー等を付けたりできるようです。 またの機会にしましょう。今回はこれにて失礼します。

Pythonによる不動産情報のデータ取得&分析(3)【賃貸物件/分析編】

さて、いよいよ分析にチャレンジします。

目的は、家賃の適正水準を探ることです。今後、マンション売却物件の分析も行いたいと思っていますので、賃貸価格と売買価格の比較を行い、割安割高の検証を行うことも念頭に置いています(出来るかどうかはやってみないと分かりませんが)。

前回、賃貸物件データの前処理を行いましたので、そのデータを用いて、pyhonにて重回帰分析を行いたいと思います。が、統計分析についても、全くのシロウトですので、書籍やブログ等を参考にしながら行います。

まずは、前回保存したデータをDataframeとして取り込みます。また、逐次グラフ化して確認しながら作業を行っていきますので、Jupyter Notebookを利用します。

f:id:akatak:20180825092959p:plain

なお、70㎡の面積に引き直した家賃を「標準家賃」として、家賃単価(円/㎡)×70で計算した結果を列として追加しておきます。

f:id:akatak:20180825093602p:plain

賃貸データの概要の把握

データ系列の情報を見ておきましょう。

f:id:akatak:20180825093745p:plain

15万件の賃貸データがありますね。まず、家賃の分布と家賃単価の分布を見てみましょう。それぞれの要約統計量describeヒストグラムhist()を表示します。

f:id:akatak:20180825100652p:plain

f:id:akatak:20180825102024p:plain

家賃の平均は10万円ほど、家賃単価の平均は3350円ですね。家賃は物件の広さによって異なりますので、今後の分析は、不動産業者が物件比較の際に良く利用する家賃単価を中心に行っていきたいと思います。

市区別の物件数

f:id:akatak:20180825101854p:plain

世田谷区の物件数が突出して多いですね。各区の人口と比較してみます。

各市区町村の平成30年3月現在の人口が以下に掲載されていますので、Excelにコピペして、csvファイル化します(市町村のデータも取り込まれます)。  

都内区市町村マップ|東京都

そのcsvファイルをdf_popとして取り込んだ上で、groupedに統合concat()します。その際に、統合方法としてjoin='inner'を指定します。その後、必要のない面積の列等を除外して、グラフ化します。

f:id:akatak:20180825103737p:plain

人口が多いと物件も多いという傾向が出てますね。大田区だけ、人口の割には物件数が少ないですが。スクレイピングの際にデータ取得が完全でなかったのが影響しているのかもしれません。

もともと目的が、家賃の適正水準を探ることなので、データ数が十分にあれば良いとします。

市区別の家賃・家賃単価

市区別の家賃・家賃単価の平均を見ると、当たり前かと思いますが、明らかに傾向がありますよね。

f:id:akatak:20180825105628p:plain

f:id:akatak:20180825105638p:plain

ついでに家賃単価の箱ひげ図を描いてみましょう。グラフを平均によって並べ替えするのには、seabornライブラリが便利です。傾向がよく分かって面白いですね。世田谷区が案外低いですね。

f:id:akatak:20180825121833p:plain

築年数別の家賃単価

築年数別に家賃単価を散布図にしてみます。

f:id:akatak:20180825105932p:plain

上下に異常値がありそうですので、中身を見てみましょう。

f:id:akatak:20180825134725p:plain f:id:akatak:20180825110406p:plain

面積が2㎡のものがあったり、逆に660㎡のものがあったり、どうも入力ミスと思われるデータが散見されます。そうでないものもありますが、単価が14000円以上、500円以下のものはこの際、除外して分析していくことにします。

f:id:akatak:20180825110822p:plain

そのうえで、築年数毎の平均値をグラフ化すると、以下のようになります。

f:id:akatak:20180825112355p:plain

60年以降の平均値が大きくバラついていますので、築年数毎の物件数を見てみましょう。2000件以下だけ表示させます。

f:id:akatak:20180825112419p:plain

データ数の少ない築年数56年以降を分析から除外してしまいましょう。 さらに、市区別・築年数別の家賃単価(平均)をデータフレーム化したものを作成し、グラフ化します。 f:id:akatak:20180825113439p:plain f:id:akatak:20180825113612p:plain

区別に家賃単価の水準感が異なり、更に築年数が増加すれば、単価が下がっていく傾向がよく分かりますね。

最寄駅からの時間による家賃単価

最寄駅からの時間によって単価がどう変化するのかを確認しておきます。 最寄駅から物件までの移動手段として「徒歩」の場合にてグラフ化します。

f:id:akatak:20180825114742p:plain

これも異常値がありそうです。データ数を見てみましょう。

f:id:akatak:20180825114908p:plain

明らかに、750分は間違いですね。75分もどうでしょうか。切りの良いところで40分超のデータは除外しましょう。さらに、これも市区別にグラフ化しておきます。

f:id:akatak:20180825120804p:plain

これも異常値ですので、確認して必要があれば除外しておきましょう。

重回帰分析を行ってみる

さて、いよいよ重回帰分析です。有名なライブラリscikit-learnをimportして、分析を行います。異常値を除外したdf_modから、重回帰分析の説明変数、目的変数とするもののみ取り出して、データフレーム化しておきます。面積と部屋数、物件階と建物の高さは、各々やや相関が高いので、ここでは面積、物件階を残すことにします。この当たりは専門家でないので、適当です。すみません。

f:id:akatak:20180825125144p:plain

# scikit-learnライブラリをimportします
import sklearn
from sklearn.linear_model import LinearRegression as LR

# 線形回帰モデルのインスタンス化
model = LR()

# 説明変数Xに家賃単価(目的変数)と除外するとした「部屋数」「階数」以外を挿入
X = df_for_analysis_base.drop(['家賃単価', '部屋数','階数'], axis=1)

# 数値以外の説明変数(「市区」「手段」)をダミー化
X = pd.get_dummies(X)

# 目的変数に家賃単価を代入する
y = df_for_analysis_base['家賃単価']

説明変数Xはこんな感じ。 f:id:akatak:20180825130844p:plain

予測モデルの作成は、以下の1行を実行するだけです。

# 予測モデルの作成
model.fit(X, y)

結果として、回帰係数model.coef_、切片model.intercept_、決定係数score()をそれぞれ表示させます。

f:id:akatak:20180825131710p:plain

決定係数は61%となりました。まぁこんなもんなんでしょうか。 説明変数と係数の関係をもう少し分かりやすく表示させてみましょう。

coeff_df = pd.DataFrame(X.columns)
coeff_df.columns = ['説明変数']
coeff_df['係数推定'] = model.coef_

coeff_dfを表示させると以下の通りとなりました。

f:id:akatak:20180825133227p:plain

従って、(場所)世田谷区(築年数)20年(駅からの距離)徒歩10分(面積)70㎡ (物件階)2階の賃貸物件の家賃単価は、4164 .43 -3.84 +276.05 - 15.0470 - 26.3320 - 22.2110 + 50.852 = 2736円/㎡となります。

賃貸物件の面積が70㎡ですので、約191,500 円が家賃推定額となります。

(場所)江東区(築年数)15年(駅からの距離)徒歩5分(面積)80㎡ (物件階)10階の賃貸物件ですと、4164.43 -161.86 + 276.05 -15.0480 - 26.3315 - 22.41 * 5 + 50.85 * 10 = 3076円/㎡が家賃単価となります。

賃貸物件の面積が80㎡ですので、約246,000円が家賃推定額となります。

いかがでしょうか。なんとなくイメージあってますか。

検証してみる

データを学習用データ(Train)と検証用データ(Test)に分けて、残差プロットなるものを作成し、検証してみることも行うようです。

f:id:akatak:20180825133712p:plain

y=0の回りに、残差がランダムにばらけているように見えれば、モデルは良かったと言えるようですが、上のグラフの場合はどうなんでしょう。ランダムにはばらけていないように見えるので、モデルを修正していく必要があるかもしれません。

今回は、方角のデータ等、家賃単価に影響がありそうなデータを取り込んでいません。また、統計や機械学習は、まだ、勉強を始めたばかりですので、今後、機会があれば、モデルの修正にチャレンジしていきたいと思います。

長くなりましたので、本日はこれにて失礼します。次回以降は、売却物件について分析していきたいと思います。

Pythonによる不動産情報のデータ取得&分析(2)【賃貸物件/データ前処理編】

こんにちは。 皆さん、賃貸物件のダウンロードはうまくいきましたでしょうか。

以前はうまくいっていたのですが、最近、BeautifulSoupのhtml.parserがうまく機能していないのか、ダウンロードの度に、一定数のページでGoogleChromeの検証での表示と異なる状況となり、結果としてエラーが発生してしまいます。再度、同じスクリプトを走らせると、今度は前回と異なるページでエラーが発生したりして、挙動が一定しません。全体の2〜3割がうまくダウンロードできませんが、データ数としては全体で15万件ほどと十分確保できましたので、このまま進めたいと思います。

賃貸物件データの統合

前回と同様に他の22区(+興味のある三鷹市武蔵野市)についてもデータをダウンロードしておきます。これらのファイルをpandasを利用して読み込み、1つのファイルの統合し、csvファイルとして書き出します。

import pandas as pd

# dataフォルダに保存した各区の賃貸物件データの読み込み。i
df_adachi = pd.read_csv('data/suumo_adachi.csv', sep=',', index_col=0)
df_arakawa = pd.read_csv('data/suumo_arakawa.csv', sep=',', index_col=0)
df_bunkyo = pd.read_csv('data/suumo_bunkyo.csv', sep=',', index_col=0)
df_chiyoda = pd.read_csv('data/suumo_chiyoda.csv', sep=',', index_col=0)
df_chuo = pd.read_csv('data/suumo_chuo.csv', sep=',', index_col=0)
df_edogawa = pd.read_csv('data/suumo_edogawa.csv', sep=',', index_col=0)
df_itabashi = pd.read_csv('data/suumo_itabashi.csv', sep=',', index_col=0)
df_katsushika = pd.read_csv('data/suumo_katsushika.csv', sep=',', index_col=0)
df_kita = pd.read_csv('data/suumo_kita.csv', sep=',', index_col=0)
df_koto = pd.read_csv('data/suumo_koto.csv', sep=',', index_col=0)
df_meguro = pd.read_csv('data/suumo_meguro.csv', sep=',', index_col=0)
df_minato = pd.read_csv('data/suumo_minato.csv', sep=',', index_col=0)
df_nakano = pd.read_csv('data/suumo_nakano.csv', sep=',', index_col=0)
df_nerima = pd.read_csv('data/suumo_nerima.csv', sep=',', index_col=0)
df_ota = pd.read_csv('data/suumo_ota.csv', sep=',', index_col=0)
df_setagaya = pd.read_csv('data/suumo_setagaya.csv', sep=',', index_col=0)
df_shibuya = pd.read_csv('data/suumo_shibuya.csv', sep=',', index_col=0)
df_shinagawa = pd.read_csv('data/suumo_shinagawa.csv', sep=',', index_col=0)
df_shinjuku = pd.read_csv('data/suumo_shinjuku.csv', sep=',', index_col=0)
df_suginami = pd.read_csv('data/suumo_suginami.csv', sep=',', index_col=0)
df_sumida = pd.read_csv('data/suumo_sumida.csv', sep=',', index_col=0)
df_taito = pd.read_csv('data/suumo_taito.csv', sep=',', index_col=0)
df_toshima = pd.read_csv('data/suumo_toshima.csv', sep=',', index_col=0)
df_mi_mu = pd.read_csv('data/suumo_mi_mu.csv', sep=',', index_col=0)  # 三鷹市と武蔵野市の物件データ

read_csvメソッドの引数としてindex_col=0を設定していますが、これは、元のcsvファイルの最初の列(0列)をpandasデータフレームのindex列に指定するものです。 一旦全ての区のデータをデータフレームとして取り込んだ後、concatメソッドを利用して、これらのデータフレームを一つに統合します。各区のデータを列に追加していきますので、axis=0とします。また、各データフレームのindexを新たな通し番号として設定するために、ignore_index=Trueとしておきます。

df = pd.concat([df_adachi, df_arakawa, df_bunkyo, df_chiyoda, df_chuo, df_edogawa, df_itabashi, df_katsushika, df_kita, df_koto, df_meguro, df_minato, df_nakano, df_nerima, df_ota, df_setagaya, df_shibuya, df_shinagawa, df_shinjuku, df_suginami, df_sumida, df_taito, df_toshima, df_mi_mu], axis=0, ignore_index=True)

これで、東京23区の賃貸物件データがひとつのデータフレームになりました。

この統合されたデータフレームを、後からの作業をしやすくするために、一旦csvファイルに保存しておきましょう。

df.to_csv('data/suumo_tokyo.csv', sep=',')

データ前処理

さて、分析をしやすくするために、データを加工していきましょう。 まず、先ほど保存したデータフレームの全体像を見ると以下のようになります。

df.info()

f:id:akatak:20180818113630p:plain

また、最初の4行を見ておきましょう。

df.head(4)

f:id:akatak:20180818113637p:plain

データの前処理は以下の通り行うことにします。データの前処理は、アウトプットやエラー等を確認しながら行う必要があり、Jupyter Notebookを利用すると良いかと思います。

  • 住所から「都県」欄・「市区」欄を新設する
  • 築年数を「文字」から「数値」に置き換える
  • 家賃を「文字」から「数値」(円単位)にする
  • 管理費を「文字」から「数値」(円単位)にする
  • 「家賃合計」欄を新設する(家賃合計=家賃+管理費)
  • 「家賃単価(円/平米)」(=「家賃合計」÷「面積」)を計算する
  • 間取りから「部屋数」欄を作成する

最後に、

  • 立地1・立地2・立地3を「路線」「最寄駅」「手段」「時間」に分解する

「都県」欄・「市区」欄を新設

# 文字列を複数文字で split
df['都県'] = df['住所'].apply(lambda x: re.split('[都県]', x)[0]) 
df['市区'] = df['住所'].apply(lambda x: re.split('[都県市区]', x)[1]) 

複数の文字(この場合、「都」「県」「市」「区」)が合った場合に、その文字の前と後に分解するreモジュールのsplitを利用します。1行目により、「都県」の前後に分解し1番目の文字列を「都県」欄に格納します。また、2行目により、「都県市区」文字の前後に分解し、2番目の文字列を「市区」欄に格納します。

築年数を「文字」から「数値」に返還

まず、

df['築年数'].unique()

にて、築年数の種類を確認しておくと処理しやすいでしょう。結果は以下の通りとなります。

array(['築21年', '築2年', '築18年', '築17年', '築25年', '築27年', '築8年', '築6年', '築49年',
       '築1年', '築23年', '築13年', '築19年', '築11年', '築28年', '築5年', '新築', '築29年',
       '築4年', '築12年', '築3年', '築15年', '築24年', '築32年', '築54年', '築22年', '築7年',
       '築31年', '築16年', '築9年', '築10年', '築20年', '築30年', '築42年', '築34年',
       '築26年', '築14年', '築43年', '築40年', '築38年', '築48年', '築39年', '築41年',
       '築56年', '築33年', '築52年', '築35年', '築46年', '築45年', '築51年', '築36年',
       '築37年', '築53年', '築55年', '築44年', '築0年', '築47年', '築58年', '築50年',
       '築59年', '築60年', '築72年', '築65年', '築67年', '築57年', '築66年', '築74年',
       '築83年', '築82年', '築99年', '築64年', '築70年', '築61年', '築69年', '築63年',
       '築84年', '築62年', '築93年', '築68年', '築79年', '築75年', '築87年', '築78年',
       '築71年', '築77年', '築94年', '築97年', '築80年', '築73年'], dtype=object)

これらを見ると、
1. データの両端から「築」「年」を取り除く。
2. 「新」を「0」に置き換える。
3. 文字列を数値に変換する。
ことを行えば良いことが分かります。

df['築年数'] = df['築年数'].str.strip('築年')       # 両端から'文字'を削除
df['築年数'] = df['築年数'].str.replace('新', '0') # '1文字目'を'2文字目'で置換
df['築年数'] = pd.to_numeric(df['築年数'])      # 文字列を数値に変換

これで、築年数が無事に数値に変換できました。

家賃を「文字」から「数値」(円単位)に

まず、以下を計算することで「万円」が含まれていないデータがないことを確認しておきます。

sum(df['家賃'].apply(lambda x: '万円' not in x))

0であればOKです。 続いて、「万円」を「」で置き換えて、floatに変換後、10000を掛けます。小数点が表示されるのが煩わしいので、結果を整数に変換しておきます。

df['家賃'] = df['家賃'].str.replace('万円', '').apply(lambda x: int(float(x) * 10000))

管理費を「文字」から「数値」(円単位)に

さて、管理費です。これもdf['管理費'].unique()で内容を見ておきましょう。 管理費がないか不明なものは「-」と表示されている以外は、特段留意するところはなさそうです。

df['管理費'] = df['管理費'].str.replace('-','') # '-'を''で置換
df['管理費'] = df['管理費'].str.replace('円','') # '円'を''で置換
df['管理費'] = pd.to_numeric(df['管理費'], downcast='integer') #文字列を整数に変換
df['管理費'] = df['管理費'].apply(lambda x: 0 if np.isnan(x) else x) # NaNのデータの場合に0で置き換えます。

4行目を入れる代わりに、1行目の'-'の''への置換を'0円'への置換としておいた方がすっきりするかもしれません。

「家賃合計」欄を新設

「家賃単価(円/平米)」を計算

更に家賃合計と㎡当たりの家賃単価も計算しておきましょう。

df['家賃合計'] = df['家賃'] + df['管理費']
df['家賃単価(円/平米)'] = df['家賃合計'] / df['面積']                     

間取りから「部屋数」欄を作成

さて、間取りです。 これも同様にdf['間取り'].unique()にて、どんなデータがあるか見ておきましょう。

array(['3LDK', '1SLDK', '2LDK', 'ワンルーム', '1LDK', '4LDK', '1K', '3DK',
       '1DK', '2DK', '2K', '3K', '5SLDK', '2LK', '2SLDK', '2SK', '1SK',
       '4SK', '3SDK', '4DK', '2SDK', '5LDK', '4SLDK', '6LDK', '4K', '6DK',
       '3SLDK', '5K', '5DK', '5SDK', '1SDK', '6K', '4SDK', '1LK', '1SLK',
       '5SK', '7LDK', '8LDK', '9LDK', '6SDK', '6SLDK', '3SK', '7DK', '9DK',
       '3SLK', '3LK', '22LDK', '11SLDK', '7SLDK', '2SLK', '18DK', '32SLDK'], dtype=object)

部屋数を計算しやすくするために、「ワンルーム」との表示を「1RM」とでもしておきましょう。

df['間取り'] = df['間取り'].str.replace('ワンルーム', '1RM')

各間取りの数値部分が部屋数を示していますので、数値部分のみ取り出したいと思います。

df['部屋数'] = df['間取り'].apply(lambda x: int(re.search('[0-9]+', x).group(0)))

立地1・立地2・立地3を「路線」「最寄駅」「手段」「時間」に分解

さて、いよいよ、立地1〜3をそれぞれ「路線」「最寄駅」「手段」「時間」に分解していきましょう。入力が必ずしも、ルールに基づいていない場合もあり、エラーが起きやすい箇所です。ひとつひとつの作業を丁寧に確認しながら行っていくことが必要です。 まず、立地1から分解していきます。

df['立地1_路線'] = df['立地1'].apply(lambda x: x.split('/')[0])
df['立地1_駅'] = df['立地1'].apply(lambda x: x.split('/')[1].split(' ')[0])
df['立地1_手段'] = df['立地1'].apply(lambda x: x.split('/')[1].split(' ')[1][0])
df['立地1_分'] = df['立地1'].apply(lambda x: re.search('[0-9]+',x.split('/')[1].split(' ')[1]).group(0))

うまくいきました。続いて、立地2です。立地2がない場合(NaN)があり、その場合、「-」に変換しておきます。 また、立地1とは「−」の場合の処理が異なります。

df['立地2'].fillna('-',inplace=True)
df['立地2_路線'] = df['立地2'].apply(lambda x: x.split('/')[0])
df['立地2_駅'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_駅'] = df['立地2_駅'].apply(lambda x: x.split(' ')[0])
df['立地2_手段'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_手段'] = df['立地2_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)
df['立地2_分'] = df['立地2'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地2_分'] = df['立地2_分'].apply(lambda x: x.split(' ')[1] if x != '-' else x)
df['立地2_分'] = df['立地2_分'].apply(lambda x: re.search('[0-9]+', x).group(0) if x !='-' else x)

これもうまくいきました。いよいよ立地3に参りましょう。処理は立地2と同様です。

df['立地3'].fillna('-',inplace=True)
df['立地3_路線'] = df['立地3'].apply(lambda x: x.split('/')[0])
df['立地3_駅'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_駅'] = df['立地3_駅'].apply(lambda x: x.split(' ')[0])
df['立地3_手段'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_手段'] = df['立地3_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)

うーん。やはりエラーがでました。

f:id:akatak:20180818131035p:plain

以下のスクリプトをを動かして、エラーが発生した列を探しましょう。

errors = []
for i, item in enumerate(df['立地3_手段']):
    try:
        if item != '-':
            item.split(' ')[1]
        else:
            item
    except Exception as e:
        errors.append([e, i])
print(errors)

すると、以下のように

[[IndexError('list index out of range',), 103908]]

と表示されました。103908行のデータが何かおかしそうです。5列目(立地3)の中身を見るには、

df.iloc[103908,5]

とします。すると、

'自01/02自由が丘駅-駒大/深沢小学校 歩1分'

と表示されました。「路線/最寄駅 手段○分」というルールに従っていないようですので、修正しておきましょう。

df.iloc[103908, 5] = '東急バス(自由が丘駅-駒大)/深沢小学校 歩1分'

とでも入力して、以下の再度スクリプトを回してみると、今度はうまく行きました。

df['立地3_路線'] = df['立地3'].apply(lambda x: x.split('/')[0])
df['立地3_駅'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_駅'] = df['立地3_駅'].apply(lambda x: x.split(' ')[0])
df['立地3_手段'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_手段'] = df['立地3_手段'].apply(lambda x: x.split(' ')[1][0] if x != '-' else x)
df['立地3_分'] = df['立地3'].apply(lambda x: x.split('/')[1] if x != '-' else x)
df['立地3_分'] = df['立地3_分'].apply(lambda x: x.split(' ')[1] if x != '-' else x)
df['立地3_分'] = df['立地3_分'].apply(lambda x: re.search('[0-9]+', x).group(0) if x != '-' else x)

なお、次回の分析のために、物件の「階数」と物件がある「物件階」もデータを加工しておきます。

df['階数'] = df['階数'].str.replace('平屋','1階建')
df['階数'] = df['階数'].apply(lambda x: re.search('[0-9]+階建', x).group(0))
df['階数'] = df['階数'].apply(lambda x: int(x.strip('階建')))

にて、建物の階数を数字で確保します。 その他、「物件階」も個別に作業した上で、数値化して保存します。

修正後のデータを保存

最後に

df.to_csv('app/data/suumo_tokyo_mod.csv')

として、加工後のデータを保存しておきましょう。

いかがでしたでしょうか。人が作ったデータですので、間違って入力している場合もあります。この過程は、ひとつひとつの作業を、結果を見ながら、適宜、修正しながら行っていく必要があるかと思います。その場合に、Jupyter Notebookは威力を発揮します。

皆さんも是非試してみてはいかがでしょうか。 長くなりましたが、本日はこの辺で失礼します。

Pythonによる不動産情報のデータ取得&分析(1)【賃貸物件/Webスクレイピング編】

暑い日が続きますね。皆さんはいかがお過ごしでしょうか。

我が家では、不動産の賃貸で住み替えか?購入か?を検討しなくてはいけない局面が生じたので、何かデータを取得して分析できないかな、とネットサーフィンをしていたところ、発見!

【データで見る世界】機械学習を使って東京23区のお買い得賃貸物件を探してみた 〜スクレイピング編〜

ありがとうございます。このような先人の方がいらっしゃると、真似をしつつ、何とかやってみようという気がしてきます。

さて、まずは、東京23区の賃貸物件について、「データで見る世界」さんのブログを参考にして、Pythonを使って取得してみることにしましょう。「データで見る世界」さん同様にSUUMOさんからデータを取得していくにあたり、まずはSUUMO規約を確認しましょう。

SUUMO(スーモ)規約

私的利用を超えて使用してはいけないことは書かれていますが、それ以外に、スクレイピングに関する制限はなさそうですね。どうやらスクレイピングをしても大丈夫そうです。

不動産情報を取得する準備

まず、いつものようにrequestsモジュールとBeautifulSoupモジュールをimportしましょう。それ以外にもtimeモジュールとpandasモジュールもimportしておきます。

import requests
from bs4 import BeautifulSoup
import time
import pandas as pd

SUUMOさんのホームページから「関東」「賃貸物件」と選択していくと、「関東の賃貸住宅[賃貸マンション・アパート情報探し]」のページとなり、「沿線・エリアから探す」よう選択が求められますので、「東京都」「エリア」を選びます。そうすると、「東京都ー市区郡を選択」するページが表示されます。ここで東京23区を全て選択してもいいかと思いますが、全部で20万件程度のデータになるかと思います。時間もかかるし、実行中にエラーとなる可能性も高くなりますので、区毎にデータを取得していきます。

最初は、足立区を選択してみましょう。足立区の賃貸物件検索ページのアドレスをコピペし、urlに格納します。

# SUUMO 賃貸 東京都足立区 検索
url = 'https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&bs=040&ta=13&sc=13121&cb=0.0&ct=9999999&et=9999999&cn=9999999&mb=0&mt=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&fw2=&srch_navi=1'

いつものようにurlの情報を取得して、htmlで構文解析を行い、結果をsoupオブジェクトに格納します。そして、そのsoupオブジェクトを通じて、基本的にデータを取得していくことになります。

result = requests.get(url)
c = result.content
soup = BeautifulSoup(c, "html.parser")

実際にホームページをご覧になると分かると思いますが、物件データは数十ページに跨がっています。また、データ数によって日々、ページ数は変わってきますので、まずはページ数を取得して、その後1ページずつデータしていきます。

ページ数・urlの取得

# ページ数を取得
body = soup.find("body")
pages = body.find_all("div", {'class':'pagination pagination_set-nav'})
pages_text = str(pages)
pages_split = pages_text.split('</a></li>\n</ol>')
num_pages = int(pages_split[0].split('>')[-1])

「データで見る世界」さんとは、自分自身で理解しやすいように、スクリプトは変更しています。 実際にデータを取得していく際には、かなり変更している部分がありますので、参考にしていただければと思います。

さて、次に、urlsリストに1ページずつ、urlを加えていきます。ここはそのまま使わさせていただいています。

# 全てのページのURLを作成
# urlを入れる箱(リスト)を設定
urls = []

# 1ページ目を格納
urls.append(url)

# 2ページ目以降を格納
for i in range(num_pages-1):
    pg = str(i+2)
    url_page = url + '&pn=' + pg
    urls.append(url_page)

スクレイピングによるデータ取得の準備

まずは、全データを格納するリストdataとエラーが出たときにその内容を格納するリストerrorsを用意します。賃貸物件の件別データ(1つの物件でも複数の部屋の物件が出ている場合は、その件別も含む)を1つ1つ取り込みます。その場合の件別データそのものもリスト形式なので、dataはリストのリストになります。

data = []
errors = []

さて、スクレイピングの本体部分に入っていきます。urlsに格納したurlを一つずつ取り出して、1ページ毎にスクレイピングしていきます。

for url in urls:

スクレイピングの最中にエラーが出ても、スクリプトが動き続けるように、try〜exceptを入れます。ついでにerrorsにエラーの内容、エラーが出たときのurl、何番目のデータかを保存しておきます。

    try:
    
    # ここにスクリプトのエンジン部分を書きます。
    
    except Exception as e:
        errors.append([e, url, len(data)])
        pass    

いよいよ、スクレイピングのエンジン部分に入っていきましょう。url毎にsoupオブジェクトを作り、soupオブジェクトから各種情報を取り出すようにします。

        result = requests.get(url)
        c = result.content
        soup = BeautifulSoup(c, "html.parser")

Google Chromeの検証機能を使って探していくと、ページの中で件別データ全体を選択するのはdivタグのうち、id='js-bukkenList'のものになります。findメソッドの引数でこれを指定して、summaryに情報を格納します。更に、summaryに格納された情報の中で、各件別データを選択するのは、divタグのうち、class='cassetteitem'ですので、find_allメソッドの引数でこれを指定して、件別データをcassetteitemsに格納します。

        summary = soup.find("div",{'id':'js-bukkenList'})
        cassetteitems = summary.find_all("div",{'class':'cassetteitem'})

スクレイピングのエンジン部分

その後は、cassetteitemsから件別データ(cassetteitem)毎に情報を取り出していきます。この段階でもエラー処理を行えるようにしております。

        for cas in cassetteitems:
            try:
              # 情報取得用の箱を準備します。
                new = ''  # 新着
                subtitle = '' # 物件名
                location = '' # 住所
                station_list = []  # 最寄駅(リスト)
                yrs = ''           # 築年数
                heights = ''      # (建物の)階数
                floor = ''         # (物件のある)階数
                rent = ''      # 家賃
                admin = ''         # 管理費
                others = ''     # その他(敷金/礼金等)
                floor_plan = ''   # 間取り
                area = 0       # 面積

                # 物件名
                subtitle = cas.find("div",{"class":"cassetteitem_content-title"}).string

                # 住所
                location = cas.find("li",{"class":"cassetteitem_detail-col1"}).string

                # 最寄駅
                sta = cas.find("li", {"class":"cassetteitem_detail-col2"})
                stas = sta.find_all("div", {"class":"cassetteitem_detail-text"})
                for s in stas:
                    station_list.append(s.string)

                # 築年数、階数
                col3 = cas.find("li",{"class":"cassetteitem_detail-col3"})
                yrs = col3.find_all('div')[0].string
                heights = col3.find_all('div')[1].string

                tbodies = cas.find_all('tbody')

                for tbody in tbodies:
                    cols = tbody.find_all('td')
                    for i, col in enumerate(cols):
                        if i == 0:
                            try:
                                new = col.span.string.strip('\r\t\n')
                            except:
                                new = ''
                        if i == 2:
                            floor = col.string
                        if i == 3:
                            rent = col.string
                        if i == 4:
                            admin = col.string
                        if i == 5:
                            others = col.string
                        if i == 6:
                            floor_plan = col.string
                        if i == 7:
                            area = float(col.contents[0].split('m')[0])
                    data.append([new, subtitle, location, station_list[0], station_list[1], station_list[2], yrs, heights, floor,
                                 rent, admin, others, floor_plan, area])
            except Exception as e:
                errors.append([e, url,len(data)])
                pass

        time.sleep(1) # スクレイピングする際の礼儀として、1秒待ちましょう

これで全ての物件データが取得でき、dataとerrorsに格納されました。

pandasを使ってデータをcsvファイルとして保存

さて、これらのデータをpandasのメソッドを利用してDataFrameオブジェクトに変換、その後、csvファイルとして保存します。

# data listを DataFrameに変換
df = pd.DataFrame(data, columns=['新着','物件名','住所','立地1','立地2','立地3','築年数','階数','物件階','家賃','管理費','敷金礼金','間取り','面積'])

# csvファイルとして保存
df.to_csv('data/suumo_adachi.csv', sep = ',',encoding='utf-8')

# ついでに errors fileも保存
df_errors = pd.DataFrame(errors)
df_errors.to_csv('data/errors_adachi.csv', sep = ',', encoding='utf-8')

以上でデータが保存できました。念のため、errorsを開いて、エラーがないことを確認しましょう。たまに、通信環境等によってか、エラーが発生することがありますが、少ない時は無視します。エラーが多い時だけ、時間を空けて再度トライしてみます。これでうまくいくことが多いので、エラーが発生した場合には、時間をおいてみましょう。

次回以降、取得したデータの加工をしていきます。