はじめに
「GTFSを使うことになったから調べてて。」とのこと。
何のことでしょう。GTFSって何? 路線バス情報? データですか?
ならば「@nako_sleep_9hさん主催のpiyopiyo.ex #14:Elixirでデータ分析をしてみよう!」でLivebook Explorerの使い方を勉強会したばかりのElixir初心者として、これを利用しない手はありません。
ここは業務の大義名分の下、Livebookを使ってElixirを勉強するシチュエーションに持ちこみましょう。
まあ何だかわからないけど手始めに、「Elixir Livebookで佐賀県のGTFSからバス停を地図に落とせたらいいなー」をやってみました。
実行環境
- Mac Ventura 13.1
- Livebook app
Livebookはデスクトップアプリを使ってます。
佐賀県のGTFSデータ
使うGTFSがまだ手に入らないとのことなので、佐賀県が公開してくださってるデータをありがたく使わせていただきます。
saga-current.zip をダウンロードして、@RyoWakabayashi さんの記事を参考にして読み込んでみます。
色々やってるうちにこんなになってしまったセッティング。
Mix.install(
[
{:explorer, "~> 0.5"},
{:csv, "~> 3.0"},
{:geo, "~> 3.4"},
{:kino, "~> 0.8"},
{:kino_maplibre, "~> 0.1.3"},
{:req, "~> 0.3.0"}
]
)
上記記事とか色々を参考にしたエリアスとかの設定
alias Explorer.DataFrame, as: Df
alias Explorer.Series, as: Sr
alias MapLibre, as: Ml
require Explorer.DataFrame
試しにダウンロードデータ解凍してフォルダごとノートファイルのとこに設置して読み込んでみます。
Path.absname('./gtfs-sample/saga-current/calendar_dates.txt')
|> Df.from_csv!()
|> Kino.DataTable.new()
こんな調子で本命の停留所データも読み込み....できない....
とあるカラムがintegerと解釈されてる中で空文字("")が混じっていて、そこではじかれてるらしい?
ここで、「データフレーム」すらわかってなかった自分に気が付く。
データ解析がしやすい形になっているらしい?
ではCSVをまんま読み込んで文字データでデータフレームの形を作ってみてはどうだろうか。
stops_data =
Path.absname("./gtfs-sample/saga-current/stops.txt")
|> File.stream!()
|> Enum.map(& &1 |> String.replace("\uFEFF", ""))
|> CSV.decode!()
|> Enum.to_list()
BOM入ってるしー。
これをヘッダとデータ切り離して、ぐに〜と交わらせたらデータフレーム作れるんじゃないだろうか。
csv_header =
stops_data
|> hd()
|> Enum.map(& &1 |> String.to_atom())
csv_maps =
stops_data
|> tl
|> Enum.map(& List.zip([csv_header, &1]) |> Enum.into(%{}))
stops_df =
csv_maps
|> Df.new()
stops_df |> Kino.DataTable.new()
それらしいのが出てきた。けど、このままだと全部文字列データなので、緯度経度だけでもなんとかします。
stop_lat = stops_df[:stop_lat] |> Sr.cast(:float)
stop_lon = stops_df[:stop_lon] |> Sr.cast(:float)
stops_df =
stops_df
|> Df.put(:stop_lat, stop_lat)
|> Df.put(:stop_lon, stop_lon)
stops_df |> Kino.DataTable.new
GeoJSON にして可視化してみる
どうしたら地図に落とせるのだろうか、ということでまた@RyoWakabayashiさんの記事のお世話になります。
GeoJSONにするために、さっきのデータフレームから緯度経度のタプル配列を作ってみます。
lon_list =
stops_df[:stop_lon]
|> Sr.to_enum()
|> Enum.to_list()
lat_list =
stops_df[:stop_lat]
|> Sr.to_enum()
|> Enum.to_list()
lon_lat_list =
lon_list
|> Enum.zip(lat_list)
ここで気が付く。自分が描きたいのはポイントで、記事にあるポリゴンデータではない。
ではGeoライブラリにポイントっぽいものを探してみます。
ここで@piacerexさんが hexdocs.pm のURLの後にライブラリ名入れると調べられると言っていたのを思い出したのでやってみます。
%Geo.Point がそれっぽい?
てゆーことは、もしかしてポリゴンデータの配列の代わりにポイントデータの配列を形作ってやればいいのだろうか。
stop_geojson =
%Geo.GeometryCollection{
geometries: lon_lat_list
|> Enum.map(fn p -> %Geo.Point{coordinates: p} end)
}
なんかそれっぽくなった気がします。
では、上の@RyoWakabayashiさんの記事を参考にしてセンター座標を作ってみます。
stop_coordinates =
stop_geojson.geometries
|> Enum.map(& &1.coordinates)
lon = Enum.map(stop_coordinates, & elem(&1, 0))
lat = Enum.map(stop_coordinates, & elem(&1, 1))
stop_center =
{
(Enum.min(lon) + Enum.max(lon)) / 2,
(Enum.min(lat) + Enum.max(lat)) / 2
}
これをMapLibreに入れたら地図に落としてくれるのだろうか。
スマートセルを使わないでコードで書いてみたい。
ところで落としたいのはポイントなのでそれっぽいのないだろうかと探す。
見てるとcircleというのがある。これを使えばいいのかなと思った。
と、ここでMapLibreの :style にいい感じな地図データのURL例を見つけました。
Ml.new(center: stop_center, zoom: 11, style: "https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL")
|> Ml.add_geo_source("data", stop_geojson)
|> Ml.add_layer(
id: "stop",
source: "data",
type: :circle,
paint: [circle_color: "#aa00ff", ]
)
びっくりです。とてもらしいんですけど、マジですか?!
生半可なelixir初心者でもこんなことが出来てしまうのですか?
しかも、知らなかったのですが、このMapLibreってクォータービューになる!!
凄すぎる!!
まとめ
GTFSとお友達になるためにElixir Livebookを使うのは楽しい。
(振り返ってみると無理やりデータフレームにする必要あったのだろうか?)