公共交通データとの
向き合い方 Ver.3.1

株式会社team-7 渡邊 徹志
[email protected]

本日のアジェンダ・資料

About tesshy

渡邊徹志(1984年生まれ・TRONと同い年、栃木県出身) @tesshy

2009-04 - 2015-03: 東京大学大学院学際情報学府学際情報学専攻総合分析情報学コース博士後期課程坂村・越塚研究室(単位取得満期退学)
2012-04 - 2018-07: YRPユビキタス・ネットワーキング研究所研究員
2018-07 - 2019-02: 無職
2019-02 - 2024-01: 株式会社MaaS Tech Japan CTO
2024-02 - 2024-03: 無職
2024-03 - 2025-02: 株式会社Vook 執行役員(技術担当)
2024-03 - 2024-04: 無職
2025-05 -        : 株式会社team-7、複数社お手伝い、農業(主にデジタル化、ドライバーとして)
  • 学士、修士は共に無線通信による位置測位の研究、以降は画像認識、AR、救急医療システム、オープンデータ、公共交通データ、etc
    • odptの最初期の仕様策定と実装は自分が担当してました、もう10年前・・・
  • 最近はデータ基盤設計、データ分析、プロマネ、農作業を手伝い、バスの中で仕事してる
    • 大型・けん引免許・クボタロボット農機使用者訓練修了証
  • 基本的に技術・ガジェオタ: ここ8年ぐらい年始にラスベガスにカジノCESに行ってます
    • 来年も行きます!何かありましたらお声がけください!!

愛車と共に

田植え中

稲刈り中

柏市公共交通情報連携アプリ(2013年)

東京メトロオープンデータコンテスト(2014年)

TraISARE(2022年)

背景・目的

公共交通オープンデータチャレンジ2025にて 新たにGTFS-Flex準拠のデータとデマンドODのデータが公開された。 本セミナーでは GTFS-FlexとデマンドODデータを使用する方法を紹介し、その応用例を示す。

  • GTFS-Flex及びデマンドODデータをどうやって扱うか
    • 可視化を例に
    • 色々組み合わせる

ヒント 1: よくある話

「何を実現したい」から「何が必要か」を考えましょう。手段が目的とならないように・・・。

可視化してみる

  • データを扱う上で必要なことは、まず可視化
    • どんなデータなのか
    • 何に使えそうか
    • どうやって処理(加工)するか
    • データが間違ってないか
  • データの民主化(Data Democratization)への第一歩
    • 従来一部の専門家だけが扱っていたものを、広く使えるように
    • 公開されるだけではなく、如何に使われるか
  • 使うツールはDuckDB(SQL)とKepler.glだけ
    • データ処理だけに全集中
  • GTFS-Flex: https://gtfs.org/ja/community/extensions/flex/
    • デマンド型モビリティの情報記述に対応したGTFS
    • GTFSの拡張と捉えてもらえればスムーズかと

得られるもの

DuckDBについて

DuckDB as the hub of data ingestions and transformation

Needham, M., Hunger, M., Simons, M. (2024). DuckDB in Action. Manning Publications.

  • 構造データをサクッといい感じに扱えるツール
  • 組み込み型DB、SQLiteのOLAP版と言われることもある
    • OLAP: Online Analytical Processing (大規模データ分析向け)
    • 列指向型DB(Columnar DB)であるため、データの圧縮効率が高い
      • 同じ列には同じ型で同じようなデータが入るため
  • GISデータ(GeoJSON、Shapefile、etc)も読み込めて簡易なGIS演算もできる
    • 公共交通データを扱う際に超重要
  • 読み込んだデータをSQLで処理(変換・整形)できる
    • 標準SQLはほぼサポートされている
  • dbtdltと組み合わせて使うとETL(Extract, Transform, Load)環境が作れる
    • DuckDB単体でもELT的なことはできるが、更新系の管理や変換ルールを管理するために使う

DuckDBのインストール

https://duckdb.org/docs/installation

  • Windows: winget install DuckDB.cli
    • WSLの場合はLinuxの手順を使う
  • macOS: curl https://install.duckdb.org | sh
    • Homebrew からインストールも可能
  • Linux: curl https://install.duckdb.org | sh
    • ワンライナーでインストールできるようになった!

Kepler.glについて

https://kepler.gl

  • Webブラウザ上でGISデータをいい感じに可視化できるツール
  • Uberが開発したツールがOSS化されたもの
    • deck.glをベースに構築されている
  • 地理情報を含んだCSVやGeoJSONを読み込んで可視化できる
    • サーバーに情報が送られるのではなく、ブラウザの中で処理されて表示される
  • レイヤーを重ねて可視化できる
    • e.g. バス停勢圏のレイヤーに、夜間人口のレイヤーを重ねる
  • Forsquare Studioという高機能なプロプライエタリ版がある
    • 元はUnfoldedというスタートアップが開発していたものをForsquareが買収
  • 2025/02頃からDuckDB-Wasmが組み込まれ、読み込んだデータに対してSQLで処理した結果をダイレクトに可視化することが可能に!
    • が、先日DuckDB-Wasmのマルウェア感染が公表されたのでちょっと注意(2025/09/09時点で公開されているのはv1.28.0を使用しているので問題ないはず)

DuckDBとKepler.glを使って簡易なバス停勢圏を出してみる

  • バス停勢圏: バス停が利用される範囲。バス停から徒歩で300-500mとして設定される場合が多い。最近は300mが主流・・・?
    • ここでは半径約300mの範囲を出してみる
    • 厳密には歩行空間ネットワーク及び勾配を加味するのが正しい、はず
  • DuckDBでstops.txtを直接読み込んでバス停勢圏のポリゴンをGeoJSONで出力する
    • ST_Bufferを使う、distanceにはメートルではなく度を指定する必要がある(ざくっとWGS84で東京における300mは約0.003度)
# ヒアドキュメントでクエリを流し込む、便利。
duckdb << EOS 
LOAD spatial;
COPY (
  SELECT ST_Union_Agg(geom) AS area FROM( -- 生成されたバス停毎の勢圏ポリゴンを重なっているものでマージする
    SELECT *, ST_Buffer(ST_Point(stop_lon, stop_lat), 0.003) AS geom, FROM read_csv('./stops.txt') -- ST_Bufferでバス停勢圏ポリゴンを生成する
  )
) TO './stops_area.geojson' WITH (FORMAT gdal, DRIVER 'GeoJSON', LAYER_CREATION_OPTIONS 'WRITE_BBOX=YES', SRS 'EPSG:4326'); -- stop_area.geojsonとしてポリゴンを保存する。SRSは元データに合わせるべき
EOS

バス停とバス停勢圏

GTFS・GTFS-Flexがあると何ができるか?

  • 乗車履歴を組み合わせることで、移動履歴を可視化
    • どの移動に需要があるか?
  • 交通空白地帯・移動可能範囲を可視化
    • どの程度の範囲をカバーしているのか?
    • どこが不便なのか?
  • (将来的に)経路探索サービスへの反映
    • 来訪者が使いやすいサービスに

需要情報(OD)を可視化してみる

  • 今回のコンテストでは、デマンド型モビリティの情報にはGTFS-Flexに加えて需要情報が提供されている
  • 乗車地・降車地及びタイムスタンプが記録されているので、これをOD(Origin-Distination)として可視化してみる
# contest2025.duckdbにtamamura_demand.txtをtamamura.demandとしてロードする
SCHEMA=tamamura && cat ${SCHEMA}_demand.txt | duckdb contest2025.duckdb -c "CREATE OR REPLACE TABLE ${SCHEMA}.demand AS SELECT * FROM read_csv('/dev/stdin')"
-- demandにstopsをJOINしてODを表現したSQL
SELECT current_schema() AS region, pickup_stops.stop_name AS pickup_stop_name,  pickup_stops.stop_lat AS pickup_stop_lat,  pickup_stops.stop_lon AS pickup_stop_lon, drop_off_stops.stop_name AS drop_off_stop_name,  drop_off_stops.stop_lat AS drop_off_stop_lat,  drop_off_stops.stop_lon AS drop_off_stop_lon, count FROM (SELECT pickup_stop_id, drop_off_stop_id, count(pickup_stop_id) AS count 
FROM demand GROUP BY pickup_stop_id, drop_off_stop_id) AS demand_count 
LEFT JOIN stops AS pickup_stops ON demand_count.pickup_stop_id = pickup_stops.stop_id 
LEFT JOIN stops AS drop_off_stops ON demand_count.drop_off_stop_id = drop_off_stops.stop_id;
# od.csv出力用シェルスクリプト(SCHEMAを書き換えれば他の地域でも生成可能)
SCHEMA=tamamura && duckdb contest2025.duckdb -csv -c "USE ${SCHEMA}; SELECT current_schema() AS region, pickup_stops.stop_name AS pickup_stop_name,  pickup_stops.stop_lat AS pickup_stop_lat,  pickup_stops.stop_lon AS pickup_stop_lon, drop_off_stops.stop_name AS drop_off_stop_name,  drop_off_stops.stop_lat AS drop_off_stop_lat,  drop_off_stops.stop_lon AS drop_off_stop_lon, count FROM (SELECT pickup_stop_id, drop_off_stop_id, count(pickup_stop_id) AS count FROM demand GROUP BY pickup_stop_id, drop_off_stop_id) AS demand_count LEFT JOIN stops AS pickup_stops ON demand_count.pickup_stop_id = pickup_stops.stop_id LEFT JOIN stops AS drop_off_stops ON demand_count.drop_off_stop_id = drop_off_stops.stop_id;" > od.csv

OD可視化(Arc)

需要情報(利用回数)を可視化してみる

  • 停留所毎に利用回数を集計してみる
SELECT * FROM stops 
LEFT JOIN ( SELECT pickup.pickup_stop_id AS stop_id, pickup.pickup_count, drop_off.drop_off_count FROM (SELECT pickup_stop_id, COUNT(*) AS pickup_count FROM demand GROUP BY pickup_stop_id) AS pickup 
LEFT JOIN (SELECT drop_off_stop_id, COUNT(*) AS drop_off_count FROM demand GROUP BY drop_off_stop_id) AS drop_off 
ON pickup.pickup_stop_id = drop_off.drop_off_stop_id) AS counts  
ON stops.stop_id = counts.stop_id;
# stops_count.csv出力用シェルスクリプト
SCHEMA=tamamura && duckdb contest2025.duckdb -csv -c "USE ${SCHEMA}; SELECT current_schema() AS region, * FROM stops LEFT JOIN ( SELECT pickup.pickup_stop_id AS stop_id, pickup.pickup_count, drop_off.drop_off_count FROM (SELECT pickup_stop_id, COUNT(*) AS pickup_count FROM demand GROUP BY pickup_stop_id) AS pickup LEFT JOIN (SELECT drop_off_stop_id, COUNT(*) AS drop_off_count FROM demand GROUP BY drop_off_stop_id) AS drop_off ON pickup.pickup_stop_id = drop_off.drop_off_stop_id) AS counts  ON stops.stop_id = counts.stop_id;" > stops_count.csv

乗車回数分布

乗車履歴の取得方法についての考察

誰が、いつ、何処から何処に(OD)、何で、(いくらで)移動したか

  • デマンド型の場合: 予約時に時間と発着地点が確定されるので取得が容易
  • 定時定路線(コミュバスなど)の場合
    • ICカード決済システムの導入
      • 乗車・降車両方で必ずタッチするシステムにする
    • マイナンバーカードによる乗降車システムの検討
    • 車載カメラによる乗降者判定
      • GPSと組み合わせれば最低限実現可能(なはず)
    • 紙で記録 -> デジタル化
      • 何処で乗降したかを記録(人数が少ない場合は不可能では無いが大変)
      • 乗った人数・降りた人数を停留所毎に記録してもいいが、ODが取れない為分析できる幅は狭まる(区間乗車数が分析できない)

定時定路線のGTFSとGTFS-Flexの勢圏を重ねてみる

  • 同じ地域内で運行されている定時定路線(永井バス)とデマンドタクシー(たまGO)の勢圏を可視化してみる
    • 補完関係が可視化される
  • GTFS・GTFS-Flexを整備すれば、現況把握に非常に有用
    • 予め整備すれば、交通空白地帯の解消に向けた検討資料としても使用できる

たまGOと永井バスのバス停勢圏

国勢調査と組み合わせてみる

  • e-Stat統計地理情報システムから国勢調査の結果を取得する
    • GIS処理しやすいように加工されたデータが提供されている
  • 玉村町が含まれるM5439の5次メッシュShapeデータをダウンロードする
    • 1次メッシュ枠情報: https://www.e-stat.go.jp/pdf/gis/primary_mesh_jouhou.pdf
    $ unzip QDDSWQ5439.zip
  • 玉村町が含まれるM5439の5次メッシュ国勢調査結果データをダウンロードする
  • ダウンロードしてきたファイルを解凍し、文字コードをCP932(ShiftJIS)からUTF-8に変換する(しなくてもなんとかなるが、なんとなく気持ち悪いので変換する)
$ unzip -p tblT001142Q5439.zip | iconv -c -f cp932 -t utf-8 > tblT001142Q5439_utf8.txt
  • メッシュコードをJOINしてGeoJSON::ポリゴンとして保存する
-- メッシュ内総人口はT001142001カラムに保存されているので、メッシュコードのポリゴンと人口をJOINしてGeoJSONとして保存する
duckdb << EOS 
load spatial;
COPY (
    SELECT mesh_5th.KEY_CODE, census.T001142001::INT AS population, geom 
    FROM (SELECT * FROM ST_Read('./QDDSWQ5439/MESH05439.shp')) AS mesh_5th 
    LEFT JOIN (SELECT * FROM read_csv('tblT001142Q5439_utf8.txt', header = true)) AS census 
    ON mesh_5th.KEY_CODE = census.KEY_CODE
) TO 'census.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON', LAYER_CREATION_OPTIONS 'WRITE_BBOX=YES');
EOS

たまGOと永井バスとセンサスと.png

おまけ: 背景地図を国土地理院ベクタータイルにする

  • Kepler.gl標準の地図でも見えなくは無いけれども・・・
  • 国土地理院が公開しているベクトルタイルを表示する用にStyle JSONを配布してくれている

国土地理院最適化ベクトルタイル(標準地図風スタイル)の適用

国土地理院最適化ベクトルタイル(標準地図風スタイル)の適用::結果

経路探索と組み合わせてみる

  • Valhalla
  • Motis
    • マルチモーダル輸送システムにおける効率的な計画とルーティングを容易にするために設計されたOSS
    • GTFS、RT、Flex、GBFSに対応
    • APIも備えておりかなりいい感じ(但しFlexを反映したIsochroneにはまだ対応してないっぽい)
  • OpenTripPlanner
    • Flexible transit routingという機能が備わっている(が、どうにも動かせなかった・・・)
    • Ver.2から分析用の機能が削られて経路案内用に特化しつつあるので今後の出番は恐らく減っていくものと思われる

経路探索エンジンを使うと何ができる?

  • 経路案内: 但し乗換所要時間などが精緻でないので、ナビゲーションとして使うには要注意
  • 経路推定: 乗車券が時刻t1にAバス停からBバス停で使われた場合に乗車したバス路線の推定
  • 等時線(Isochrone)分析: 特定の出発地点からある時間で到達可能な範囲を示したもの

ValhallaにOSMを食わせて歩行経路で300mなバス停勢圏を出してみる

  • 半径約300mなバス停勢圏は出してみたが、実際歩いて300mが望ましいのでOSM: Open Street Mapの道路ネットワークデータを使用してそれを出してみる
  • Valhalla
  • 方針
    • ValhallaをDockerで動かす
    • DuckDBからValhallaを叩いて、その結果を整形して出力する

ValhallaをDockerで動かす

docker run --rm -it -p 8002:8002 -v $PWD/custom_files:/custom_files ghcr.io/valhalla/valhalla-scripted:latest
  • 起動したら一旦停止し、 ./custom_files/valhalla.json が生成されているはずなので内容を確認する

DuckDBからValhallaを叩いて、その結果を整形して出力する

  • やりたいことはstopsに記載された緯度経度から徒歩300mのIsochrone出力
  • DuckDBのhttp_client Extentionを使って、DuckDBからValhallaを直接叩く
    • 検証しきれていないが、どうも1km以下の距離になると結果が不正確になるケースがある模様。時間をパラメーターで与える方がいいかもしれない
-- 中心座標と徒歩距離を与えたらIsochroneを示すPolygonを返すマクロ
CREATE OR REPLACE MACRO get_isochrone_geom(lng, lat, dist := 0.3) AS (
    SELECT ST_GeomFromGeoJSON((res->>'body')::JSON->'$.features[0].geometry')
    FROM (
        SELECT http_get(
            'http://localhost:8002/isochrone',
            headers => MAP { 'accept': 'application/json' },
            params => MAP { 'json': '{"polygons": true, "denoise": 0.0, "generalize": 1.0 , "costing":"pedestrian","contours":[{"distance":' || dist || '}], "locations":[{"lat":' || lat || ',"lon":' || lng || '}]}' }
        ) AS res
    )
);

-- Stopsに徒歩300mのPolygonをJOINして、GeoJSONとして出力
USE tamamura;
COPY (
    SELECT ST_Union_Agg(geom) AS area FROM (
        SELECT stop_lat AS lat, stop_lon AS lon, get_isochrone_geom(lon, lat) as geom FROM stops
    )
) TO 'stops_isochrone.geojson' WITH (FORMAT gdal, DRIVER 'GeoJSON', LAYER_CREATION_OPTIONS 'WRITE_BBOX=YES', SRS 'EPSG:4326');

徒歩半径比較

Motisを動かしてGTFS-Flexな経路を出してみる

  • セットアップ方法: https://github.com/motis-project/motis?tab=readme-ov-file#quick-start
    • GithubのREADMEしかドキュメントが無いっぽい漢気仕様
    • ただシングルバイナリー+アセットだけなのでだいぶ楽
    • 注意: どうも最新のVer.2.3.0でGTFS-Flexを読み込ませて探索させるとセグフォで落ちるので、Ver2.2.0を使用するのを推奨
  • 実行方法
# 関東のOSM、たまGOのGTFS-Flex、永井バスのGTFSを読み込むconfigを書き出す
./motis config ./kanto.osm.pbf ./tamamura_GTFS-flex.zip ./nagai.zip
# 設定ファイルを読み込んで、Motisが使用するデータを生成する
./motis import
# サーバーを実行する
./motis server

MotisでFlexな経路探索

参考::上記で生成されたconfig.yaml

osm: kanto.osm.pbf
tiles:
  profile: tiles-profiles/full.lua
  db_size: 274877906944
  flush_threshold: 100000
timetable:
  first_day: TODAY
  num_days: 365
  railviz: true
  with_shapes: true
  adjust_footpaths: true
  merge_dupes_intra_src: false
  merge_dupes_inter_src: false
  link_stop_distance: 100
  update_interval: 60
  http_timeout: 30
  incremental_rt_update: false
  use_osm_stop_coordinates: false
  extend_missing_footpaths: false
  max_footpath_length: 15
  max_matching_distance: 25.000000
  preprocess_max_matching_distance: 0.000000
  datasets:
    nagai:
      path: nagai.zip
      default_bikes_allowed: false
      default_cars_allowed: false
      extend_calendar: false
    tamamuraGTFS-flex:
      path: tamamura_GTFS-flex.zip
      default_bikes_allowed: false
      default_cars_allowed: false
      extend_calendar: false
elevators: false
street_routing: true
osr_footpath: false
geocoding: true
reverse_geocoding: true

まとめ

  • 可視化はデータ活用の第一歩
    • 可視化をすることで、データを直感的かつ俯瞰的に見ることができる
    • 可視化をするためにデータを処理することで、応用ができるようになる
  • 様々なデータを組み合わせることで、新たな価値が生まれる?!
    • 組み合わせる為には、対象データの「空間」を揃える必要がある
      • 今回は地理空間に揃えた、他に時間軸、属性軸なども考えられる
    • このような処理を行う際、DuckDBはお手軽で非常に強力なツール
  • GTFS、GTFS-Flexのデータを整備することは現在だけでなく将来への投資にも
    • 交通空白地帯の分析から、交通流(人流)の把握を見越して
    • プリミティブな方法から、今後新たに登場するツールによる利活用

Appendix: ツール・サービス紹介

  • DuckDB: ローカルOLAP環境、GISデータも扱える、今後のデータ処理におけるデファクトはこれだと思ってる

  • Kepler.gl: Uberが開発したWebGLを使った高速な地理情報可視化環境

  • OpenTripPlanner: オープンソースの経路探索エンジン、GTFSを読み込んで経路探索ができる

  • Valhalla: Mapzenが開発した経路探索エンジン、OSMのデータを使って経路探索できる

  • GraphHopper: 歴史ある経路探索エンジン、GTFSを読み込んで経路探索ができる

  • gtfs-realtime-bindings: GTFS-RTを扱う時に使うライブラリ、これがデファクトだと思う(方法はいっぱいあるけど)

  • Visual Studio Code: Microsoftが開発した軽量なIDE、PythonやSQLの開発にも使える、最早デファクトエディタ(エディタ戦争の終焉)

  • Jupyter Lab: Pythonのインタラクティブな開発環境、データ分析にはよく使われる

  • Quarto: Markdownで記述したドキュメントを、PDFやHTML、docxに出力できる。内部でPandocを使用。このプレゼンもQuarto製、TeXの時代は終わったのかもしれない・・・

  • uv: Rustで実装された、Pythonパッケージ管理システム。最近venv環境構築にも対応したのでpipenvから乗り換えた(脱Lock地獄)

  • Polars: Rustで実装された高速なデータフレームライブラリ、PythonのPandasと互換性がある

  • GeoPandas: Pandasの拡張で、GISデータを扱うためのライブラリ

  • deck.gl: Uberが開発したWebGLを使った高速な地理情報可視化ライブラリ、Python Bindingもある

  • Pandas: Pythonのデータフレームライブラリ、ど定番。

  • Dusk: Pythonの分散処理ライブラリ、Pandasで処理を書いてしまいデータが大きすぎて処理できない場合の救世主

  • dbt: ETLツール、SQLでデータ変換を記述できる

  • dlt: Data Load Tool、データロードを管理するためのツール

  • Apache Superset: Airbnbが開発したBIツール、OSSのBIツールの中ではGISデータの扱いが比較的得意

  • Forsquare Studio: Kepler.glの高機能版

  • Databricks: Apache Sparkをベースにしたクラウド型データ基盤、データ分析環境。DuckDBで管理しきれなくなったらこちらに移行検討を(最初から検討した方がいいケースもあり)