1. はじめに
以前の記事「新潟県燕市のコミュニティバスの1日の動きを可視化してみる」では、燕市のGTFSデータを利用してバスの動きを可視化しました。しかし、当時のアプローチでは バス停間の通過点を手作業で追加 しており、作業負担が大きいという課題がありました。
今回の記事では、その課題を解決するために 西沢ツールのGTFS形状データ作成ツール(v4.22) を活用し、 shape.txt
を生成し、そしてそこで得られた shape.txt
を既存の stops.txt
, stop_times.txt
, trips.txt
と統合し、Mobmap Web で視覚化できるようにしました。
注:これは完全に私の個人的な趣味でやりました、燕市さんと私はなんの関係もないです。
2. 取り組んだこと
燕市が公開しているGTFSデータには stops.txt
, stop_times.txt
, trips.txt
は含まれていますが、 shape.txt
がありません。そのため、
- バス停の位置座標情報や時刻表はあるが、どのルートをバスが通るのかわからない。
-
shape.txt
がないとバス停間の補間が直線的になり、道がないところをショートカットして走るなどありえない経路をたどるようになる。 - 手動でバス停間の通過点を作成するのは非常に手間がかかる。
この課題を解決するために、西沢ツールの GTFS形状データ作成ツール(v4.22) を使用して shape.txt
を作成しました。
3. 使用データと使用ツール
-
GTFSデータ:
燕市の公式サイトから取得しました。ここからダウンロードしました(2025/3/14) -
西沢ツール GTFS形状データ作成ツール(v4.22):
ここからダウンロードしました(2025/3/14) -
西沢ツールの使用方法で参考にした資料:
兵庫県が公開しているGTFS経路形状作成ツール演習 -
Mobmap Web:
可視化ツールとしてMobmap Webを使いました。(作成2025/3/16)
4. 作成手順
4.1. shape.txt
の生成
-
燕市のGTFSデータをダウンロード
- 燕市公式サイトから
stops.txt
,stop_times.txt
,trips.txt
を取得
- 燕市公式サイトから
-
西沢ツール(v4.22)で shape.txt を生成
- 公開されているPDFの経路図を参考に経路を手作業で一つひとつ選択ていき、
shape.txt
を出力(ここは手作業が必要です)
- 公開されているPDFの経路図を参考に経路を手作業で一つひとつ選択ていき、
4.2. Python によるデータ統合と補間処理
import pandas as pd
import numpy as np
import pytz
from datetime import datetime, timedelta
from scipy.spatial import KDTree
from scipy.interpolate import CubicSpline
import re
# --- データ読み込み ---
stops_df = pd.read_csv("stops.txt", encoding="utf-8")
stop_times_df = pd.read_csv("stop_times.txt", encoding="utf-8")
shapes_df = pd.read_csv("shapes.txt", encoding="utf-8")
trips_df = pd.read_csv("trips.txt", encoding="utf-8")
# --- GTFS データの前処理 ---
stop_times_df["arrival_time"] = pd.to_timedelta(stop_times_df["arrival_time"])
stop_times_df["departure_time"] = pd.to_timedelta(stop_times_df["departure_time"])
stop_times_df = stop_times_df.merge(stops_df, on="stop_id")
shapes_df.sort_values(["shape_id", "shape_pt_sequence"], inplace=True)
# `trip_id` に対応する `shape_id` と `route_id` を取得
trips_df = trips_df[["trip_id", "shape_id", "route_id"]]
stop_times_df = stop_times_df.merge(trips_df, on="trip_id")
# --- `route_id` を 0,1,2,... の連番に変換 ---
unique_route_ids = sorted(trips_df["route_id"].unique()) # route_id のユニーク値を取得しソート
route_id_mapping = {old_id: new_id for new_id, old_id in enumerate(unique_route_ids)} # 連番化
# --- Mobmapに入れるためのデータを生成 ---
mobmap_data = []
japan_timezone = pytz.timezone("Asia/Tokyo")
base_date = datetime(2025, 3, 19)
# trip_id ごとに処理
for trip_id, trip_data in stop_times_df.groupby("trip_id"):
trip_data = trip_data.sort_values("arrival_time").reset_index(drop=True)
shape_id = trip_data.loc[0, "shape_id"]
# `route_id` を連番に変換
route_id = route_id_mapping[int(trip_data.loc[0, "route_id"])]
shape_points = shapes_df.loc[shapes_df["shape_id"] == shape_id, ["shape_pt_lat", "shape_pt_lon"]].values
if len(shape_points) < 2:
continue
tree = KDTree(shape_points)
stop_indices = tree.query(trip_data[["stop_lat", "stop_lon"]].values)[1]
# trip_id から数値を抽出
trip_numeric_str = "".join(re.findall(r"\d+", trip_id))
if trip_numeric_str:
trip_numeric_id = int(trip_numeric_str)
else:
print(f"Warning: trip_id '{trip_id}' から数値が抽出できませんでした。")
continue
for i in range(len(trip_data) - 1):
start_stop = trip_data.iloc[i]
end_stop = trip_data.iloc[i + 1]
start_time = base_date + start_stop["departure_time"]
end_time = base_date + end_stop["arrival_time"]
start_idx, end_idx = sorted([stop_indices[i], stop_indices[i + 1]])
segment_points = shape_points[start_idx:end_idx + 1]
if len(segment_points) < 3:
continue # 少なすぎる場合はスキップ
# スプライン補間して経路を作成
segment_times = np.linspace(0, (end_time - start_time).total_seconds(), num=max(10, len(segment_points) * 3))
lat_spline = CubicSpline(np.linspace(0, 1, len(segment_points)), segment_points[:, 0])
lon_spline = CubicSpline(np.linspace(0, 1, len(segment_points)), segment_points[:, 1])
latitudes = lat_spline(np.linspace(0, 1, len(segment_times)))
longitudes = lon_spline(np.linspace(0, 1, len(segment_times)))
for t, lat, lon in zip(segment_times, latitudes, longitudes):
mobmap_data.append({
"id": trip_numeric_id,
"timestamp": (start_time + timedelta(seconds=t)).astimezone(japan_timezone),
"latitude": lat,
"longitude": lon,
"route_id": route_id # 連番化した route_id
})
# --- CSV ファイルに保存 ---
mobmap_df = pd.DataFrame(mobmap_data)
mobmap_df.sort_values("timestamp", inplace=True)
mobmap_df.to_csv("tsubame_gtfs_realtime_data.csv", index=False, header=True)
print("Mobmap 用の CSV ファイルが作成されました: tsubame_gtfs_realtime_data.csv")
4.3. アニメーション化した結果
燕市のコミュニティバスの動き(時刻表通りに動いた場合)をパーティクルで表した動画を以下に示します。BGMもなくバスの動きをずっと見るのは辛いと思ったので、今回は少し説明やBGMを入れています。
なお、対象にしたバス路線は以下になります。各路線ごとにパーティクルの色を分けています。
- 燕市循環バス「スワロー号」:青色
- 燕市コミュニティバス実証運行:緑色
- 弥彦・燕広域循環バス「やひこ号」:赤色
5. まとめ
ぎこちない動きもありますが、以前の記事「新潟県燕市のコミュニティバスの1日の動きを可視化してみる」とほぼ同じ動きを再現できたと思います。今回は、西沢ツールのGTFS形状データ作成ツール(v4.22)を活用し、 shape.txt
を生成し、そして得られた shape.txt
を既存の stops.txt
, stop_times.txt
, trips.txt
と統合し、Mobmap Webでバスの動きを可視化できました。
本記事により以下のことを達成できたと考えています。
- 今まで手動でやっていたバスの走行ルートを再現する手間をある程度は削減
- Pythonのプログラムを用いたバス停間の補間により、実際のバスの経路を再現
ただし、西沢ツールのGTFS形状データ作成ツール(v4.22)では、路線の選択を手動で行う必要があるため、完全な自動化には至っていません。また、バスの動きにもぎこちない部分があり、bus_shape.py
で行った補間方法にはさらなる改善の余地がありそうです。
また、燕市内のコミュニティバスの動きだけを可視化しても、地域全体の公共交通事情を把握するのは難しいと感じました。そこで、今後は隣接する三条市のコミュニティバスや、越後交通の路線バスの動きも合わせて可視化し、より包括的な分析を行いたいと考えています。
注:
この記事は、以下の著作物を改変して利用しています。
燕市コミュニティバスのGTFSデータ 、クリエイティブ・コモンズ・ライセンス表示4.0 国際 https://creativecommons.org/licenses/by/4.0/legalcode.ja