cucumber flesh

Rを中心としたデータ分析・統計解析らへんの話題をしていくだけ

標準的な公共交通機関の情報形式 GTFS をRで処理する: gtfsrouter編

f:id:u_ribo:20191117103306j:plain

この記事はRアドベントカレンダー2019の5日目の投稿です。昨日は id:ando_Roid さんの投稿でした。

はじめに

今回で4回目となる「東京公共交通オープンデータチャレンジ」の応募が始まりました。これは公共交通オープンデータやその他のオープンデータを活用したアプリケーションおよびサービスの提案に関するコンテストです。ここでは、鉄道、バス、航空などの交通機関のデータが、GTFS (General Transit Feed Specification) 形式で公開されています。

GTFSは公共交通機関の時刻表と地理的情報に関するオープンフォーマットとして定義されます。リアルタイムの運行情報を反映した動的データ (GTFS Realtime)と、ダイヤ改正等の事情がない限り安定した静的データがあり、GTFSといえば厳密には静的データの方を指します。複数の事業者・交通機関が共通の形式に従ってデータを整備することで、経路探索や時刻表のアプリケーション開発が容易になるのが利点です。そのため、GTFSを提案したGoogle以外でも広く使われています。日本の交通機関に関しても、GTFSをベースに日本独自の交通事情を加味しながら拡張されたフォーマット (GTFS-JP) が整備されています。

さて、今回はそんなGTFSをRで扱ってみようという記事になります。日本語の内容として id:nonki1974 さんがgtfsrパッケージの利用方法を スライドにまとめています。なのでここでは、もう一つの例として gtfsrouterパッケージを紹介します。

目次です。

gtfsrouterパッケージ

github.com

gtfsrouterパッケージの特徴として次の点が挙げられます。

  • 2019年12月5日現在、CRANに登録されている。gtfsrは未登録
    • ただし、現在はGitHub上の開発版を利用するのが良さそう (issue #14 参照)
  • GTFSデータをdata.tableオブジェクトとして処理するため、処理速度の高速化が期待できる
  • 指定時間内に移動可能な領域 (Isochrone map) の描画ができる

まずはパッケージを利用可能な状態にしておきます。CRANに登録されているバージョンではなくGitHub上の開発版をインストールします。

install.packages("remotes")
remotes::install_github("ATFutures/gtfs-router")
# gtfs_isochrone()を実行する際に必要です
# install.packages(c("alphahull", "geodist"))
library(magrittr)
library(gtfsrouter)
library(data.table)

ここではデモデータとして用意されているベルリン、ブランデンブルクを運行する交通機関と、日本の岡山電気軌道株式会社が運営する岡電バス http://www.okayama-kido.co.jp/bus/ のGTFSデータを処理する例を見ていきます。なお岡電バスのデータは冒頭の「東京公共交通オープンデータチャレンジ」で提供されているものではなく、元々Creative Commonsライセンス4.0のもとで公開されているデータとなります。

データ公開元: https://loc.bus-vision.jp/ryobi/view/opendata.html

デモデータの処理

デモデータと岡電バスのデータを分けて解説するのは、現行のCRANバージョンでは、transfers.txtが存在しないGTFSデータを読み込むことができないためです。またtransfers.txtがない場合に実行できないいくつかの関数がありますので留意してください (issue #14 参照)。

berlin_gtfs_to_zip()
tempfiles <- 
  list.files(tempdir (), full.names = TRUE)
filename <- 
  tempfiles [grep ("vbb.zip", tempfiles)]

ここまでがデータを用意するところです。次のextract_gtfs()でGTFSの圧縮ファイル(運行情報に関する各種のcsvファイルが含まれる)を読み込みます。

gtfs <- 
  extract_gtfs(filename)

データはリストに格納されています。どのような情報が格納されているかは、names()を使って調べます。また、各データごとの件数が知りたい時はsummary()を使うと良いでしょう。

names(gtfs)
## [1] "calendar"   "routes"     "trips"      "stop_times" "stops"     
## [6] "transfers"
summary(gtfs)
## A gtfs object with the following tables and respective numbers of entries in each:

##   calendar     routes      trips stop_times      stops  transfers 
##       2395         52      28590      22666        957       9833

読み込んだデータの一例として便情報 trips を表示します。この出力で、各情報はdata.tableオブジェクトであることが確認できます。

head(gtfs$trips)
## route_id service_id   trip_id trip_headsign trip_short_name direction_id block_id shape_id wheelchair_accessible bikes_allowed
## 1: 10141_109        139 107928371 S Blankenburg              NA            0       NA     1100                     1             1
## 2: 10141_109        139 107928370 S Blankenburg              NA            0       NA     1100                     1             1
## 3: 10141_109        139 107928369 S Blankenburg              NA            0       NA     1100                     1             1
## 4: 10141_109        139 107928368 S Blankenburg              NA            0       NA     1100                     1             1
## 5: 10141_109        139 107928367 S Blankenburg              NA            0       NA     1100                     1             1
## 6: 10141_109        139 107928366 S Blankenburg              NA            0       NA     1100                     1             1
class(gtfs$trips)
## [1] "data.table" "data.frame"
# 起点と終点、時刻を選んでデータを抽出
gtfs_route(gtfs, 
           from = "Schonlein",
           to = "Berlin Hauptbahnhof",
          # 12:02 in seconds
          start_time = 12 * 3600 + 120,
          day = "Sunday")
## route_name     trip_name                       stop_name arrival_time departure_time
## 1          U8  S+U Wittenau        U Schonleinstr. (Berlin)     12:09:00       12:09:00
## 2          U8  S+U Wittenau       U Kottbusser Tor (Berlin)     12:11:00       12:11:00
## 3          U8  S+U Wittenau          U Moritzplatz (Berlin)     12:13:00       12:13:00
## 4          U8  S+U Wittenau  U Heinrich-Heine-Str. (Berlin)     12:14:30       12:14:30
## 5          U8  S+U Wittenau    S+U Jannowitzbrucke (Berlin)     12:15:30       12:15:30
## 6          S3 S Spandau Bhf    S+U Jannowitzbrucke (Berlin)     12:20:54       12:21:24
## 7          S3 S Spandau Bhf S+U Alexanderplatz Bhf (Berlin)     12:22:54       12:23:42
## 8          S3 S Spandau Bhf     S Hackescher Markt (Berlin)     12:24:54       12:25:24
## 9          S3 S Spandau Bhf  S+U Friedrichstr. Bhf (Berlin)     12:26:54       12:27:42
## 10         S3 S Spandau Bhf         S+U Berlin Hauptbahnhof     12:29:36       12:30:12

あらかじめ特定の日付や曜日、ルートのパターンを限定しておくことでgtfs_route()の処理を高速化が期待できます。 gtfs_timetable()でデータの抽出を行います。

# 出力結果は省略
gtfs_sunday <- 
  gtfs_timetable(gtfs, 
                  day = "Sunday") 

gtfs_sunday %>% 
  gtfs_route(from = "Schonlein",
                    to = "Berlin Hauptbahnhof",
                    start_time = 12 * 3600 + 120)

data.tableによるGTFSデータの操作

次に岡電バスのGTFSデータを見てみます。ここでは先ほどのgtfs_route()gtfs_timetable()を使わずに任意のGTFSデータを紐付ける処理を解説します。先述の通り、extract_gtfs()の読み込み結果はdata.tableオブジェクトを格納したリストですので、このデータを処理するためにはdata.tableパッケージの関数についての理解が求められます(dplyrパッケージに慣れている場合はas.data.frame()としても良いです)。

zipファイルをダウンロードすることになりますが、このファイルは展開せずにそのまま利用します。

download.file(url = "http://loc.bus-vision.jp/gtfs/okaden/gtfsFeed",
              destfile = "data/okaden_GTFS-JP.zip")
# ここで警告が出ますが問題はありません
gtfs <- 
  extract_gtfs("data/okaden_GTFS-JP.zip")
Warning message:
This feed contains no transfers.txt

まずは便情報 trips から対象のデータを絞っていきます。tripsには目的地の停留所名があるので、これを元にします。またroute_id、trip_idの情報が他のデータと紐付けるために必要となります。

# 目的地が「岡山駅」の便を検索
# route_id ... ルートを識別するためのID
# trip_id... 旅程を識別するID
# trip_headsign... 目的地
dt_trips <- 
  gtfs$trips[trip_headsign == "岡山駅"][, list(route_id, trip_id, trip_headsign, shape_id)]

岡山駅」が目的地となるルートは複数あるので、1つのルートのデータを使うことにします。

dt_trips <- 
  dt_trips[route_id == "10001_1008_1"] %>% 
  unique(by = "route_id")
dt_trips
##        route_id              trip_id trip_headsign shape_id
## 1: 10001_1008_1 347_1114411_20191201        岡山駅 2_1008_1

次にルート routes から路線名を調べます。

gtfs$routes[route_id == dt_trips$route_id][, list(route_id, route_long_name)]
##        route_id route_long_name
## 1: 10001_1008_1      大学病院線

続いて、停車地の情報を得れるようにします。stop_times が trip_idの情報と紐づきます。

dt_stopstimes <- 
  gtfs$stop_times[trip_id == dt_trips$trip_id][, list(trip_id, stop_id)]
dt_stopstimes
##                 trip_id stop_id
## 1: 347_1114411_20191201    18_5
## 2: 347_1114411_20191201    37_1
## 3: 347_1114411_20191201    38_1
## 4: 347_1114411_20191201    39_3
## 5: 347_1114411_20191201    40_1
## 6: 347_1114411_20191201   138_1
## 7: 347_1114411_20191201    63_4
## 8: 347_1114411_20191201    64_1

しかしこれだけではバス停の位置や名前の情報がありません。さらに 停車地 stops のデータを参照する必要が生じます。

dt_stops <-
  gtfs$stops[stop_id %in% dt_stopstimes$stop_id][, list(stop_id, stop_name, stop_lat, stop_lon)]
dt_stops
##    stop_id                          stop_name  stop_lat  stop_lon
## 1:    18_5                             岡山駅 34.665280 133.91846
## 2:    37_1 イオンモール岡山前・源吉兆庵本社前 34.661926 133.91895
## 3:    38_1                       山陽新聞社前 34.657890 133.91911
## 4:    39_3                       岡山市役所前 34.655470 133.91866
## 5:    40_1                           水道局前 34.653824 133.91756
## 6:    63_4                             清輝橋 34.651443 133.92488
## 7:    64_1                         岡南営業所 34.644432 133.92485
## 8:   138_1                       大学病院入口 34.652370 133.92053

これで「大学病院線」の停車地の情報が用意できました。

今度は車両が通過する経路を作りましょう。経路の情報は shapes にあり、便情報とはshape_idで繋がります。

dt_shapes <- 
  gtfs$shapes[shape_id == dt_trips$shape_id][, list(shape_pt_lon, shape_pt_lat)]
dt_shapes
##      shape_pt_lon  shape_pt_lat
##  1: 133.924891667 34.6442527778
##  2: 133.925005556 34.6464027778
##  3: 133.925002778 34.6468666667
##  4: 133.925036111 34.6476444444
##  5: 133.925097222 34.6482138889
##  6: 133.925211111 34.6504444444
##  7: 133.925475000 34.6512555556
##  8: 133.925400000 34.6513361111
##  9: 133.924158333 34.6516277778
## 10: 133.922086111 34.6520777778
## 11: 133.917530556 34.6531000000
## 12: 133.917425000 34.6532277778
## 13: 133.917550000 34.6536944444
## 14: 133.917827778 34.6540611111
## 15: 133.919227778 34.6562055556
## 16: 133.919277778 34.6564500000
## 17: 133.918972222 34.6630277778
## 18: 133.918952778 34.6640222222
## 19: 133.918997222 34.6646805556
## 20: 133.919166667 34.6654277778
## 21: 133.919100000 34.6655166667
## 22: 133.918891667 34.6654416667
## 23: 133.918411111 34.6647361111
## 24: 133.918325000 34.6647111111
## 25: 133.918236111 34.6647638889
## 26: 133.918277778 34.6648972222
## 27: 133.918613889 34.6653805556
##      shape_pt_lon  shape_pt_lat

もう一つ、曜日をベースとして停留地を探す処理の例を示します。

trip_ids <-
  gtfs$trips[.(gtfs$calendar[.(1),
                             on = c("monday")]),
             on = "service_id"][, trip_id] %>%
  unique()

dt_okaden_monday <-
  merge(gtfs$stop_times[.(trip_ids),
                        on = .(trip_id)] %>%
          .[, -c("stop_headsign", "pickup_type", "drop_off_type")],
        gtfs$stops[, c("stop_id", "stop_name", "stop_lat", "stop_lon")],
        by = "stop_id")
# 並び替え
setorder(dt_okaden_monday, trip_id, stop_sequence)
dt_okaden_monday
## stop_id              trip_id arrival_time departure_time stop_sequence shape_dist_traveled                          stop_name  stop_lat  stop_lon
## 1:    64_1 347_1100111_20191201        54540          54540             1                  NA                         岡南営業所 34.644432 133.92485
## 2:    63_4 347_1100111_20191201        54660          54660             2                  NA                             清輝橋 34.651443 133.92488
## 3:   138_1 347_1100111_20191201        54720          54720             3                  NA                       大学病院入口 34.652370 133.92053
## 4:    40_1 347_1100111_20191201        54840          54840             4                  NA                           水道局前 34.653824 133.91756
## 5:    39_3 347_1100111_20191201        54900          54900             5                  NA                       岡山市役所前 34.655470 133.91866
## ---                                                                                                                                                  
##   32714:    37_1 355_1403536_20191201        67980          67980            29                  NA イオンモール岡山前・源吉兆庵本社前 34.661926 133.91895
## 32715:    18_6 355_1403536_20191201        68040          68040            30                  NA                             岡山駅 34.665115 133.91835
## 32716:    19_5 355_1403536_20191201        68100          68100            31                  NA                           岡山駅前 34.665688 133.92114
## 32717:    22_1 355_1403536_20191201        68280          68280            32                  NA                       NTT岡山前 34.663220 133.92645
## 32718:    27_8 355_1403536_20191201        68640          68640            33                  NA                             天満屋 34.661713 133.92828

ややこしさもありますが、理解すれば使いやすいデータだと思います。

RでGTFSデータを扱う際のTips

ちょっとしたtipsです。

連続値になっている時刻の変換

通過時刻 stop_times などに記録される arrival_time は実数になっています。これだと時刻がわかりにくいので、HH:MM:SS形式に変換しておくと確認が楽になります。これにはhms::as_hms()が利用できます。

min(dt_okaden_monday$arrival_time)
## [1] 20640
# 時刻表データに記録される最初と最後の時刻を表示
hms::as_hms(range(dt_okaden_monday$arrival_time))
## 05:44:00
## 22:58:00

sfオブジェクトへの変換

GTFSには、バスの停留所やルートを構成するポイントの情報が記録されています。gtfsrouterやgtfsrパッケージで読み込んだデータも、こうした緯度経度の情報はデータフレームの列に格納されています。そのため、地図上にマッピングしたり地理的な解析を行う際は地理空間オブジェクトへの変換が必要となります。ここでは、先ほどdata.tableを操作して得たデータを元にRの地理空間オブジェクトとして普及しているsfへの変換を試みます。POINTデータとして停留所、LINESTRINGの例として路線をそれぞれ扱います。

library(sf)
sf_okaden_stops <-
  dt_stops %>%
  st_as_sf(coords = c("stop_lon", "stop_lat"), crs = 4326)
sf_okaden_stops
## Simple feature collection with 8 features and 2 fields
## geometry type:  POINT
## dimension:      XY
## bbox:           xmin: 133.91756 ymin: 34.644432 xmax: 133.92488 ymax: 34.66528
## epsg (SRID):    4326
## proj4string:    +proj=longlat +datum=WGS84 +no_defs
##   stop_id                          stop_name                    geometry
## 1    18_5                             岡山駅  POINT (133.91846 34.66528)
## 2    37_1 イオンモール岡山前・源吉兆庵本社前 POINT (133.91895 34.661926)
## 3    38_1                       山陽新聞社前  POINT (133.91911 34.65789)
## 4    39_3                       岡山市役所前  POINT (133.91866 34.65547)
## 5    40_1                           水道局前 POINT (133.91756 34.653824)
## 6    63_4                             清輝橋 POINT (133.92488 34.651443)
## 7    64_1                         岡南営業所 POINT (133.92485 34.644432)
## 8   138_1                       大学病院入口  POINT (133.92053 34.65237)
sf_okaden_routes <- 
  dt_shapes %>% 
  as.matrix() %>% 
  st_linestring() %>% 
  st_sfc(crs = 4326)

geometryだけの情報になりますが、sfcオブジェクトとして扱いたい場合があれば sfheadersパッケージの関数を使うのも一つの手段となります。

dt_stops %>% 
  sfheaders::sfc_point(x = "stop_lon", 
                       y = "stop_lat")
## Geometry set for 8 features 
## geometry type:  POINT
## dimension:      XY
## bbox:           xmin: 133.91756 ymin: 34.644432 xmax: 133.92488 ymax: 34.66528
## epsg (SRID):    NA
## proj4string:    NA
## First 5 geometries:

## POINT (133.91846 34.66528)

## POINT (133.91895 34.661926)

## POINT (133.91911 34.65789)

## POINT (133.91866 34.65547)

## POINT (133.91756 34.653824)

最後にleaflet上でマッピングしてみましょう。

mapview::mapview(sf_okaden_stops) +
  mapview::mapview(sf_okaden_routes)

f:id:u_ribo:20191205064112p:plain

明日は id:upura さんの予定です。そちらも楽しみですね。 Enjoy!