GIS奮闘記

元GISエンジニアの技術紹介ブログ。主にPythonを使用。

スポンサーリンク

SUUMO の中古物件情報を Tableau で分析してみる ~データ収集編~

さて、本日は 「SUUMO の中古物件情報を Tableau で分析してみる ~データ収集編~」です。私が中古物件を探しているのですが、条件(最寄りの路線や駅近など)によって価格の変動やどういった傾向にあるのかを知りたかったため、Tableau を使って傾向を分析してみようと思います。

なお、本シリーズは以下3エントリーにわたって SUUMO の中古物件情報を扱います。本エントリーはデータ収集編です。

  • データ収集編
    • 分析に必要なデータを収集します。
  • データ分析編
    • 収集したデータの分析を行います。
  • データ予測編
    • 機械学習を使用して物件価格の推論を行います。

データ取得方法

Beautiful Soup を使ったスクレイピングでデータを取得します。関連エントリーを以下に記載しますので、興味がある方はぜひ読んでみてください。

www.gis-py.com

スクレイピング対象データ

f:id:sanvarie:20210508130543p:plain

f:id:sanvarie:20210508130624p:plain

対象エリア

横浜市

理由

横浜市で中古物件の購入を検討しているため

アウトプット

以下のような形でデータを取得し、CSV に出力します。

フィールド名 説明
カテゴリ 中古マンション or 中古一戸建て
販売価格 販売価格
所在地 物件所在地
沿線 物件最寄沿線
最寄駅 物件最寄駅
徒歩(分) 物件から最寄駅までの徒歩時間。バスの場合は物件からバス停までの時間
バス(分) バス乗車時間。バスを使用しない場合は0
土地面積 土地面積。中古マンションは建物面積=土地面積とする
建物面積 中古一戸建てもしくは中古マンションの建物面積
バルコニー バルコニー面積。中古マンションのみ使用
間取り 物件の間取り
築年数 築年月から計算

欲しかったデータ

以下のようなデータも取得できればより詳細な分析ができそうですが、簡単に取得はできなさそうなので今回は諦めます・・・

  • 向き(日当たり)
  • 駐車場有無
  • 庭有無
  • 周辺環境

データ加工内容

以下にデータ加工内容を記載します。ただスクレイピングするだけなら楽なのですが、分析用となるとデータをかなり加工しなければならないのでそれなりに手間がかかりますね。

  • 69.98m2(登記)
  • 2LDK+S(納戸)
  • 103.51m2(実測)
  • 50.11m2(壁芯)
    • ()とその内容を除去する。また、m2を除去する
  • JR
    • JR に変換する
  • ◯◯万円
    • 数値に変換する
  • バルコニーの「-」の表記
    • 0に変換する
  • ◯◯万円~◯◯万円
    • 大きい方の値を使用する(間取りや専有面積も同様)
  • ◯◯万円※権利金含む◯◯万円
    • ※ 以降を除去する
  • LDK+S
    • +S を除去する
  • LK
    • LDKに変換する
  • LLDDKK
    • LDKに変換する(LDDKのように似たようなものも同様)
  • ワンルーム
    • 1Kに変換する
  • 湘南新宿ライン
    • 湘南新宿ライン高海と湘南新宿ライン宇須を湘南新宿ラインに変換する

加工しきれなかったデータ

存在しない沿線(神奈川中央交通など)や駅名(横浜パークタウンなど)の記載があり、そのあたりのデータはとりあえず今回収集して分析時に除外しようと思います。(SUUMO さんには正確なデータを入力してほしいものです)

環境

Windows10 64bit
Python3.8.5

サンプルコード

思ったよりもデータの加工が必要でコードが長くなってしまいました。もう少し改善の余地はあると思いますが、とりあえずサンプルとしては以下です。

# -*- coding: utf-8 -*-
import urllib.request
from bs4 import BeautifulSoup
import pandas as pd

# 中古一戸建てと中古マンションのurl
url_list = ["https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=021&ta=14"  \
            "&jspIdFlg=patternShikugun&sc=14101&sc=14102&sc=14103&sc=14104&sc=14105&sc=14106"  \
            "&sc=14107&sc=14108&sc=14109&sc=14110&sc=14111&sc=14112&sc=14113&sc=14114"  \
            "&sc=14115&sc=14116&sc=14117&sc=14118&kb=1&kt=9999999&tb=0&tt=9999999&hb=0"  \
            "&ht=9999999&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1&pn={}",

            "https://suumo.jp/jj/bukken/ichiran/JJ010FJ001/?ar=030&bs=011&ta=14" \
            "&jspIdFlg=patternShikugun&sc=14101&sc=14102&sc=14103&sc=14104&sc=14105&sc=14106" \
            "&sc=14107&sc=14108&sc=14109&sc=14110&sc=14111&sc=14112&sc=14113&sc=14114" \
            "&sc=14115&sc=14116&sc=14117&sc=14118&kb=1&kt=9999999&mb=0&mt=9999999" \
            "&ekTjCd=&ekTjNm=&tj=0&cnb=0&cn=9999999&srch_navi=1&pn={}"]

# 収集するデータのカラム
cols = ["カテゴリ", "販売価格", "所在地", "区", "沿線", "最寄駅", "徒歩(分)", "バス(分)",
        "土地面積", "建物面積", "バルコニー", "間取り", "築年数"]

def identify_floor_plan(floor_plan):
    # 間取りの種類が多すぎるのでまとめる
    if floor_plan.find("ワンルーム") > -1:
        floor_plan = "1K"
    if floor_plan.find("+") > -1:
        floor_plan = floor_plan[:floor_plan.find("+")]
    if floor_plan.find("LK") > -1:
        floor_plan = floor_plan[0:1] + "LDK"
    if floor_plan.find("LL") > -1 or \
       floor_plan.find("DD") > -1 or \
       floor_plan.find("KK") > -1:
        floor_plan = floor_plan = floor_plan[0:1] + "LDK"

    return floor_plan

def convert_price(price):
    # 万円※権利金含むの処理
    if price.find("※") > -1:
        price = price[:price.find("※") ]

    # ○○万円~○○万円の処理
    if price.find("~") > -1:
        price = price[price.find("~") + 1: ]

        if price.find("億") > -1:
            # 1億円ジャストのような場合の処理
            if price.find("万") == -1:
                price = int(price[:price.find("億")]) * 100000000
            else:
                oku = int(price[:price.find("億")]) * 100000000
                price = oku + int(price[price.find("億") + 1:-2]) * 10000
        else:
            price = int(price[:price.find("万円")]) * 10000
    else:
        if price.find("億") > -1:
            # 1億円ジャストのような場合の処理
            if price.find("万") == -1:
                price = int(price[:price.find("億")]) * 100000000
            else:
                oku = int(price[:price.find("億")]) * 100000000
                price = oku + int(price[price.find("億") + 1:-2]) * 10000
        else:
            price = int(price[:-2]) * 10000

    return price

def remove_brackets(data):
    # ○○~○○の処理
    if data.find("~") > -1:
        data = data[data.find("~") + 1:]

    # m2以降を除去
    if data.find("m2") > -1:
        data = data[:data.find("m")]

    # ()を除去
    if data.find("(") > -1:
        data = data[:data.find("(")]

    return data

def get_line_station(line_station):

    # JRをJRに変換
    if line_station.find("JR") > -1:
        line_station = line_station.replace("JR", "JR")

    # バスと徒歩の時間を取得
    if line_station.find("バス") > -1:
        bus_time = line_station[line_station.find("バス") + 2 : line_station.find("分")]

        # バスの場合は徒歩時間=(バス時間×10) + バス停までの徒歩時間とする
        walk_time = line_station[line_station.find("停歩") + 2 : line_station.rfind("分")]

    else:
        bus_time = 0
        walk_time = line_station[line_station.find("徒歩") + 2 : line_station.rfind("分")]

    # 沿線と駅を取得
    line = line_station[ : line_station.find("「") ]

    if line.find("湘南新宿ライン高海") > -1 or \
       line.find("湘南新宿ライン宇須") > -1:
        line = "湘南新宿ライン"

    station = line_station[line_station.find("「") + 1 : line_station.find("」")]

    return line, station, bus_time, walk_time

def get_page_count(hit_count):

    # データ整形
    hit_count = hit_count.strip()
    hit_count = hit_count.replace(",", "")
    hit_count = hit_count.replace("件", "")

    # ページ数計算
    page_count = divmod(int(hit_count), 30)
    if page_count[1] == 0:
        page_count = page_count[0]
    else:
        page_count = page_count[0] + 1

    return page_count

def main():

    df = pd.DataFrame(index=[], columns=cols)

    for i, url_base in enumerate(url_list):
        # 対象urlのデータを取得
        url = url_base.format(1)
        html = urllib.request.urlopen(url).read()
        soup = BeautifulSoup(html)
        hit_count = soup.find("div", class_="pagination_set-hit").text

        # 各urlのページ数計算
        page_count = get_page_count(hit_count)

        data = {}
        for page in range(1, page_count + 1):

            # ページごとにリクエスト
            if page != 1:
                url = url_base.format(page)
                html = urllib.request.urlopen(url).read()
                soup = BeautifulSoup(html)

            for s in soup.findAll('div', 'dottable-line'):

                if i == 0:
                    data["カテゴリ"] = "中古一戸建て"

                else:
                    data["カテゴリ"] = "中古マンション"

                if len(s.findAll('dt')) == 1:
                    if s.find('dt').text == "販売価格":

                        # ○○万円を数字に変換
                        price = convert_price(s.find("span").text)
                        data["販売価格"] = price

                if len(s.findAll('dt')) == 2:

                    if s.findAll('dt')[0].text == "所在地":
                        area = s.findAll("dd")[0].text
                        data["所在地"] = area
                        data["区"] = area[area.find("市") + 1:area.find("区") + 1]

                    if s.findAll('dt')[1].text == "沿線・駅":

                        # データ加工
                        line, station, bus_time, \
                            walk_time = get_line_station(s.findAll("dd")[1].text)

                        data["沿線"] = line
                        data["最寄駅"] = station
                        data["徒歩(分)"] = walk_time
                        data["バス(分)"] = bus_time

                if s.find('table', class_ = 'dottable-fix') != None:
                    if s.findAll('dt')[0].text == "土地面積":

                        land_area = remove_brackets(s.findAll("dd")[0].text)
                        data["土地面積"] = land_area

                    if s.findAll('dt')[1].text == "間取り":

                        floor_plan = remove_brackets(s.findAll("dd")[1].text)

                        # 間取りをまとめる
                        floor_plan = identify_floor_plan(floor_plan)
                        data["間取り"] = floor_plan

                    if s.findAll('dt')[0].text == "建物面積":

                        house_area = remove_brackets(s.findAll("dd")[0].text)
                        data["建物面積"] = house_area

                    if s.findAll('dt')[0].text == "専有面積":

                        house_area = remove_brackets(s.findAll("dd")[0].text)
                        data["建物面積"] = house_area

                        # 中古マンションは建物面積=土地面積とする
                        data["土地面積"] = house_area

                    if s.findAll('dt')[0].text == "バルコニー":

                        if s.findAll("dd")[0].text.find("-") > -1:
                            data["バルコニー"] = 0
                        else:
                            balcony_area = remove_brackets(s.findAll("dd")[0].text)
                            data["バルコニー"] = balcony_area

                    else: # 一戸建ての場合は0
                        data["バルコニー"] = 0

                    if s.findAll('dt')[1].text == "築年月":

                        # 築年数を算出
                        built_year = 2021 - int(s.findAll("dd")[1].text[:4])
                        data["築年数"] = built_year

                # データフレームに1物件ずつデータを格納
                if len(data) == 13:
                    df = df.append(data, ignore_index=True)
                    data = {}

    # CSV 出力
    df.to_csv(r"D:\data\csv\property.csv", index=False, encoding = "utf-8")

if __name__ == '__main__':
    main()

出力結果を確認

全部で8329件でした。一部抜粋したものを以下に載せます。いい感じにデータを取得できたと思います。

f:id:sanvarie:20210513113825p:plain

f:id:sanvarie:20210513113855p:plain

さいごに

細かいデータの加工がなかなか上手くいかず以外とコードを書くのに手こずってしまいましたが、何とか SUUMO から物件情報を取得することができました。このデータを使って次回のエントリーでTableau を使ったデータ分析をしようと思います。本日は以上です。