1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

1. はじめに

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

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

2. 使用データと使用ツール

  • Mobmap Web:
    可視化ツールとしてMobmap Webを使いました。(作成2025/3/29)

3. PythonでのGTFSデータ整形

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

  • 時刻表の時刻を基にバス停の間を1分間隔で直線的に補間
  • 補間点を最も近いshapeデータ(実際の路線形状)に投影し、これをバス停間を移動しているときの移動点とする
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日の動きを可視化してみたで作成した以下のコードをそのまま使いました。

4. Mobmap Web用データの作成

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

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

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

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時からの動きです。始発のバスはその前から動いていますが、私の環境ではなぜかデータ取得がうまくいきませんでしたので、本動画では対象外とさせていただきました。

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

  • 越後交通の路線バス:赤色
  • 燕市コミュニティバス:青色
  • 三条市コミュニティバス」黄色

6. まとめ

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

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

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

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

注:

この記事は、以下の著作物を改変して利用しています。

CC BY 2.1(https://creativecommons.org/licenses/by/2.1/jp/deed.ja)

CC BY 4.0 https://creativecommons.org/licenses/by/4.0/legalcode.ja

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?