1
0

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で物件データをスクレイピングして定量評価してみた

1
Posted at

はじめに

引っ越しを考えている。
僕はつくづく不動産運がないので、自分の勘に頼って決めるのはやめたい。「なんとなく良さそう」ではなく、定量的にデータを把握して論理的に物件を選びたいのだ。

ということで、今回はPythonを使って賃貸物件データを集め、自分の希望条件における「コスパ最強の駅」はどこなのかを諸々計算してみることにした。

1. データの収集:SUUMOから物件情報をスクレイピング

まずはデータがないと始まらない。SUUMOの物件情報をスクレイピングして取得していく。
スクレイピングの基本的な実装については、こちらの素晴らしい記事を大いに参考にさせていただいた。先人の知恵に感謝。

自分の職場が東京駅周辺なので、今回の検索条件は以下のように設定した。

  • 東京駅から90分圏内、乗り換えなし
  • 間取り:1K / 1DK / 1LDK
  • 構造:鉄筋系・鉄骨系

この条件でヒットした物件データを根こそぎ取得するコードがこちら。

import os
import time
import datetime
import urllib
import requests
import pandas as pd
from bs4 import BeautifulSoup
from retry import retry

@retry(tries=3, delay=10, backoff=2)
def load_page(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.content, 'html.parser')
    return soup

def get_suumo_data(base_url, max_page=10, start_page=1):
    data_samples = []

    for page in range(start_page, max_page + 1):
        soup = load_page(base_url.format(page))
        mother = soup.find_all(class_='cassetteitem')

        for child in mother:
            data_home = []
            # 基本情報
            data_home.append(child.find(class_='ui-pct ui-pct--util1').text) # カテゴリ
            data_home.append(child.find(class_='cassetteitem_content-title').text) # 建物名
            data_home.append(child.find(class_='cassetteitem_detail-col1').text) # 住所
            
            # 最寄り駅のアクセス (常に3つの要素を確保)
            access_elements = child.find(class_='cassetteitem_detail-col2').find_all(class_='cassetteitem_detail-text')
            for i in range(3):
                data_home.append(access_elements[i].text if i < len(access_elements) else "")
                    
            # 築年数と階数 (常に2つの要素を確保)
            age_stories_elements = child.find(class_='cassetteitem_detail-col3').find_all('div')
            for i in range(2):
                data_home.append(age_stories_elements[i].text if i < len(age_stories_elements) else "")

            # 部屋情報
            rooms = child.find(class_='cassetteitem_other')
            for room in rooms.find_all(class_='js-cassette_link'):
                data_room = []
                for id_, grandchild in enumerate(room.find_all('td')):
                    if id_ == 2:   # 階
                        data_room.append(grandchild.text.strip())
                    elif id_ == 3: # 家賃と管理費
                        data_room.append(grandchild.find(class_='cassetteitem_other-emphasis ui-text--bold').text)
                        data_room.append(grandchild.find(class_='cassetteitem_price cassetteitem_price--administration').text)
                    elif id_ == 4: # 敷金と礼金
                        data_room.append(grandchild.find(class_='cassetteitem_price cassetteitem_price--deposit').text)
                        data_room.append(grandchild.find(class_='cassetteitem_price cassetteitem_price--gratuity').text)
                    elif id_ == 5: # 間取りと面積
                        data_room.append(grandchild.find(class_='cassetteitem_madori').text)
                        data_room.append(grandchild.find(class_='cassetteitem_menseki').text)
                    elif id_ == 8: # URL
                        get_url = grandchild.find(class_='js-cassette_link_href cassetteitem_other-linktext').get('href')
                        abs_url = urllib.parse.urljoin(base_url, get_url)
                        data_room.append(abs_url)
                
                # 取得時間の追加して結合
                acquired_at = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                data_samples.append(data_home + data_room + [acquired_at])
        
        time.sleep(1)
        print(f'{page}ページ目:{len(data_samples)}件取得 Done!', flush=True)

    return data_samples

def save_csv(data_samples, save_dir, name):
    columns = [
        'category', 'building_name', 'address', 'access_1', 'access_2', 'access_3',
        'age', 'stories', 'floor', 'rent', 'admin_fee', 'deposit', 'gratuity',
        'layout', 'area', 'url', 'acquired_at'
    ]
    file_path = os.path.join(save_dir, f'{name}.csv')
    df = pd.DataFrame(data_samples, columns=columns)
    df.to_csv(file_path, index=False, encoding='utf-8-sig')
    return file_path

if __name__ == "__main__":
    # 東京駅から90分、乗り換え無し。1K/1DK/1LDK。鉄筋系/鉄骨系の検索URL
    base_url = "https://suumo.jp/jj/chintai/ichiran/FR301FC001/?ar=030&ta=13&bs=040&ekInput=25620&tj=90&nk=0&ct=9999999&cb=0.0&md=02&md=03&md=04&kz=1&kz=2&et=9999999&mt=9999999&mb=0&cn=9999999&shkr1=03&shkr2=03&shkr3=03&shkr4=03&fw2=&pc=30&page={}"
    
    save_dir = "./data"
    os.makedirs(save_dir, exist_ok=True)
    
    # ※全ページ取得には時間がかかるので注意
    data = get_suumo_data(base_url, max_page=4236, start_page=1)
    save_csv(data, save_dir, "tokyo_90_all_0_4236")

2. データの整形と「東京駅までの所要時間」の取得

物件データは取れたが、ここでひとつ問題がある。
SUUMOのデータには「最寄り駅」は記載されているが、「その最寄り駅から東京駅まで何分かかるのか」がわからないのだ。

分析するには駅ごとの所要時間の対応表が欲しい。ということで、これもYahoo!乗換案内をスクレイピングしてサクッと作成する。
ちなみに、この辺りのデータクレンジングやマージ処理のコードは丸々Geminiに書いてもらった。本当に便利な時代になったものだ。

以下のコードは marimo を使ってインタラクティブに処理している部分の抜粋だ。

import numpy as np
import pandas as pd
import re
import urllib.parse
import time
import requests
from bs4 import BeautifulSoup

def clean_suumo_data(df_raw: pd.DataFrame) -> pd.DataFrame:
    """SUUMOのスクレイピングデータを分析用に整形(「万円」の数値化など)"""
    df = df_raw.copy()
    # アクセス情報から駅名と徒歩分数を抽出
    for i in range(1, 4):
        col = f'access_{i}'
        if col not in df.columns: continue
        s = df[col].astype(str).replace('nan', '')
        df[f'access_{i}_station'] = s.str.split(' ').str[0].replace('', np.nan)
        df[f'access_{i}_walk_min'] = s.str.extract(r'歩(\d+)分')[0].astype(float)
        df = df.drop(columns=[col])

    # 金額データの数値化(万円を円に統一)
    for col in ['rent', 'admin_fee', 'deposit', 'gratuity']:
        if col in df.columns:
            s = df[col].astype(str).replace(['-', 'nan', ' ', ''], '0')
            is_man = s.str.contains('万円', na=False)
            nums = s.str.extract(r'([\d\.]+)')[0].astype(float).fillna(0.0)
            df[col] = np.where(is_man, nums * 10000, nums)
    return df

def create_station_time_mapping(unique_stations: np.ndarray) -> pd.DataFrame:
    """Yahoo!乗換案内から東京駅までの所要時間を取得"""
    station_times = {}
    headers = {"User-Agent": "Mozilla/5.0"}

    for st_raw in unique_stations:
        station_clean = st_raw.split('/')[-1] if '/' in st_raw else st_raw
        url = f"https://transit.yahoo.co.jp/search/result?from={urllib.parse.quote(station_clean)}&to={urllib.parse.quote('東京')}"
        
        time_min = None
        try:
            res = requests.get(url, headers=headers)
            soup = BeautifulSoup(res.text, 'html.parser')
            route1_time = soup.select_one('#route01 .time')
            if route1_time:
                text = route1_time.get_text()
                m_hour_min = re.search(r'(\d+)時間(\d+)分', text)
                m_min = re.search(r'(\d+)分', text)
                if m_hour_min:
                    time_min = float(int(m_hour_min.group(1)) * 60 + int(m_hour_min.group(2)))
                elif m_min:
                    time_min = float(m_min.group(1))
        except Exception:
            pass
        station_times[st_raw] = time_min
        time.sleep(2) # 負荷軽減

    return pd.DataFrame(list(station_times.items()), columns=['station_raw', 'time_to_tokyo_min'])

# 実行部分(イメージ)
# df_clean = clean_suumo_data(pd.read_csv("data/tokyo_90_all_0_4236.csv"))
# unique_stations = pd.unique(df_clean[['access_1_station', 'access_2_station', 'access_3_station']].values.ravel())
# df_times = create_station_time_mapping([st for st in unique_stations if str(st) != 'nan'])
# df_final = pd.merge(...) # 物件データと時間データを結合してサマリーを作成

データクレンジングを行い、異常値(パースに失敗した駅など)を取り除いて、分析用の綺麗なデータセットを準備した。

3. まずは可視化:移動時間と家賃の関係

データが整ったので、まずはシンプルに 「東京駅までの移動時間」と「平均家賃(家賃+管理費)」 をプロットしてみる。

newplot.png

当たり前だが、移動距離が遠くなる(時間がかかる)につれて、家賃はきれいに低くなっている。 経験則通りの結果だが、データとしてはっきりと右肩下がりのトレンドが出ているのを見ると気持ちがいい。

グラフの左上、家賃35万円あたりにいくつか強烈な「外れ値」がある。確認してみると、神谷町駅、外苑前駅、虎ノ門ヒルズ駅などだった。そりゃまぁ高いのも納得である。

4. 本題:本当にお得な「コスパ最強駅」はどこか?

ここからが本題。本当に知りたいのは「単に家賃が安い駅」ではなく、「移動時間の割に家賃が安い(=相場よりお得な)駅」はどこなのか、ということだ。

散布図のデータに対して回帰分析(1次式)を行い、「適正家賃(相場)」のトレンドラインを引く。そして、実際の平均家賃がそのトレンドラインからどれくらい下回っているか(安くなっているか)を計算してランキング化してみた。

結果がこちらだ。

🏆 東京駅通勤:コスパ最強駅ランキング TOP10(相場よりどれだけお得か)
------------------------------------------------------------
 1位: 五月台駅       (東京まで 51.0分) - 平均家賃:  4.0万円 ✨[相場より 9.41万円 お得!]
 2位: 黒川駅         (東京まで 61.0分) - 平均家賃:  4.4万円 ✨[相場より 8.89万円 お得!]
 3位: 京王永山駅     (東京まで 53.0分) - 平均家賃:  4.8万円 ✨[相場より 8.52万円 お得!]
 4位: はるひ野駅     (東京まで 57.0分) - 平均家賃:  4.9万円 ✨[相場より 8.43万円 お得!]
 5位: 小田急永山駅   (東京まで 62.0分) - 平均家賃:  4.9万円 ✨[相場より 8.36万円 お得!]
 6位: 聖蹟桜ヶ丘駅   (東京まで 56.0分) - 平均家賃:  5.0万円 ✨[相場より 8.33万円 お得!]
 7位: 青梅駅         (東京まで 77.0分) - 平均家賃:  5.0万円 ✨[相場より 8.20万円 お得!]
 8位: 松が谷駅       (東京まで 69.0分) - 平均家賃:  5.3万円 ✨[相場より 7.95万円 お得!]
 9位: 中央大学・明星大学駅 (東京まで 76.0分) - 平均家賃:  5.2万円 ✨[相場より 7.94万円 お得!]
10位: 京王多摩センター駅  (東京まで 61.0分) - 平均家賃:  5.4万円 ✨[相場より 7.94万円 お得!]
------------------------------------------------------------

小田急多摩線・京王相模原線エリア(五月台、黒川、永山など)が上位を独占! 東京駅まで1時間前後で通えるにも関わらず、家賃相場が4万円台に収まっており、都心の相場トレンドから計算すると8〜9万円も割安という結果になった。

傾向として、やはり遠いほうが「お得率(トレンドラインからの乖離)」は高くなるようだ。

おわりに

とりあえず、今回はここまで。
「なんとなく」で探していた物件選びが、データとコードの力で一気にクリアになっていく感覚はとても楽しい。

自分の満足する物件が見つかるまで、条件を変えながらこの検証を繰り返していこうと思う。
引っ越しを控えているエンジニアの方は、ぜひご自身の希望条件に合わせてコードを走らせてみてはいかがだろうか。

自分も希望の物件が見つかるまで、諸々分析をし続けようと思う。失敗したくないのと、何より楽しいがある。また、続きがあれば、更新する。

github

めちゃくちゃ汚いが、ここにある。気が向けば諸々きれいにする。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?