0
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日の動きを shapeデータを作成して可視化してみた

Posted at

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. 使用データと使用ツール

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 によるデータ統合と補間処理

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を入れています。

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

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

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

0
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
0
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?