新潟県燕市のコミュニティバスの1日の動きを shapeデータを作成して可視化してみた

# 1. はじめに

以前の記事「[新潟県燕市のコミュニティバスの1日の動きを可視化してみる](https://qiita.com/bebebefu/items/a973233bbaf0f13918a5)」では、燕市の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)](https://www.city.tsubame.niigata.jp/soshiki/toshi_seibi/3/6/11802.html)

- **西沢ツール GTFS形状データ作成ツール（v4.22）：**  
  [ここからダウンロードしました（2025/3/14）](https://gtfs-gis.jp/gtfs/)
- **西沢ツールの使用方法で参考にした資料：**
   兵庫県が公開している[GTFS経路形状作成ツール演習](https://web.pref.hyogo.lg.jp/ks05/documents/20221128-text3.pdf)

- **Mobmap Web:**
可視化ツールとして[Mobmap Web](https://mobmap.locationmind.com/)を使いました。（作成2025/3/16）


# 4. 作成手順
## 4.1. `shape.txt` の生成

1. **燕市のGTFSデータをダウンロード**
   - 燕市公式サイトから `stops.txt`, `stop_times.txt`, `trips.txt` を取得

2. **西沢ツール（v4.22）で shape.txt を生成**
   - 公開されているPDFの経路図を参考に経路を手作業で一つひとつ選択ていき、`shape.txt` を出力（ここは手作業が必要です）

## 4.2. Python によるデータ統合と補間処理



```python:bus_shape.py
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を入れています。

なお、対象にしたバス路線は以下になります。各路線ごとにパーティクルの色を分けています。

- 燕市循環バス「スワロー号」：青色
- 燕市コミュニティバス実証運行：緑色
- 弥彦・燕広域循環バス「やひこ号」：赤色

<iframe width="560" height="315" src="https://www.youtube.com/embed/FUIcjrljBIA?si=FcI0fsrhNP4MBFfe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 5. まとめ
ぎこちない動きもありますが、以前の記事「[新潟県燕市のコミュニティバスの1日の動きを可視化してみる](https://qiita.com/bebebefu/items/a973233bbaf0f13918a5)」とほぼ同じ動きを再現できたと思います。今回は、西沢ツールの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](https://creativecommons.org/licenses/by/4.0/legalcode.ja)