はじめに
「地理情報をまとめて地図上にプロットしたやつを見たい」ときに、お手軽にやっつける方法です。
Embulk + Elasticsearch(以下ES)の備忘録を兼ねて。
事前準備
入力データ
プロット対象となるデータを用意します。
今回は「AEDオープンデータプラットフォーム」から、東京都におけるAEDについてのデータを使わせていただくことにしました。
WebAPIを毎回叩いてしまうのは気が引けるので、ローカルにデータを保存しておきます。
// "https://aed.azure-mobile.net/api/aedinfo/東京都/" を取得
$ curl https://aed.azure-mobile.net/api/aedinfo/%E6%9D%B1%E4%BA%AC%E9%83%BD/ -o aedinfo_tokyo.json
Elasticsearch + kibana インストール
2018/01時点では6.x系の情報が少なかったので、5.6系を使います。
今回はめんどくさかったのでbrewで入れていますが、Docker使ってもいいと思います。お好みで。
$ brew install elasticsearch@5.6 kibana@5.6
$ brew services start elasticsearch@5.6
$ brew services start kibana@5.6
http://localhost:5601/ にアクセスして起動を確認しておきます。
Embulk インストール
こちらもbrewで。
$ brew install embulk
$ embulk --version
embulk 0.8.30
今回利用するプラグインも合わせて入れておきます。
// in(JSON), out(ES)
$ embulk gem install embulk-parser-jsonpath embulk-output-elasticsearch
// filters
$ embulk gem install embulk-filter-insert embulk-filter-ruby_proc embulk-filter-column
これで準備完了です。
スキーマ定義、データ投入
早速Embulkのconfig.ymlを書いていきたいところですが、ESで地理情報を扱うgeo_point形式にマッピングするには事前のスキーマ定義(template定義)1が必要となるので、まずはこちらを作成します。
今回は以下のようなスキーマを定義しました。シンプルに名称、住所、地理情報の3フィールドです。
{
"template": "aedmap*",
"mappings": {
"aed_schema": {
"properties": {
"LocationName": {
"type": "string"
},
"address": {
"type": "string"
},
"location": {
"type": "geo_point"
}
}
}
}
}
ESに登録します。
$ curl -XPUT localhost:9200/_template/aedmap -d @schema.json
{"acknowledged":true}
これで、"aedmap*"にマッチするindex名に対し、"aed_schema"が有効になります。
config.yml作成
in:
でjsonから取得した項目をfilters:
で変換しつつ、out:
でESへと投入します。
filters:
の変換ルールは以下の通りです。
-
location
,address
フィールドを新規追加(embulk-filter-insert) - フォーマット変換(embulk-filter-ruby_proc)
- JSONの
Latitude
とLongitude
をgeo_point書式2に文字列フォーマット、ESのlocation
フィールドに変換 - JSONの
Prefecture
とCity
とAddressAres
を文字列連結、ESのaddress
フィールドに変換
- JSONの
- 出力対象フィールド指定(embulk-filter-column)
in:
type: file
path_prefix: ./aedinfo_tokyo.json
parser:
charset: UTF-8
newline: LF
type: jsonpath
columns:
- {name: Id, type: long}
- {name: LocationName, type: string}
- {name: Perfecture, type: string}
- {name: City, type: string}
- {name: AddressArea, type: string}
- {name: Latitude, type: double}
- {name: Longitude, type: double}
filters:
- type: insert
columns:
- location: ''
- address: ''
- type: ruby_proc
columns:
- name: location
proc: |
->(_,record) do
return record['Latitude'].to_s + "," + record['Longitude'].to_s
end
skip_nil: false
type: string
- name: address
proc: |
->(_,record) do
return record['Perfecture'].to_s + record['City'].to_s + record['AddressArea'].to_s
end
skip_nil: false
type: string
- type: column
columns:
- {name: LocationName, type: string}
- {name: address, type: string}
- {name: location, type: string}
#out: {type: stdout}
out:
type: elasticsearch
index: aedmap-tokyo-index
index_type: aed_schema
nodes:
- {host: 127.0.0.1}
実際のデータを利用したサンプルを以下に示します。
{
"Id": 68655,
"LocationName": "みずほ銀行 東青梅支店",
"Perfecture": "東京都",
"City": "青梅市",
"AddressArea": "東青梅2-13-1",
"Latitude": 35.7902422,
"Longitude": 139.2730937,
"FacilityId": null,
"FacilityName": null,
"FacilityPlace": null,
"ScheduleDayType": null,
"ScheduleDayStartTime": null,
"ScheduleDayEndTime": null,
"AccessAvailabilityOfPad": null,
"FacilityUser": null,
"PhotoOfAedUrl": null,
"Url": null,
"FacilityOwner": null,
"FacilityOperater": null,
"ContactPoint": null,
"ContactTelephone": null,
"ContactExtension": null,
"FacilityNote": null,
"TypeOfPad": null,
"ExpiryDate": null,
"ExpiryDateOfPads": null,
"ExpiryDateOfBatteries": null,
"TypeOfDefibrillator": null,
"ModelNumber": null,
"SerialNumber": null,
"Source": "http://shop.www.mizuhobank.co.jp/b/mizuho/",
"VenueId": null,
"DateOfUpdatingInformation": "2014-09-29T21:44:14.840Z"
}
{
"LocationName": "みずほ銀行 東青梅支店",
"address": "東京都青梅市東青梅2-13-1",
"location": "35.7902422,139.2730937"
}
out: {type: stdout}
を使って動作確認後、問題なさそうならESに投入します。
$ embulk run config.yml
2018-01-10 14:13:03.181 +0900: Embulk v0.8.30
2018-01-10 14:13:09.413 +0900 [INFO] (0001:transaction): Loaded plugin embulk-output-elasticsearch (0.4.5)
2018-01-10 14:13:09.462 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-insert (1.1.1)
2018-01-10 14:13:09.572 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-ruby_proc (0.7.0)
2018-01-10 14:13:09.620 +0900 [INFO] (0001:transaction): Loaded plugin embulk-filter-column (0.7.1)
2018-01-10 14:13:09.752 +0900 [INFO] (0001:transaction): Loaded plugin embulk-parser-jsonpath (0.2.0)
2018-01-10 14:13:09.783 +0900 [INFO] (0001:transaction): Listing local files at directory '.' filtering filename by prefix 'aedinfo_tokyo.json'
2018-01-10 14:13:09.785 +0900 [INFO] (0001:transaction): "follow_symlinks" is set false. Note that symbolic links to directories are skipped.
2018-01-10 14:13:09.790 +0900 [INFO] (0001:transaction): Loading files [aedinfo_tokyo.json]
2018-01-10 14:13:09.949 +0900 [INFO] (0001:transaction): Using local thread executor with max_threads=8 / output tasks 4 = input tasks 1 * 4
2018-01-10 14:13:10.001 +0900 [INFO] (0001:transaction): Logging initialized @7060ms
2018-01-10 14:13:10.447 +0900 [INFO] (0001:transaction): Connecting to Elasticsearch version:5.6.5
2018-01-10 14:13:10.448 +0900 [INFO] (0001:transaction): Executing plugin with 'insert' mode.
2018-01-10 14:13:10.448 +0900 [INFO] (0001:transaction): Inserting data into index[aedmap-tokyo-index]
2018-01-10 14:13:10.539 +0900 [INFO] (0001:transaction): {done: 0 / 1, running: 0}
2018-01-10 14:13:10.697 +0900 [INFO] (0023:task-0000): JSONPath = $
2018-01-10 14:13:12.642 +0900 [INFO] (0023:task-0000): Inserted 954 records
2018-01-10 14:13:13.021 +0900 [INFO] (0023:task-0000): Inserted 851 records
2018-01-10 14:13:13.319 +0900 [INFO] (0023:task-0000): Inserted 852 records
2018-01-10 14:13:13.597 +0900 [INFO] (0023:task-0000): Inserted 860 records
2018-01-10 14:13:13.598 +0900 [INFO] (0001:transaction): {done: 1 / 1, running: 0}
2018-01-10 14:13:13.599 +0900 [INFO] (0001:transaction): Insert completed. 3517 records
2018-01-10 14:13:13.606 +0900 [INFO] (main): Committed.
2018-01-10 14:13:13.607 +0900 [INFO] (main): Next config diff: {"in":{"last_path":"aedinfo_tokyo.json"},"out":{}}
kibana経由できちんと地理情報が確認できればOK。記事作成時点では3517件のデータが投入されました。
地図表示(kibana + OpenStreetMap)
Coodinate Map 設定
Visualize -> Create a Visualization -> Coodinate Map を選択。
Filedに地理情報を保持するフィールド(今回はlocation
)を選びます。▶ボタンを押して、データが地図上にプロットされていればOKです。
OpenStreetMap TileServer設定
これでもそれなりにいい感じですが、もう少しズームしたい場合や、地図表記は日本語がいい場合もあるでしょう。kibanaのconfigファイルを編集して対応します。
(追記)
tilemap.url: "http://tile.openstreetmap.jp/{z}/{x}/{y}.png"
tilemap.options.maxZoom: 18
今回はとりあえず動作確認ということで、openstreetmap.jpのTilemapServerをお借りしています。ヘビーに使う場合はTileMapServerを自分で立てるなどしてください。
参考:
kibanaを再起動します。
$ brew services restart kibana@5.6
ページを再読込して、さらに細かくズームできるようになっていればOKです。
あとは「足立区にやたら多いのはなんでだろう…?」「都心部に少ないのは単にデータが登録されてないのか、それともAEDがよく配置されている病院や学校などの公共施設が少ないから…?」という感じで色々推測しつつお楽しみください。
■おまけ
embulkの汎用性が高いので一度やり方を覚えてしまえばかなり応用が効きます。参考までに。
都内コンビニ数(ヒートマップ)
Sinfonica - 2011年6月号「フリーソフトによるデータ実践GIS」東京都コンビニエンスストアの緯度経度データのダウンロード ページで公開されていた tokyo3.csv をお借りしたものです。Optionからヒートマップを選ぶとこんな感じのプロットもできます。(少々パラメータの追い込みがめんどうですが...)
やっぱ都心はコンビニ多いよね(小並感)
都内IT系(Web系)求人情報数
仕事探しがめんどくさくなって自作した求人情報クローラーがMySQLに放り込んでた案件情報を、逆ジオコーディングしてマッピングしたものです。10サイトくらいから取得したデータを、合計20,000件くらい。
求人の性質上同じ会社が複数回求人を出していたり、最寄り駅や区名レベルでの地理情報しかないものも多々あったりして、データとしての正確性はお察しレベルですが、その辺差し置いてもやっぱり渋谷、新宿あたりは強いってことなんでしょうか。次に住むなら東京の南寄りかなあ。。。
とまあこんな感じで、色々遊んでみてください!
お役に立てば幸いです。
参考:
- elasticsearch - elastic search converting and inserting lat lon as geo_point in bulk - Stack Overflow
- KibanaのTileMapにOpenStreetMapを利用する | Developers.IO