2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

foliumを使って新潟県県央地域の路線バスの1日の動きを可視化してみた

Posted at

1. はじめに

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

この可視化には、Webサービスである「Mobmap Web(リンク)」を使用していましたが、今後は施設の利用状況なども含めて、より柔軟に可視化できる方法を模索したいと考えています。そこで今回は、その第一歩として、前回と同様の可視化をPythonの地図描画ライブラリ「folium」を使って再現することに取り組みました。

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

3. バスの動きを示すデータについて

前回の記事(新潟県三条市のコミュニティバスと新潟県燕市のコミュニティバスと燕市周辺の越後交通の路線バスの1日の動きを可視化してみた)で作成した、三条市のコミュニティバスのデータと燕市コミュニティバスのデータと越後交通の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

4. アニメーション化

アニメーション化用のプログラムを以下に示します。前回作成したデータを1分間隔に丸め込み、位置情報を線形補間しました。なぜこのようなことをしたかと言いますと、Mobmap Webを使ったときと比べてバスの動きに滑らかさがなかったためです。

bus_folium.py
import pandas as pd
import folium
from folium.plugins import TimestampedGeoJson

# CSV読み込み
df = pd.read_csv('0321_echigo_tsubame_sanjo_gtfs_realtime_data.csv', parse_dates=['timestamp'])

# タイムゾーンを削除し、60秒間隔で時刻を丸める
df['timestamp'] = df['timestamp'].dt.tz_localize(None)
df['rounded_timestamp'] = df['timestamp'].dt.round('60s')

# 補間処理(丸め後)
interpolated_rows = []

for bus_id, group in df.groupby('id'):
    group = group.drop_duplicates('rounded_timestamp').set_index('rounded_timestamp').sort_index()
    group_resampled = group.reindex(pd.date_range(group.index.min(), group.index.max(), freq='60s'))

    group_resampled['id'] = bus_id
    group_resampled['route_id'] = group['route_id'].ffill()
    group_resampled[['latitude', 'longitude']] = group_resampled[['latitude', 'longitude']].interpolate()
    group_resampled = group_resampled.dropna(subset=['latitude', 'longitude', 'route_id'])

    # interpolated_rows.append(group_resampled.reset_index().rename(columns={'index': 'timestamp'}))
    interpolated_rows.append(group_resampled.reset_index().rename(columns={'index': 'rounded_timestamp'}))

df_interpolated = pd.concat(interpolated_rows).reset_index(drop=True)

# route_idごとの色設定
color_dict = {0.0: 'red', 1.0: 'blue', 2.0: 'yellow'}

# GeoJSON用データ展開
expanded_rows = []
for bus_id, group in df_interpolated.groupby('id'):
    group = group.sort_values('timestamp').reset_index(drop=True)
    for i in range(len(group)-1):
        current_row = group.iloc[i].copy()
        current_row['next_timestamp'] = group.iloc[i+1]['timestamp']
        expanded_rows.append(current_row)

df_expanded = pd.DataFrame(expanded_rows)

# GeoJSON生成
def df_to_geojson(df):
    features = []
    for _, row in df.iterrows():
        color = color_dict[row['route_id']]
        feature = {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': [row['longitude'], row['latitude']],
            },
            'properties': {
                'times': [
                    row['timestamp'].strftime('%Y-%m-%dT%H:%M:%S'),
                    row['next_timestamp'].strftime('%Y-%m-%dT%H:%M:%S')
                ],
                'icon': 'circle',
                'iconstyle': {
                    'fillColor': color,
                    'color': color,
                    'fillOpacity': 0.8,
                    'stroke': 'true',
                    'radius': 6
                },
                'popup': (
                    f"バスID: {row['id']}<br>"
                    f"路線ID: {int(row['route_id'])}<br>"
                    f"時刻: {row['timestamp'].strftime('%Y-%m-%d %H:%M:%S')}"
                )
            }
        }
        features.append(feature)
    return {'type': 'FeatureCollection', 'features': features}

geojson_data = df_to_geojson(df_expanded)

# 地図作成
m = folium.Map(location=[df['latitude'].mean(), df['longitude'].mean()], zoom_start=13)

# Foliumアニメーション設定
TimestampedGeoJson(
    geojson_data,
    period='PT1M',  # 60秒間隔
    add_last_point=False,
    auto_play=True,
    loop=False,
    max_speed=120,  # 2分を1秒に高速再生
    loop_button=True,
    date_options='YYYY/MM/DD HH:mm:ss',
    time_slider_drag_update=True,
    duration='PT1M'
).add_to(m)

# 凡例表示
legend_html = '''
<div style="position: fixed; bottom: 50px; left: 30px; width: 120px; z-index:9999; background-color:white; padding:10px; border:2px solid grey; border-radius:6px;">
<b>凡例</b><br>
<i style="background:red;width:10px;height:10px;border-radius:50%;display:inline-block;"></i> Route 0<br>
<i style="background:blue;width:10px;height:10px;border-radius:50%;display:inline-block;"></i> Route 1<br>
<i style="background:yellow;width:10px;height:10px;border-radius:50%;display:inline-block;"></i> Route 2
</div>
'''
m.get_root().html.add_child(folium.Element(legend_html))

# HTMLとして保存
m.save('bus_animation.html')

print("アニメーションHTML生成完了: bus_animation.html")

そして、新潟県県央地域のバスの1日の動きをパーティクルで表した動画を以下に示します。
燕市と三条市のコミュニティバスは時刻表通りに動いたらという前提の動きになっています。そして越後交通の路線バスは2025/3/21のGTFS-RTデータを処理した動きになっています。なお、可視化した動きは朝7時からの動きです。

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

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

色々と試してみたのですが、あまり滑らかな動きにはできませんでした。

5. まとめ

今回は、foliumを用いて、GTFSデータとGTFS-RT(リアルタイム)データを組み合わせ、越後交通および新潟県燕市・三条市のコミュニティバスの1日の運行状況を可視化しました。
ただし、以前使用したMobmap Webと比べると、動きの滑らかさにはやや劣る結果となってしまいました。

今後は、補間方法を工夫するなどしてこの課題に取り組みつつ、foliumを活用したオープンデータの可視化にも引き続き取り組んでいきたいと考えています。

注:

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

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

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?