こんにちは。東京のように様々な路線が入り組んでいる地域では、しばしば、歩けばすぐなのに、電車だけで移動しようとするとやたらと遠回りをさせられる駅(のペア)というのが存在します。いくつか(東京の人にとって)身近な例を挙げます。
- 蒲田 ⇔ 京急蒲田
- 距離にして 800m ほどだが、電車だけで行き来するにはわざわざ品川まで行って乗り換える必要がある
- 板橋 ⇔ 下板橋
- 距離にして 500m ほどだが、電車だけで行き来するにはわざわざ池袋まで行って乗り換える必要がある
このような駅のペアについて、最も極端な例はどこなのでしょうか?駅や路線についての情報を公開している 駅データ.jp のデータを使って、全国の駅を対象に調べてみました。
問題の整理
まず、上述の「電車だけで移動しようとするとやたらと遠回りをさせられる駅のペア」について、定量的なスコアを定める必要があります。今回はシンプルに以下のように設定してみました。
$$ s(x, y) = \frac{最短ルートでの移動距離(x, y)}{直線距離(x, y)} $$
最短ルートでの移動距離については、駅を頂点、駅間の隣接を辺、駅間の直線距離を辺の重みとする無向非負重み付きグラフを構築し、ダイクストラ法で最短ルートとそのコストを求めます。
(厳密には駅と駅との間は必ずしも直線で結ばれているわけではありませんが、正確に線路の長さを調べるのはあまりにも大変そうだったので、今回は駅と駅は直線で結ばれているものと仮定します。同様に、駅の標高についても無視して、すべての駅は海抜 0m に位置していることを仮定します。)
また、駅間の「直線距離」については、 Haversine 公式 を使って計算します。地球が楕円体であることを考慮できていませんが、今回の検証は日本国内のみを対象としているので、ここの誤差が順位に影響を及ぼすことはないでしょう。
アルゴリズム
以下に擬似コードを示します。
best_score = 0
best_pair = (null, null)
for x in stations:
for y in stations:
score = score(x, y)
if score > best_score
best_score = score
best_pair = (x, y)
ダイクストラ法を $n^2$ 回実行している…と思いきや、「駅 X から他の全ての駅までの最短距離のリスト」はダイクストラ法 1 回で計算できるので、オーダーは $ O(n^2 \log n) $ で済みます。駅は 10000 個くらいなので、 競技プログラミングならアウトですがローカルで遊ぶなら十分高速に求まるでしょう。
(今回は駅の隣接構造が対象なので、グラフが比較的疎であることを仮定しています。)
実際のコードは長いので記事の最後に掲載します。
結果
ということで実行してみたところ、以下の結果が得られました。
西武秩父 御花畑
(84.99251091551496, 0.25699414395876485)
西武秩父 -> 横瀬 -> 芦ヶ久保 -> 正丸 -> 西吾野 -> 吾野 -> 東吾野 -> 武蔵横手 -> 高麗 -> 東飯能 -> 高麗川 -> 毛呂 -> 越生 -> 明覚 -> 小川町 -> 竹沢 -> 折原 -> 寄居 -> 波久礼 -> 樋口 -> 野上 -> 長瀞 -> 上長瀞 -> 親鼻 -> 皆野 -> 和銅黒谷 -> 大野原 -> 秩父 -> 御花畑
西武秩父駅 と 御花畑駅 の間は、直線距離にして 250m ほどしか離れていないにも関わらず、電車だけで移動しようと思うと、以下に示す 85km もの長大なルートを辿らねばならないようです。
- 西武秩父駅で西武秩父線に乗り、東飯能駅へ
- JR八高線に乗り換え、寄居駅へ
- 秩父鉄道に乗り換え、御花畑駅へ到着
ところが Wikipedia をよく見てみると西武秩父線と秩父鉄道は直通運転を行っており、御花畑駅から(西武秩父駅の隣の) 影森駅 へは直接移動できるようです。どうやら隣駅データの不備のようなので、ダウンロードしたデータを手動で修正し、御花畑駅と影森駅とが隣接していることにして再度実行してみたところ、今度は以下のような結果になりました。
豊田市 新豊田
(42.56331648677354, 0.2594896328164014)
豊田市 -> 上挙母 -> 土橋 -> 竹村 -> 若林 -> 三河八橋 -> 三河知立 -> 知立 -> 牛田 -> 新安城 -> 宇頭 -> 矢作橋 -> 中岡崎 -> 北岡崎 -> 大門 -> 北野桝塚 -> 三河上郷 -> 永覚 -> 末野原 -> 三河豊田 -> 新上挙母 -> 新豊田
豊田市駅 と 新豊田駅 の間は、やはり直線距離にして 250m ほどしか離れていない(どころか、遊歩道で繋がっている)にも関わらず、電車だけで移動しようと思うと、以下に示す 42km のルートを辿らねばならないようです。
- 豊田市駅で名鉄三河線に乗り、知立駅へ
- 名鉄名古屋本線に乗り換え、岡崎公園前駅(中岡崎駅)へ
- 中岡崎駅で愛知環状鉄道線に乗り換え、新豊田駅へ到着
色々と調べてみても、相互乗り入れなどでこのルートよりも短く行く方法はなさそうです。というわけで、豊田市 ⇔ 新豊田 が、最も「歩けばすぐなのに、電車だけで移動しようとするとやたらと遠回りをさせられる」駅のペアであることがわかりました!おめでとうございます!
この記事を見た電車マニア YouTuber の方はぜひ「豊田市から新豊田まで電車で行ってみた!」という動画を出してください。
おまけ検証
さて、最高記録は豊田市だったわけですが、都民としては関東近郊の駅でどうなるのかが気になるところです。ということで、皇居から 100km 圏内の駅に絞って探索してみます。
川越市 本川越
(56.53761304045153, 0.37926609714786125)
川越市 -> 川越 -> 西川越 -> 的場 -> 笠幡 -> 武蔵高萩 -> 高麗川 -> 東飯能 -> 飯能 -> 元加治 -> 仏子 -> 入間市 -> 稲荷山公園 -> 武蔵藤沢 -> 狭山ヶ丘 -> 小手指 -> 西所沢 -> 所沢 -> 航空公園 -> 新所沢 -> 入曽 -> 狭山市 -> 新狭山 -> 南大塚 -> 本川越
- 300m ほどですが、東飯能と所沢で乗り換えて 56km の道のりです。
大阪エリアではどうでしょうか?なんば駅から 100km 圏内に絞ります。
河内磐船 河内森
(42.151604514132515, 0.3085725237667945)
河内磐船 -> 星田 -> 寝屋川公園 -> 忍ケ丘 -> 四条畷 -> 野崎 -> 住道 -> 鴻池新田 -> 徳庵 -> 放出 -> 鴫野 -> 蒲生四丁目 -> 関目成育 -> 森小路 -> 千林 -> 滝 井 -> 土居 -> 守口市 -> 西三荘 -> 門真市 -> 古川橋 -> 大和田 -> 萱島 -> 寝屋川市 -> 香里園 -> 光善寺 -> 枚方公園 -> 枚方市 -> 宮之阪 -> 星ヶ丘 -> 村野 -> 郡津 -> 交野市 -> 河内森
九州でも見てみます。熊本駅から 100km 圏内。
二日市 紫
(24.364659554838546, 0.3782880365256588)
二日市 -> 天拝山 -> 原田 -> けやき台 -> 基山 -> 立野 -> 小郡 -> 大保 -> 三沢 -> 三国が丘 -> 津古 -> 筑紫 -> 桜台 -> 朝倉街道 -> 紫
検証に用いたコード
同じディレクトリ内に 駅データ.jp でダウンロードできる station{yyyymmdd}.csv
と join{yyyymmdd}.csv
を置いて実行してください。ちょっと(私の PC で 2 分くらい)時間がかかるので tqdm を入れていますが、それ以外は標準ライブラリだけで動きます。不要な場合は適宜外してください。
また、 RAM をそれなりに食うので 8GB のマシンだと厳しいと思います。
import heapq
import itertools
from dataclasses import dataclass
from math import sin, cos, acos, radians, isinf
from pathlib import Path
from typing import NewType
from tqdm import tqdm
StationId = NewType('StationId', int)
StationGroupId = NewType('StationGroupId', int)
@dataclass(eq=True, frozen=True)
class Station:
id: StationId
group_id: StationGroupId
name: str
longitude: float
latitude: float
def compute_distance(station1: Station, station2: Station) -> float:
R = 6378.137 # equatorial radius in km
x1 = radians(station1.longitude)
y1 = radians(station1.latitude)
x2 = radians(station2.longitude)
y2 = radians(station2.latitude)
dist = R * acos(sin(y1) * sin(y2) + cos(y1) * cos(y2) * cos(x2 - x1))
return dist
def list_route_distances(graph: dict[StationGroupId, list[tuple[StationGroupId, float]]], start: StationGroupId):
distances = { node: float('inf') for node in graph }
distances[start] = 0
heap = [(0.0, start)]
while heap:
current_distance, current_node = heapq.heappop(heap)
if current_distance > distances[current_node]:
continue
for neighbour, neighbour_distance in graph[current_node]:
distance = current_distance + neighbour_distance
if distance < distances[neighbour]:
distances[neighbour] = distance
heapq.heappush(heap, (distance, neighbour))
return distances
def compute_route(graph: dict[StationGroupId, list[tuple[StationGroupId, float]]], start: StationGroupId, end: StationGroupId):
distances = { node: float('inf') for node in graph }
distances[start] = 0
previous_nodes: dict[StationGroupId, StationGroupId | None] = { node: None for node in graph }
heap = [(0.0, start)]
while heap:
current_distance, current_node = heapq.heappop(heap)
if current_distance > distances[current_node]:
continue
for neighbour, neighbour_distance in graph[current_node]:
distance = current_distance + neighbour_distance
if distance < distances[neighbour]:
distances[neighbour] = distance
previous_nodes[neighbour] = current_node
heapq.heappush(heap, (distance, neighbour))
path: list[StationGroupId] = []
current = end
while current is not None:
path.append(current)
current = previous_nodes[current]
path.reverse()
return path
def main():
stations: dict[StationGroupId, Station] = {}
station_group_map: dict[StationId, StationGroupId] = {}
for line in [*Path('.').glob('station*.csv')][0].read_text().strip().splitlines()[1:]:
(
station_cd,
station_g_cd,
station_name,
_station_name_k,
_station_name_r,
_line_cd,
_pref_cd,
_post,
_address,
lon,
lat,
_open_ymd,
_close_ymd,
_e_status,
_e_sort
) = line.split(',')
station = Station(
StationId(int(station_cd)),
StationGroupId(int(station_g_cd)),
station_name,
float(lon),
float(lat)
)
stations[StationGroupId(int(station_g_cd))] = station
station_group_map[StationId(int(station_cd))] = StationGroupId(int(station_g_cd))
graph: dict[StationGroupId, list[tuple[StationGroupId, float]]] = {}
for line in [*Path('.').glob('join*.csv')][0].read_text().strip().splitlines()[1:]:
(
_line_cd,
station_cd1,
station_cd2
) = line.split(',')
if len(station_cd1) == 6 or len(station_cd2) == 6:
continue
station1 = stations[station_group_map[StationId(int(station_cd1))]]
station2 = stations[station_group_map[StationId(int(station_cd2))]]
dist = compute_distance(station1, station2)
if station1.group_id not in graph:
graph[station1.group_id] = []
graph[station1.group_id].append((station2.group_id, dist))
if station2.group_id not in graph:
graph[station2.group_id] = []
graph[station2.group_id].append((station1.group_id, dist))
straight_distances: dict[tuple[StationGroupId, StationGroupId], float] = {}
for source_station_group_id, destination_station_group_id in tqdm(itertools.combinations(stations.keys(), r=2), total=len(stations) * (len(stations) - 1) // 2):
source_station = stations[source_station_group_id]
destination_station = stations[destination_station_group_id]
dist = compute_distance(source_station, destination_station)
straight_distances[(source_station_group_id, destination_station_group_id)] = dist
straight_distances[(destination_station_group_id, source_station_group_id)] = dist
max_score = 0.0
max_pair = (StationGroupId(0), StationGroupId(0))
max_distances = (0.0, 0.0)
for source_station_group_id, _ in tqdm(stations.items()):
if source_station_group_id not in graph:
continue
max_score_for_station = 0.0
max_dest = StationGroupId(0)
max_distances_for_station = (0.0, 0.0)
for destination_station_group_id, route_distance in list_route_distances(graph, source_station_group_id).items():
if source_station_group_id == destination_station_group_id:
continue
if isinf(route_distance):
continue
straight_distance = straight_distances[(destination_station_group_id, source_station_group_id)]
score = route_distance / straight_distance
if score > max_score_for_station:
max_score_for_station = score
max_dest = destination_station_group_id
max_distances_for_station = (route_distance, straight_distance)
if max_score_for_station > max_score:
max_score = max_score_for_station
max_pair = (source_station_group_id, max_dest)
max_distances = max_distances_for_station
print(stations[max_pair[0]].name, stations[max_pair[1]].name)
print(max_distances)
path = compute_route(graph, max_pair[0], max_pair[1])
print(' -> '.join([ stations[station_group_id].name for station_group_id in path ]))
if __name__ == "__main__":
main()