新潟県燕市のコミュニティバスの1日の動きを可視化してみる

# はじめに
　「新潟のバス2社のGTFS Realtimeデータを可視化してみる」の記事[[1]](#参考サイト)にてコミュニティバスが街中を縫うようにして動いていくことを可視化したいと述べていましたが、それについて取り組んでみました。
　具体的には新潟県燕市のコミュニティバスの1日の運行パターンを、公開されているGTFSデータを利用して、時刻表通りに動いた場合を可視化してみました。

注：これは完全に私の個人的な趣味でやりました、燕市さんと私はなんの関係もないです。

# 用いたデータ
GTFSデータとは何か？というところについては@kohei-otaさんの記事[[2]](#参考サイト)を参考にしていただくとして、今回は以下のデータを対象としました。

- 対象バス：
    - 燕市循環バス「スワロー号」
    - 燕市コミュニティバス実証運行
    - 弥彦・燕広域循環バス「やひこ号」
- 利用データ：[[3]](#参考サイト)で公開されている燕市コミュニティバスのGTFSデータ
- 可視化ツール：Mobmap [[4]](#参考サイト)

# アニメーション化した結果
対象バスは以下になります。各路線ごとにパーティクルの色を分けています。
- 燕市循環バス「スワロー号」（青色：燕三条駅行き　水色：長辰行き）
- 燕市コミュニティバス実証運行（緑色：燕駅行き　黄緑色：新生町行き）
- 弥彦・燕広域循環バス「やひこ号」（オレンジ色：ビジョンよしだ行き　赤色：てまりの湯行き）
<iframe width="560" height="315" src="https://www.youtube.com/embed/FVc9fm7ulaU?si=FcI0fsrhNP4MBFfe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

# 手順
バスが動く様子を前述のようにアニメーションするために以下の手順を踏みました。

## 1. データ取得
 [https://www.city.tsubame.niigata.jp/soshiki/toshi_seibi/3/6/11802.html](https://www.city.tsubame.niigata.jp/soshiki/toshi_seibi/3/6/11802.html)で公開されているGTFSデータ（zip化されたtxtファイル）を取得。

## 2. 疑似GTFS-Realtimeの作成
公開されているデータはGTFSデータであり、リアルタイムのバスの位置が記載されているGTFS-Realtimeデータではなかったため、以下のコードで1分ごとの「疑似GTFS-Realtime」データ（バスのリアルタイムの位置を表すVehiclePositionの.binファイル）を生成しました。
```python:
import pandas as pd
from google.transit import gtfs_realtime_pb2
import time
from datetime import datetime, timedelta
import re

# GTFSデータファイルの読み込み
stop_times_df = pd.read_csv('stop_times.txt', encoding='utf-8')
stops_df = pd.read_csv('stops.txt', encoding='utf-8')

bus_info = pd.merge(stop_times_df, stops_df, on='stop_id')

# 時刻のフォーマット変更 (HH:MM:SS)
bus_info['arrival_time'] = pd.to_timedelta(bus_info['arrival_time'].apply(lambda x: f'0 days {x}'))

# trip_idから特定の文字列を抽出する関数
def extract_identifier(trip_id):
    hour_match = re.search(r'(\d{2})時', trip_id)
    minute_match = re.search(r'(\d{2})分', trip_id)
    system_number_match = re.search(r'_系統(\d+)', trip_id)
    
    if hour_match and minute_match and system_number_match:
        hour = hour_match.group(1)
        minute = minute_match.group(1)
        system_number = system_number_match.group(1)
        return f'{hour}{minute}{system_number}'
    return 'unknown'

# 初期時間と終了時間の設定（年月日なし）
start_time = timedelta(hours=6, minutes=30)
end_time = timedelta(hours=18)

# 基準日の設定（任意の日付）
base_date = datetime(2023, 1, 1)

# 各バスの最終到着時刻を計算
last_arrival_times = bus_info.groupby('trip_id')['arrival_time'].max()

# 60秒間隔でループ
current_time = start_time
while current_time <= end_time:
    feed = gtfs_realtime_pb2.FeedMessage()
    
    # 必要なヘッダー情報を追加
    feed.header.gtfs_realtime_version = '2.0'
    timestamp = base_date + current_time
    feed.header.timestamp = int(time.mktime(timestamp.timetuple()))

    for trip_id, last_arrival_time in last_arrival_times.items():
        if current_time <= last_arrival_time:
            trip_stops = bus_info[(bus_info['trip_id'] == trip_id) & (bus_info['arrival_time'] == current_time)]
            
            if not trip_stops.empty:
                closest_stop = trip_stops.tail(1).iloc[0]
                # print(closest_stop)

                entity = feed.entity.add()
                extracted_trip_id = extract_identifier(trip_id)
                entity.id = extracted_trip_id
                
                vehicle = entity.vehicle
                vehicle.trip.trip_id = extracted_trip_id
                vehicle.position.latitude = closest_stop['stop_lat']
                vehicle.position.longitude = closest_stop['stop_lon']
                vehicle.timestamp = feed.header.timestamp

    # バイナリ形式でデータをシリアライズ
    data = feed.SerializeToString()

    # ファイル名の生成 (例：gtfs_realtime_data_06_30_15.bin)
    total_seconds = int(current_time.total_seconds())
    hours, remainder = divmod(total_seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    filename = f'gtfs_realtime_data_{hours:02d}_{minutes:02d}_{seconds:02d}.bin'

    # バイナリデータをファイルに書き出し
    with open(filename, 'wb') as file:
        file.write(data)

    # 次の時間へ
    current_time += timedelta(seconds=60)

```
コード内では、取得した.txtファイルのうち各バス停の緯度経度を表すstops.txtと時刻表を表すstop_times.txtを読み込んでいますが、元々のファイルにはバス停間の情報が含まれていません。そのため、単純に「疑似GTFS-Realtime」データを生成するだけではバスの動きを再現できません。この課題に対処するため、路線図を確認してバス停間の通過点をstops.txtに手動で追記し、その通過点と通過時刻をstop_times.txtに手動で追加しました。（かなり大変でした。あとあと調べたところエクセルベースのツールがあるようです。）

## 3. 作成した疑似GTFS-Realtimeの処理
.binファイルを蓄積したのと同じディレクトリにMobmapにアップロードできる形式のcsvファイルを作成するpythonコードを作成しました。

```python:
import os
import pandas as pd
import pytz
from google.transit import gtfs_realtime_pb2

def read_bin_file(file_path):
    # GTFS Realtimeデータの読み込み
    feed = gtfs_realtime_pb2.FeedMessage()
    with open(file_path, 'rb') as file:
        feed.ParseFromString(file.read())

    # データの抽出
    data_list = []
    for entity in feed.entity:
        if entity.HasField('vehicle'):
            vehicle = entity.vehicle
            route_id = vehicle.trip.trip_id[-6:]
            route_id_w = None 
            if route_id == '101001' or route_id == '101002':
                route_id_w = 0
            if route_id == '102001':
                route_id_w = 1
            if route_id == '201001' or route_id == '201002':
                route_id_w = 2
            if route_id == '202001' or  route_id == '202002':
                route_id_w = 3
            if route_id == '301001': 
                route_id_w = 4
            if route_id == '302001':
                route_id_w = 5
            data_list.append({
                'id': vehicle.trip.trip_id,
                'timestamp': vehicle.timestamp,
                'latitude': vehicle.position.latitude,
                'longitude': vehicle.position.longitude,
                'route_id': route_id_w
            })
    
    return data_list

def main():
    directory_path = '.'  # ファイルが存在するディレクトリのパス
    bin_files = [f for f in os.listdir(directory_path) if f.endswith('.bin')]
    # ファイル名に基づいて時系列順にソート
    bin_files.sort()

    all_data = []
    for file in bin_files:
        file_path = os.path.join(directory_path, file)
        file_data = read_bin_file(file_path)
        all_data.extend(file_data)

    # DataFrameに変換
    df = pd.DataFrame(all_data)

    # UNIXtimestampを日時に変換
    japan_timezone = pytz.timezone('Asia/Tokyo')
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s').dt.tz_localize(pytz.utc).dt.tz_convert(japan_timezone)

    # 時系列順にソート
    df.sort_values('timestamp', inplace=True)

    # CSVファイルに保存（既存のデータに追加）
    df.to_csv('tsubame_gtfs_realtime_data.csv', mode='a', header=False, index=False)

if __name__ == "__main__":
    main()
```

## 4.　Mobmap Webに読み込み
3.で作成したcsvファイルを[Mobmap Webの使い方](https://mobmap.locationmind.com/doc/)で公開されている方法に従いcsvファイルをMobmap Webにアップロードしました。各路線ごとにパーティクルの色を分けるため、[[5]](#参考サイト)に説明がある「追加属性の読み込みと利用」を参考に色分けしました。

# 考察と課題
　通勤・通学の時間帯よりも、日中にコミュニティバスがより頻繁に運行されていると感じました。これは、バスの行先が病院や役所や観光地などであるため、その施設の営業時間に合わせているのではないかと思いました。
　またコミュニティバスの特性から、通常の路線バスよりも運行のメリハリがわかりにくいのでないかと思っていましたが、実際にはバスが頻繁に停車するエリアと、速く移動するエリアの差が明確になりました。今回は、バスの動きを可視化することが主目的であり、具体的な運行の理由や背後にある要因の分析は行っていません。そのため周辺施設の利用者数や営業時間などを考慮して分析すると、なぜそのようなメリハリのある運行をしているのかといった知見が得られるかもしれません。

本記事の課題としては以下が挙げられます。
- 作成した疑似GTFS-Realtimeデータが１分毎のデータで解像度が荒く、かつ私が行ったバス停間の通過点の設定も適当なところがあるため、可視化したバスの動きがぎこちない。

-  燕市内のコミュニティバスの位置情報だけの可視化では地域内の交通事情が理解しきれない。

時間があればこれらの課題に対処していく予定です。例えば、隣の三条市のコミュニティバスや越後交通の路線バスや電車の動きも合わせて可視化すれば、連携している様子や興味深いパターンが見つかるかもしれません。同時に、周辺施設の利用者数や営業時間などを人の動きとして可視化対象に組み込むことも面白いかもしれないと考えています。

# 参考サイト
[1] [新潟のバス2社のGTFS Realtimeデータを可視化してみる](https://qiita.com/bebebefu/items/9d0be6bfcd1e86a21ac4)
[2] [熊本のバス5社のGTFS Realtimeオープンデータを使う](https://qiita.com/kohei-ota/items/104dac4afec818217921)
[3][標準的なバス情報フォーマット（GTFSデータ）について](https://www.city.tsubame.niigata.jp/soshiki/toshi_seibi/3/6/11802.html)
[4] [Mobmap Webリンク](https://mobmap.locationmind.com/)
[5] [Mobmapの使い方](https://mobmap.locationmind.com/doc/)

この記事は、以下の著作物を改変して利用しています。
燕市コミュニティバスのGTFSデータ 、クリエイティブ・コモンズ・ライセンス表示4.0 国際（https://creativecommons.org/licenses/by/4.0/legalcode.ja）


👆バス停間の通過点をshapeデータを作成して、補間した場合の記事を以下にまとめました。

https://qiita.com/bebebefu/items/fff066673c0b9b58ad9f