StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas + folium で Leaflet をもっと使いたい

先日参加させていただいた Japan.R でこんな話を聞いた。

Python でも folium というパッケージを使うと JavaScript を書かなくても Leaflet.js の一部機能が使えるのだがあまり情報がない。上の資料に書いてあるようなことが folium でもできるのか調べたい。

folium については前にこんなエントリを書いた。

sinhrks.hatenablog.com

データの準備

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

f:id:sinhrks:20151226220345p:plain

位置情報は "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.extractSeries 中の各要素から正規表現にマッチしたグループを取り出し、州名、緯度、経度 3 列の DataFrame に展開する。

locations = df['Location'].str.extract(u'(\D+) (\d+°\d+′[NS]) (\d+°\d+′[WE]).*')
locations.columns = ['State', 'lat', 'lon']
locations

f:id:sinhrks:20151226223048p:plain

数値としてパースできるよう、.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

f:id:sinhrks:20151226223056p:plain

最後の 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

f:id:sinhrks:20151226223104p:plain

処理した DataFramepd.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('\"', '&quot;')))

まずは シンプルなマーカー。これは前のエントリでやったことと同じ。

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)

f:id:sinhrks:20151226221237p:plain

マーカーをクラスタとして表示するには、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)

f:id:sinhrks:20151226221256p:plain

地図を拡大・縮小するとマーカーのクラスタ表示が適当に切り替わる。

f:id:sinhrks:20151226221315p:plain

サークルを表示するには 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)

f:id:sinhrks:20151226221420p:plain

ポリラインは 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)

f:id:sinhrks:20151226223237p:plain

Google Maps Direction API の利用

2 点間の経路をポリラインで描画したい、といった場合は 上のやり方 / 現在のデータでは無理だ。Google Maps Direction API を使って取得した 2 点間の経路をポリラインとして描きたい。

Google Maps Direction API へのアクセスには googlemaps パッケージを利用する。インストールは pip でできる。

ドキュメントはなく、使い方はテストスクリプトを見ろ、とだいぶ硬派な感じだ。それでも自分で API 仕様を調べるよりは早いと思う。

import googlemaps
googlemaps.__version__
# '2.4.2'

googlemapsGoogle Map に関連したいくつかの API をサポートしている。Directions API を使うには Client.directionsmode として渡せるのは "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)

f:id:sinhrks:20151226224133p:plain

各ステップ中のより詳細な座標は 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)

f:id:sinhrks:20151226225811p:plain

まとめ

folium から Leaflet の以下の機能を利用する方法を記載した。

  • シンプルマーカーの表示とクラスタ表示
  • サークルの表示
  • ポリラインの表示と Google Maps Direction API の利用

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理