先日参加させていただいた Japan.R でこんな話を聞いた。
Python
でも folium
というパッケージを使うと JavaScript
を書かなくても Leaflet.js
の一部機能が使えるのだがあまり情報がない。上の資料に書いてあるようなことが folium
でもできるのか調べたい。
folium
については前にこんなエントリを書いた。
データの準備
import numpy as np np.__version__ # '1.10.2' import pandas as pd pd.__version__ # u'0.17.1'
サンプルデータとして Wikipedia にある アメリカの国立公園 のデータを使う。まずは pd.read_html
でデータを読みこむ。
url = "https://en.wikipedia.org/wiki/List_of_national_parks_of_the_United_States" df = pd.read_html(url, header=0)[0] df
位置情報は "Location" カラムに文字列として保存されている。これを文字列処理して緯度・経度 別々の数値列に変換する。
df['Location'] # 0 Maine 44°21′N 68°13′W<feff> / <feff>44.35°N 68.21°W<feff> / 4... # 1 American Samoa 14°15′S 170°41′W<feff> / <feff>14.25°S 17... # 2 Utah 38°41′N 109°34′W<feff> / <feff>38.68°N 109.57°W<feff> / ... # 3 South Dakota 43°45′N 102°30′W<feff> / <feff>43.75°N 102.... # ... # 55 Alaska 61°00′N 142°00′W<feff> / <feff>61.00°N 142.00°W<feff> ... # 56 Wyoming, Montana, Idaho 44°36′N 110°30′W<feff> / <feff>4... # 57 California 37°50′N 119°30′W<feff> / <feff>37.83°N 119.50... # 58 Utah 37°18′N 113°03′W<feff> / <feff>37.30°N 113.05°W<feff> / ... # Name: Location, dtype: object
まずは .str.extract
で Series
中の各要素から正規表現にマッチしたグループを取り出し、州名、緯度、経度 3 列の DataFrame
に展開する。
locations = df['Location'].str.extract(u'(\D+) (\d+°\d+′[NS]) (\d+°\d+′[WE]).*') locations.columns = ['State', 'lat', 'lon'] locations
数値としてパースできるよう、.str.replace
で記号を置換する。また、南半球 / 西半球の場合は緯度経度を負とするためマイナス符号をつけておく。
locations['lat'] = locations['lat'].str.replace(u'°', '.') locations['lon'] = locations['lon'].str.replace(u'°', '.') locations.loc[locations['lat'].str.endswith('S'), 'lat'] = '-' + locations['lat'] locations.loc[locations['lon'].str.endswith('W'), 'lon'] = '-' + locations['lon'] locations
最後の 2 文字は不要のため、.str.slice_replace
を使って削除する ( None
と置換する )。これで float64
型に変換できるようになる。
locations['lat'] = locations['lat'].str.slice_replace(start=-2) locations['lon'] = locations['lon'].str.slice_replace(start=-2) locations[['lat', 'lon']] = locations[['lat', 'lon']].astype(float) locations
処理した DataFrame
を pd.concat
で元の DataFrame
に追加する。
locations.dtypes # State object # lat float64 # lon float64 # dtype: object df = pd.concat([df, locations], axis=1)
地図の描画
folium
で描画した地図は Jupyter Notebook
に埋め込んで利用したい。Jupyter
上に描画するための関数を定義する。
import folium folium.__version__ # '0.1.6' from IPython.display import HTML def inline_map(m): m._build_map() iframe = '<iframe srcdoc=\"{srcdoc}\" style=\"width: 80%; height: 400px; border: none\"></iframe>' return HTML(iframe.format(srcdoc=m.HTML.replace('\"', '"')))
まずは シンプルなマーカー。これは前のエントリでやったことと同じ。
m = folium.Map(location=[55, -108], zoom_start=3.0) for i, row in df.iterrows(): m.simple_marker([row['lat'], row['lon']], popup=row['Name']) inline_map(m)
マーカーをクラスタとして表示するには、clustered_marker
キーワードに True
を渡すだけ。
m = folium.Map(location=[55, -108], zoom_start=3.0) for i, row in df.iterrows(): m.simple_marker([row['lat'], row['lon']], popup=row['Name'], clustered_marker=True) inline_map(m)
地図を拡大・縮小するとマーカーのクラスタ表示が適当に切り替わる。
サークルを表示するには circle_marker
を使う。各国立公園の 2014 年の入園者数をサークルの大きさとしてプロットする。
m = folium.Map(location=[40, -95], zoom_start=4.0) for i, row in df.iterrows(): m.circle_marker([row['lat'], row['lon']], radius=np.sqrt(row['Recreation Visitors (2014)[5]']) * 100, popup=row['Name'], line_color='#DF5464', fill_color='#EDA098', fill_opacity=0.5) inline_map(m)
ポリラインは line
で引ける。引数には、各点の緯度と経度からなるリストのリストを渡せばよい。グランドキャニオンからいくつかの国立公園を結ぶポリラインを描画してみる。
dests = ['Grand Canyon', 'Zion', 'Bryce Canyon', 'Capitol Reef', 'Arches'] loc_df = df.set_index('Name') locations = loc_df.loc[dests, ['lat', 'lon']].values.tolist() locations # [[36.04, -112.08], # [37.18, -113.03], # [37.34, -112.11], # [38.12, -111.1], # [38.41, -109.34]] m = folium.Map(location=[37.5, -111], zoom_start=7.0) m.line(locations=locations) for dest, loc in zip(dests, locations): m.simple_marker(loc, popup=dest) inline_map(m)
Google Maps Direction API の利用
2 点間の経路をポリラインで描画したい、といった場合は 上のやり方 / 現在のデータでは無理だ。Google Maps Direction API を使って取得した 2 点間の経路をポリラインとして描きたい。
Google Maps Direction API へのアクセスには googlemaps
パッケージを利用する。インストールは pip
でできる。
ドキュメントはなく、使い方はテストスクリプトを見ろ、とだいぶ硬派な感じだ。それでも自分で API 仕様を調べるよりは早いと思う。
import googlemaps googlemaps.__version__ # '2.4.2'
googlemaps
は Google Map に関連したいくつかの API をサポートしている。Directions API を使うには Client.directions
。 mode
として渡せるのは "driving", "walking", "bicycling", "transit" いずれか。
key = "Your application key" client = googlemaps.Client(key) result = client.directions('Grand Canyon, AZ, USA', 'Arches, UT, USA', mode="driving", departure_time=pd.Timestamp.now()) import pprint pprint.pprint(result, depth=5) # [{u'bounds': {u'northeast': {u'lat': 38.6164979, u'lng': -109.336915}, # u'southwest': {u'lat': 35.8549308, u'lng': -112.1400703}}, # u'copyrights': u'Map data \xa92015 Google', # u'legs': [{u'distance': {u'text': u'333 mi', u'value': 535676}, # u'duration': {u'text': u'5 hours 36 mins', u'value': 20134}, # u'duration_in_traffic': {u'text': u'5 hours 31 mins', # u'value': 19847}, # u'end_address': u'Arches National Park, Utah 84532, USA', # u'end_location': {u'lat': 38.6164979, u'lng': -109.6157153}, # u'start_address': u'Grand Canyon Village, AZ 86023, USA', # u'start_location': {u'lat': 36.0542422, u'lng': -112.1400703}, # u'steps': [{...}, # ...
結果は経路上のポイントごとに step
として含まれるようだ。各ステップ の end_location
を取得して地図上にプロットすると、ざっくりとした経路がわかる。
steps = result[0]['legs'][0]['steps'] steps[:2] # [{u'distance': {u'text': u'344 ft', u'value': 105}, # u'duration': {u'text': u'1 min', u'value': 10}, # u'end_location': {u'lat': 36.054091, u'lng': -112.1412252}, # u'html_instructions': u'Head <b>west</b>', # u'polyline': {u'points': u'_z`{EljmkT\\fF'}, # u'start_location': {u'lat': 36.0542422, u'lng': -112.1400703}, # u'travel_mode': u'DRIVING'}, # {u'distance': {u'text': u'141 ft', u'value': 43}, # u'duration': {u'text': u'1 min', u'value': 8}, # u'end_location': {u'lat': 36.0544236, u'lng': -112.1414507}, # u'html_instructions': u'Turn <b>right</b> toward <b>Village Loop Drive</b>', # u'maneuver': u'turn-right', # u'polyline': {u'points': u'ay`{EtqmkTI@GBGBIDGFIDKL'}, # u'start_location': {u'lat': 36.054091, u'lng': -112.1412252}, # u'travel_mode': u'DRIVING'}] locs = [step['end_location'] for step in steps] locs = [[loc['lat'], loc['lng']] for loc in locs] locs # [[36.054091, -112.1412252], # [36.0544236, -112.1414507], # [36.05547, -112.1384223], # [36.0395224, -112.1216684], # [36.0520635, -112.1055832], # [35.8550626, -111.4251481], # [36.0755773, -111.3918428], # [36.9304583, -109.5745837], # [37.2655159, -109.6257182], # [37.6254146, -109.4780126], # [37.6254311, -109.4754401], # [38.5724833, -109.5507785], # [38.6109465, -109.6081511], # [38.6164979, -109.6157153]] m = folium.Map(location=[37.5, -111], zoom_start=7.0) m.line(locations=locs) m.simple_marker(locs[0], popup='Grand Canyon, AZ, USA') m.simple_marker(locs[-1], popup='Arches, UT, USA') inline_map(m)
各ステップ中のより詳細な座標は polyline
中に Encoded Polyline Algorithm Format で記録されている。これは googlemaps.convert.decode_polyline
関数でデコードできる。
steps[0]['polyline'] # {u'points': u'_z`{EljmkT\\fF'} googlemaps.convert.decode_polyline(steps[0]['polyline']['points']) # [{'lat': 36.05424, 'lng': -112.14007000000001}, # {'lat': 36.05409, 'lng': -112.14123000000001}]
全ての step
から polyline
中の座標を取得すればよさそうだ。適当な関数を書いて地図上にプロットする。
def get_polylines_from_steps(steps): results = [] decode = googlemaps.convert.decode_polyline for step in steps: pl = step['polyline'] locs = decode(pl['points']) locs = [[loc['lat'], loc['lng']] for loc in locs] results.extend(locs) return results locs = get_polylines_from_steps(steps) locs[:10] # [[36.05424, -112.14007000000001], # [36.05409, -112.14123000000001], # [36.05409, -112.14123000000001], # [36.054140000000004, -112.14124000000001], # [36.05418, -112.14126], # [36.05422, -112.14128000000001], # [36.05427, -112.14131], # [36.05431, -112.14135], # [36.05436, -112.14138000000001], # [36.05442, -112.14145]] m = folium.Map(location=[37.5, -111], zoom_start=7.0) m.line(locations=locs) m.simple_marker(locs[0], popup='Grand Canyon, AZ, USA') m.simple_marker(locs[-1], popup='Arches, UT, USA') inline_map(m)
まとめ
folium
から Leaflet の以下の機能を利用する方法を記載した。
- シンプルマーカーの表示とクラスタ表示
- サークルの表示
- ポリラインの表示と Google Maps Direction API の利用