突然ですが、住みたい場所はありますか?
僕は東京生まれ東京育ちですっかり東京に染まった東京ボーイなので、当然東京に住みたいです。もちろん都心に。
しかしながら都心の家賃はとんでもなく高いので、引っ越すにあたって来年から新社会人になる僕としてはつらいものがあります。
そこで、都心(というか行く頻度が高そうな駅)へのアクセスがよく、家賃相場の安い駅を探すプログラムを作成しました。
良い駅の定義
新宿駅や渋谷駅など、よく行きそうな駅は複数ありそうです。
各駅へのアクセスの良さと家賃の安さをパラメータとして、いい感じに評価付けをしたいです。
そこで、次の評価関数を用意し、評価関数の値が小さい駅を「良い駅」と定義します。
また、よく行きそうな駅のことを「目的駅」とし、新宿・渋谷・東京の 4 つの駅を目的駅とします。
式に深い意味はありません。強いていえば以下のことを考えて決定しました。
- 1 つの駅のみが極端に近くても嬉しくないので、全ての駅がそこそこ近いほうが評価が小さくなるように二乗和にした
- 家賃が安くなることの嬉しさは指数的に増える気がした
家賃情報の取得
「家賃 駅 相場」とかで検索をかけると様々な賃貸情報サイトがそれっぽい情報を提示してくれます。
今回は、その中でページ構成がいい感じで利用規約にスクレイピングを禁止する項目がなかった「SUUMO」から情報を引っ張ります(とはいえ、攻撃とならないようリクエストを投げる間隔は十分に空けます)。
駅周辺の賃貸情報は次のようになっています。
都道府県
└─路線
└─ 各停車駅周辺の相場
また、各駅周辺の家賃相場が表示されるページでは建物種別や間取りを選択できます。今回は建物種別を「マンション」、間取りを「1LDK/2K/2DK」として相場を取得します。
target_areas = ["tokyo", "chiba", "saitama", "kanagawa"]
requests_per_minute = 60
@retry(tries=3, delay=10, backoff=2)
def load_page(url):
margin = requests_per_minute / 60
html = requests.get(url)
time.sleep(margin)
soup = BeautifulSoup(html.content, "html.parser")
return soup
def get_line_url(area):
line_url = {}
area_url = "https://suumo.jp/chintai/soba/{}/ensen/".format(area)
soup = load_page(area_url)
line_ul = soup.find_all(class_="searchitem-list")
for ul in line_ul:
li_items = ul.find_all("li")
for li in li_items:
a = li.find("a")
if a != None:
line = a.getText()
url = a.get("href")
line_url[line] = url
return line_url
def get_station_rent(url):
'''
FR, ar, bs, ra, rn: 検索するときの隠しパラメータ
sort: {1: 駅順, 2:家賃高い順, 3:家賃低い順}
ts: {1: マンション, 2:アパート, 3:一戸建て・その他}
mdKbn: {01: ワンルーム, 02: 1K/1DK, 03: 1LDK/2K/2DK, 04: 2LDK/3K/3DK, 05:3LDK/4K~}
'''
station_rent = {}
line_url = "https://suumo.jp" + url
soup = load_page(line_url)
FR = soup.find(class_="ui-section-body").find("form").get("action")
ar = soup.find(class_="ui-section-body").find("input", attrs={"name": "ar"}).get("value")
bs = soup.find(class_="ui-section-body").find("input", attrs={"name": "bs"}).get("value")
ra = soup.find(class_="ui-section-body").find("input", attrs={"name": "ra"}).get("value")
rn = soup.find(class_="ui-section-body").find("input", attrs={"name": "rn"}).get("value")
sort = "1"
ts = "1"
mdKbn = "03"
rent_url = "https://suumo.jp{}?ar={}&bs={}&ra={}&rn={}&sort={}&ts={}&mdKbn={}".format(FR, ar, bs, ra, rn, sort, ts, mdKbn)
soup = load_page(rent_url)
table_list = soup.find_all(class_="js-graph-data")
for table in table_list:
td_list = table.find_all("td")
a = td_list[0].find("a")
if a != None:
station = a.getText()
else:
station = td_list[0].getText()
span = td_list[1].find(class_="graphpanel_matrix-td_graphinfo-strong")
if span != None and td_list[3].find("a") != None:
rent = float(span.getText())
else:
rent = 999
station_rent[station] = rent
return station_rent
def get_station_info():
filename = "rent.pkl"
if os.path.isfile(filename):
with open(filename, "rb") as f:
return pickle.load(f)
station_info = {}
for area in target_areas:
line_list = get_line_url(area)
for line, url in tqdm(line_list.items()):
station_rent = get_station_rent(url)
for station, rent in station_rent.items():
if station not in station_info:
station_info[station] = {
"rent": rent,
"lines": []
}
station_info[station]["lines"].append(line)
for station in station_info:
station_info[station]["lines"] = list(dict.fromkeys(station_info[station]["lines"]))
with open(filename, "wb") as f:
pickle.dump(station_info, f)
return station_info
家賃相場の情報は短期間では不変なのでローカルに保存し、プログラムの再実行時にリクエストが再び走らないようにしています。
また、相場の情報がなかったり相場は表示されていても物件情報がなかったりする駅があったので、そのあたりは除外しました。
以上により、各駅について周辺の家賃相場と停車する路線の情報が取得できました。
目的駅まで時間内に到着できる駅の取得
全ての駅について目的駅への到着時間を調べるのはあまりにも時間がかかりそうなので、目的駅から 分以内に到着できる駅を調べたい気持ちになります。
調べると NAVITIME が NAVITIME API を提供しており、やりたいことがそのままできそうです。
Rapid API を利用すると月間 500 回までは無料で API を叩けるので、これを使用します。
全ての目的駅について API を実行し得られた駅の共通部分を取れば、全ての目的駅に近い駅の一覧と、各目的駅への到着時間が取得できます。
RAPIDAPI_KEY = os.environ["RAPIDAPI_KEY"]
target_stations = ["新宿", "東京", "渋谷"]
def get_node_id(station):
url = "https://navitime-transport.p.rapidapi.com/transport_node/autocomplete"
host = "navitime-transport.p.rapidapi.com"
headers = {
"x-rapidapi-key": RAPIDAPI_KEY,
"x-rapidapi-host": host
}
params = {
"word": station,
"word_match": "prefix",
}
try:
response = requests.request("GET", url, headers=headers, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
print(f'HTTPException occurred: {err}')
return -1
else:
items = json.loads(response.text)["items"]
for item in items:
if re.match("^{}(\(東京都\)|\(埼玉県\)|\(千葉県\)|\(神奈川県\))?$".format(station), item["name"]):
return item["id"]
print("No perfect mutch was found with station: {}".format(station))
return -1
def fix_fluctuation(station):
if "(" in station:
station = station[:station.index("(")]
elif "〔" in station:
station = station[:station.index("〔")]
elif "[" in station:
station = station[:station.index("[")]
if station == "西ヶ原":
return "西ケ原"
elif station == "南阿佐ヶ谷":
return "南阿佐ケ谷"
elif station == "阿佐ヶ谷":
return "阿佐ケ谷"
elif station == "鶴ヶ峰":
return "鶴ケ峰"
elif station == "三ッ沢上町":
return "三ツ沢上町"
elif station == "千駄ヶ谷":
return "千駄ケ谷"
elif station == "保土ヶ谷":
return "保土ケ谷"
elif station == "市ヶ谷":
return "市ケ谷"
else:
return station
def get_distance(node_id, start_station):
url = "https://navitime-reachable.p.rapidapi.com/reachable_transit"
host = "navitime-reachable.p.rapidapi.com"
headers = {
"x-rapidapi-key": RAPIDAPI_KEY,
"x-rapidapi-host": host
}
params = {
"term": 60,
"start": node_id,
"unuse": "domestic_flight.ferry.superexpress_train.sleeper_ultraexpress.shuttle_bus",
"transit_limit": 30,
"node_type": "station",
"limit": 2000
}
try:
response = requests.request("GET", url, headers=headers, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as err:
print(f'HTTPException occurred: {err}')
return {}
else:
items = json.loads(response.text)["items"]
distance = {}
for item in items:
station = fix_fluctuation(item["name"])
if station not in distance:
distance[station] = {
"time": int(item["time"]),
"count": int(item["transit_count"]),
}
else:
distance[station]["time"] = min(distance[station]["time"], int(item["time"]))
distance[station]["count"] = min(distance[station]["count"], int(item["transit_count"]))
distance[start_station] = {
"time": 0,
"count": 0
}
return distance
def get_distance_to_stations():
basename = "distance.pkl"
distance_list = {}
for station in target_stations:
filename = "{}-".format(station) + basename
if os.path.isfile(filename):
with open(filename, "rb") as f:
distance_list[station] = pickle.load(f)
continue
node_id = get_node_id(station)
if node_id == -1:
continue
distance = get_distance(node_id, station)
distance_list[station] = distance
with open(filename, "wb") as f:
pickle.dump(distance, f)
near_station_list = {}
for _, distance in distance_list.items():
if near_station_list == {}:
near_station_list = set(distance.keys())
else:
near_station_list.intersection_update(set(distance.keys()))
distance_to_stations = {}
for station in near_station_list:
distance_to_stations[station] = {}
for start, distance in distance_list.items():
for goal, time in distance.items():
if goal in distance_to_stations:
distance_to_stations[goal][start] = {
"time": time["time"],
"count": time["count"]
}
return distance_to_stations
家賃相場の情報と同様に、到着時間の上限を大きめに取れば各目的駅について得られた結果は使いまわせそうなのでローカルに保存します(API制限的にもうれしそう)。
NAVITIME では、駅名が重複している場合は「(東京)」のように末尾に都道府県を付与しているのでこれを削除します。今回は東京近辺しか調べないのでこれによる影響はほぼ無いです。
同様に、「(つくばエクスプレス)」のように路線名が付与されている場合もあるので、これも削除します。
また、NAVITIME と SUUMO で「ケ」と「ヶ」や「ツ」と「ッ」の表記ゆれがあるので適宜直しています。
以上により、全ての目的駅への到着時間が長くない駅のリストが取得できました。
「良い駅」の計算結果
駅周辺の家賃相場と各駅への到着時間の 2 つの情報が揃いました。あとはこれを結合して評価値の低い「良い駅」を表示していきます。
駅名: 小川町, score: 13010
家賃相場: 4.1 万円
線路: ['JR八高線', '東武東上線', '都営新宿線']
到着時間:
新宿: 14分, 乗り換え回数: 0
東京: 7分, 乗り換え回数: 0
渋谷: 23分, 乗り換え回数: 1
駅名: 霞ヶ関, score: 18226
家賃相場: 5.7 万円
線路: ['東武東上線']
到着時間:
新宿: 16分, 乗り換え回数: 0
東京: 7分, 乗り換え回数: 0
渋谷: 16分, 乗り換え回数: 1
駅名: 池袋, score: 118330
家賃相場: 13.4 万円
線路: ['JR山手線', 'JR埼京線', '湘南新宿ライン宇須', '湘南新宿ライン高海', '東京メトロ丸ノ内線', '東京メトロ有楽町線', '東京メトロ副都心線', '西武池袋線', '東武東上線']
到着時間:
新宿: 9分, 乗り換え回数: 0
東京: 17分, 乗り換え回数: 0
渋谷: 17分, 乗り換え回数: 0
駅名: 新宿, score: 119880
家賃相場: 18.0 万円
線路: ['JR山手線', 'JR中央線', 'JR埼京線', 'JR総武線', '湘南新宿ライン宇須', '湘南新宿ライン高海', '東京メトロ丸ノ内線', '都営新宿線', '都営大江戸線', '京王新線', '京王線', '小田急線']
到着時間:
新宿: 0分, 乗り換え回数: 0
東京: 17分, 乗り換え回数: 0
渋谷: 9分, 乗り換え回数: 0
駅名: 羽田空港第1・第2ターミナル, score: 122325
家賃相場: 4.6 万円
線路: ['京急空港線']
到着時間:
新宿: 49分, 乗り換え回数: 1
東京: 38分, 乗り換え回数: 1
渋谷: 44分, 乗り換え回数: 1
なんかおかしいです。
実は小川町と霞ケ関は埼玉県にもあり、同名駅の処理を適当にした結果最強の駅として出力されてしまいました。
羽田空港第1・第2ターミナルに関しては SUUMO の賃貸情報がおかしいっぽいです。
小川町・霞ケ関は都心も都心で、家賃がすごいことになってるので選択肢として挙がることはないため今回は除外します。
羽田空港~もなくて問題ないでしょう。
同様に不具合のある駅がいくつかありますが、問題なさそうなので全て除外しています。
また、近すぎるくらい近いとはいえ新宿の家賃相場は僕にはまだ高すぎるし(駅チカ物件とかだと+2万とかが相場になりそうです)、羽田空港~の渋谷まで44分もちょっと遠すぎる感じがします。
評価関数をいじるのも面倒くさいので、素直に家賃相場・到着時間の上限を設定することにします。
諸々を修正・加筆した結果が以下です。
駅名: 田端, score: 142054
家賃相場: 11.0 万円
線路: ['JR山手線', 'JR京浜東北線']
到着時間:
新宿: 18分, 乗り換え回数: 0
東京: 15分, 乗り換え回数: 0
渋谷: 25分, 乗り換え回数: 0
駅名: 高円寺, score: 165245
家賃相場: 12.9 万円
線路: ['JR中央線', 'JR総武線']
到着時間:
新宿: 8分, 乗り換え回数: 0
東京: 23分, 乗り換え回数: 0
渋谷: 20分, 乗り換え回数: 1
駅名: 西日暮里, score: 179066
家賃相場: 11.5 万円
線路: ['JR山手線', 'JR京浜東北線', '東京メトロ千代田線', '日暮里・舎人ライナー']
到着時間:
新宿: 20分, 乗り換え回数: 0
東京: 15分, 乗り換え回数: 0
渋谷: 27分, 乗り換え回数: 0
駅名: 駒込, score: 193674
家賃相場: 13.0 万円
線路: ['JR山手線', '東京メトロ南北線']
到着時間:
新宿: 16分, 乗り換え回数: 0
東京: 19分, 乗り換え回数: 0
渋谷: 23分, 乗り換え回数: 0
駅名: 阿佐ケ谷, score: 194999
家賃相場: 12.7 万円
線路: ['JR中央線', 'JR総武線']
到着時間:
新宿: 10分, 乗り換え回数: 0
東京: 25分, 乗り換え回数: 0
渋谷: 22分, 乗り換え回数: 1
う~~~ん、なんか見たことあるぞ????
よく「安くてアクセスの良い駅」としてまとめられる駅がランクインしてしまいました。
後の順位の駅を見ても赤羽とか日暮里とかが出てくるので、今回発見することのできた「良い駅」はすでに十分認知されていそうです...
まとめ
俺だけが知ってる最強の穴場、隠れ家風〇〇くらいよく知られてそう
供養します。
github.com