新潟県三条市のコミュニティバスと新潟県燕市のコミュニティバスと燕市周辺の越後交通の路線バスの1日の動きを可視化してみた

# 1. はじめに

前回の記事（[新潟県燕市のコミュニティバスと燕市周辺の越後交通の路線バスの1日の動きを可視化してみた](https://qiita.com/bebebefu/items/f494071378b15500872b)）では、GTFSデータとGTFS-RT（リアルタイム）データを用いて、越後交通と燕市のコミュニティバスを組み合わせた1日の運行状況を可視化しました。

その記事の中で三条市のコミュニティバスについても、可視化したいと述べました。今回その準備ができましたので、合わせて可視化してみたのがこの記事の内容になります。

# 2. 使用データと使用ツール
- **三条市のコミュニティバスのデータについて**
    - GTFSデータ：
。[ここからダウンロードしました(2025/3/17)](https://gtfs-data.jp/search?pref=%E6%96%B0%E6%BD%9F%E7%9C%8C&target_feed=sanjocity*guruttosan)

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

- **燕市のコミュニティバスのデータについて**
    - 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)


- **越後交通の路線バスのデータ**
    - GTFSリアルタイム（GTFS-RT）データ：
    2024年3月21日に取得した、越後交通のGTFS-RTデータ（vehicle_positions）。 なお、以下のサイトから５分毎にデータを取得しました

https://bus-vision.jp/nagaoka/view/opendataNagaoka.html


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

# 3. PythonでのGTFSデータ整形

三条市のコミュニティバスのデータについては、[新潟県燕市のコミュニティバスの1日の動きを shapeデータを作成して可視化してみた](https://qiita.com/bebebefu/items/fff066673c0b9b58ad9f)で作成したコードを使おうと思ったのですが、shapeデータの作り方がまずかったのかバス停間のバスの動きの補間がうまくいきませんでした。そこで以下のコードで補間することに変えました。このコードの思想の概要は以下になります。

- 時刻表の時刻を基にバス停の間を1分間隔で直線的に補間
- 補間点を最も近いshapeデータ（実際の路線形状）に投影し、これをバス停間を移動しているときの移動点とする

```python:GTFSデータの処理コード
import pandas as pd
import numpy as np
import pytz
from datetime import datetime, timedelta
from scipy.spatial import KDTree
import re

# --- データ読み込み ---
stops_df = pd.read_csv("stops.txt", encoding="utf-8")
stop_times_df = pd.read_csv("stop_times.txt", encoding="utf-8")
trips_df = pd.read_csv("trips.txt", encoding="utf-8")
shapes_df = pd.read_csv("shapes.txt", encoding="utf-8")

# --- 平日のみ抽出 ---
trips_df = trips_df[trips_df["service_id"] == "平日"]

# --- 前処理 ---
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")
stop_times_df = stop_times_df.merge(trips_df[["trip_id", "route_id", "shape_id"]], on="trip_id")
shapes_df.sort_values(["shape_id", "shape_pt_sequence"], inplace=True)

# --- shape pointのリスト化--
shape_map = {
    shape_id: group[["shape_pt_lat", "shape_pt_lon"]].to_numpy()
    for shape_id, group in shapes_df.groupby("shape_id")
}
shape_trees = {
    shape_id: KDTree(coords)
    for shape_id, coords in shape_map.items()
}

# --- route_id連番化 ---
unique_route_ids = sorted(trips_df["route_id"].unique())
route_id_mapping = {old_id: new_id for new_id, old_id in enumerate(unique_route_ids)}

# --- 2025年3月21日に設定 ---
base_date = datetime(2025, 3, 21)
japan_timezone = pytz.timezone("Asia/Tokyo")
records = []

for trip_id, group in stop_times_df.groupby("trip_id"):
    group = group.sort_values("departure_time").reset_index(drop=True)
    if len(group) < 2:
        continue

    shape_id = group.loc[0, "shape_id"]
    route_id = route_id_mapping[int(group.loc[0, "route_id"])]
    if shape_id not in shape_map:
        continue
    shape_points = shape_map[shape_id]
    shape_tree = shape_trees[shape_id]

    trip_numeric_str = "".join(re.findall(r"\d+", trip_id))
    if not trip_numeric_str:
        continue
    trip_numeric_id = int(trip_numeric_str)

    for i in range(len(group) - 1):
        stop_a = group.iloc[i]
        stop_b = group.iloc[i + 1]
        time_a = base_date + stop_a["departure_time"]
        time_b = base_date + stop_b["arrival_time"]
        duration = (time_b - time_a).total_seconds()
        if duration <= 0:
            continue

        n_points = int(duration // 60)
        for j in range(n_points):
            ratio = j / max(1, n_points - 1)
            lat_est = (1 - ratio) * stop_a["stop_lat"] + ratio * stop_b["stop_lat"]
            lon_est = (1 - ratio) * stop_a["stop_lon"] + ratio * stop_b["stop_lon"]
            _, idx = shape_tree.query([lat_est, lon_est])
            lat, lon = shape_points[idx]
            timestamp = time_a + timedelta(seconds=60 * j)
            records.append({
                "id": trip_numeric_id,
                "timestamp": timestamp.astimezone(japan_timezone),
                "latitude": lat,
                "longitude": lon,
                "route_id": route_id
            })

        # バス停も確実に含める
        records.append({
            "id": trip_numeric_id,
            "timestamp": time_b.astimezone(japan_timezone),
            "latitude": stop_b["stop_lat"],
            "longitude": stop_b["stop_lon"],
            "route_id": route_id
        })

# --- 出力 ---
df = pd.DataFrame(records)
df.sort_values("timestamp", inplace=True)
df.to_csv("gtfs_snap_to_shape.csv", index=False)
```

一方、越後交通のGTFSリアルタイム（GTFS-RT）データは[新潟県燕市のコミュニティバスと燕市周辺の越後交通の路線バスの1日の動きを可視化してみた](https://qiita.com/bebebefu/items/f494071378b15500872b)で作成した以下のコードをそのまま使いました。

# 4. Mobmap Web用データの作成
Mobmap Webでの可視化には、先ほど作成した三条市のコミュニティバスのデータと燕市コミュニティバスのデータと越後交通のGTFS-RTデータを一つのCSVにまとめて使用しました。

それぞれのデータが異なるバス事業者のデータであることを明示するため、燕市のコミュニティバスのデータは`route_id`を`0`とし、越後交通のデータは`route_id`を`1`とし、三条市のコミュニティバスのデータは`route_id`を`2`とすることで、事業者間の識別をできるようにしました。

最終的なCSVファイルは以下のような形式になっています。

```csvs:
id,timestamp,latitude,longitude,route_id
10658301001,2025-03-21 06:58:00+09:00,37.66339075,138.8242534,0
...
49906,2025-03-21 06:23:48+09:00,37.61711121,138.8234711,1
...
20720101001,2025-03-21 07:20:00+09:00,37.63088791,138.973589,2
```

# 5. アニメーション化した結果
新潟県県央地域周辺のバスの1日の動きをパーティクルで表した動画を以下に示します。
燕市と三条市のコミュニティバスは時刻表通りに動いたらという前提の動きになっています。そして越後交通の路線バスは2025/3/21のGTFS-RTデータを処理した動きになっています。なお、可視化した動きは朝7時からの動きです。始発のバスはその前から動いていますが、私の環境ではなぜかデータ取得がうまくいきませんでしたので、本動画では対象外とさせていただきました。

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

- 越後交通の路線バス：緑色
- 燕市コミュニティバス：青色
- 三条市コミュニティバス」黄色

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

# 6. まとめ
今回は、GTFSデータとGTFS-RT（リアルタイム）データを用いて、越後交通と新潟県燕市・三条市のコミュニティバスを組み合わせた1日の運行状況を可視化しました。この動画からでも、燕市・三条市とその周辺のバス路線では、一部の路線が重複しているものの、おおむね越後交通と燕市・三条市のコミュニティバスのすみ分けができていることが分かりました。

また燕市のコミュニティバスは比較的広範囲に運行されていますが、三条市のコミュニティバスは比較的市街地が中心なのかなという印象を持ちました。ただ、時刻表を見ると再現できていない路線もあるようでして、さらに調査が必要だと感じました。

また、今回のGTFSデータを補間するためのコードで作成したデータを用いて動画化を行ったところ、バスの動きがかなりぎこちないものになってしまいました。バス停間を飛び飛びに移動しているような不自然な動きになっており、この点についても改善の余地があると感じています。

今後は、上記の改善に取り組みつつも新潟交通のデータも取り込んでより可視化の対象地域を広げたりとか、他の地域のバスの動きの可視化にも取り組めたらと考えています。

## 注：
この記事は、以下の著作物を改変して利用しています。
- 三条市コミュニティバスのGTFSデータ：https://gtfs-data.jp/search?pref=%E6%96%B0%E6%BD%9F%E7%9C%8C&target_feed=sanjocity*guruttosan
三条市のコミュニティバスのHP（https://www.city.sanjo.niigata.jp/kurashi_tetsuzuki/kotsu/3/6062.html）

CC BY 2.1（https://creativecommons.org/licenses/by/2.1/jp/deed.ja）

- 燕市コミュニティバスのGTFSデータ:https://www.city.tsubame.niigata.jp/soshiki/toshi_seibi/3/6/11802.html)

- 越後交通（http://www.echigo-kotsu.co.jp/) のGTFS-RTデータ
　Bus-Vision ながおかバスi　https://bus-vision.jp/nagaoka/view/opendataNagaoka.html

CC BY 4.0 [https://creativecommons.org/licenses/by/4.0/legalcode.ja](https://creativecommons.org/licenses/by/4.0/legalcode.ja)