読者です 読者をやめる 読者になる 読者になる

StatsFragments

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

Python pandas + folium + Jupyter でリーフレット / コロプレス図を描きたい

引き続き、 R の可視化を Python に持ってくるシリーズ。R には以下のようなパッケージがあり、地図上へのリーフレット配置やコロプレス図の描画がカンタンにできる。それぞれの概要はリンク先を。

これを Python でやりたい。調べてみると folium というパッケージが上記 両方をサポートしているようなので使ってみる。

github.com

インストール

pip で。

pip install folium

準備

以降の操作は Jupyter Notebook から行う。まずはパッケージをロードする。

import numpy as np
import pandas as pd
import folium

folium は プロット結果を html としてエクスポートすることを想定して作成されているようだ。そのため、結果を Jupyter 上に埋め込みたい場合は 以下のような関数を定義する必要がある。

from IPython.display import HTML

def inline_map(m):
    # 中間生成される json が必要なプロットがあるため、一度 html として書き出し
    m.create_map(path='tmp.html')
    iframe = '<iframe srcdoc=\"{srcdoc}\" style=\"width: 100%; height: 400px; border: none\"></iframe>'
    return HTML(iframe.format(srcdoc=m.HTML.replace('\"', '&quot;')))

リーフレット

手順は以下のようになる。

  1. folium.Map で地図を描画する範囲を緯度経度/ズームにより指定
  2. Map.simple_marker で緯度経度を指定してリーフレットを配置 (複数配置する場合は繰り返し)
  3. Map.create_map で地図を含む html を生成 ( Jupyter 上に描画する場合は上で定義した関数 inline_map を呼ぶ )
m = folium.Map(location=[33.763, -84.392], zoom_start=17)
m.simple_marker([33.763006, -84.392912], popup='World of Coca-Cola')
inline_map(m)

f:id:sinhrks:20150614211105p:plain

補足 Jupyter 上ではスクロール / 拡大縮小できる。

既定では OpenStreetMap が利用されるが、Mapboxmaps.stamen を使うこともできる。また、リーフレットのマーカーとしては以下のものが利用できる。

それぞれの描画サンプルは README で確認することができる。

コロプレス図

コロプレス図を描くには以下2つのデータソースが必要である。

  • GeoJSON もしくは TopoJSON 形式のファイル
  • コロプレス図を色分けするための値を含む pandasDataFrame

サンプルとして 国別のマクドナルドの店舗数 をプロットする。

GeoJSON ファイルの準備

国別にプロットするため、国別の GeoJSON ファイルがほしい。以下リポジトリcountries.geo.json をローカルに保存して使う。

中身は 以下のように ISO 3166-1 alpha-3 の国コードを id としたデータとなっている。

{"type":"FeatureCollection",
 "features":[{"type":"Feature","id":"AFG",
              "properties":{"name":"Afghanistan"},
              "geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],[62.230651,35.270664],...
DataFrame の準備

DataFrame は 上で準備した GeoJSON と紐づけるためのキー (一般には GeoJSON の id ) と値の 2 列をもつ必要がある。 国別のマクドナルドの店舗数 データを pd.read_html で読み込む。

url = "https://en.wikipedia.org/wiki/List_of_countries_with_McDonald%27s_restaurants"
df = pd.read_html(url, header=0, index_col=0)[0]
# 列名を変更
df.columns = ['Country', 'Date', 'First outlet location', 'Number', 'Source', 'Note', 'CEO']
df[['Country', 'Number']].head()
#                Country Number
# #                            
# 1        United States  14267
# 2               Canada   1427
# 3          Puerto Rico    108
# 4  U.S. Virgin Islands      6
# 5           Costa Rica     54

こちらのデータには国名が入っているため、GeoJSON と紐づけるためには ISO 国コードに変換する必要がある。変換には pycountry を使う。インストールしていない方は pip で。

import pycountry
pycountry.countries.get(name='Japan').alpha3
# 'JPN'

# データ中の国名が pycountry のものと違う場合の mapper
countries = {'United Kingdom': 'United Kingdom',
             'Russia': 'Russian Federation',
             'South Korea': 'Korea, Republic of',
             'Taiwan': 'Taiwan, Province of China',
             'Vietnam': 'Viet Nam', }
def f(x):
    for k, v in pd.compat.iteritems(countries):
        if k in x:
            x = v
    try:
        return pycountry.countries.get(name=x).alpha3
    except KeyError:
        return np.nan

# 国コードへの変換
df.loc[:, 'Code'] = df['Country'].apply(f)
# 国コードに変換できなかったデータは除外
df = df.dropna(subset=['Code'])

# 欠損値を 0 でパディング
df.loc[:, 'Number'] = df['Number'].fillna('0')
# 数値に変換できない文字列があるため、余計な文字を削除
df.loc[:, 'Number'] = df['Number'].str.replace('[+,]', '')
# 数値 (float) 型に変換
df.loc[:, 'Number'] = df['Number'].astype(float)
df[['Code', 'Country', 'Number']].sort('Number', ascending=False).head()
#    Code        Country  Number
# #                             
# 1   USA  United States   14267
# 8   JPN          Japan    3164
# 49  CHN          China    2000
# 11  DEU        Germany    1468
# 2   CAN         Canada    1427

これで、国コード / プロットする値をもつ DataFrame が準備できた。アメリカすごいな、、、。

コロプレス図の描画

Map.geo_json で コロプレス図を描画するレイヤーを Map に追加できる。ここで使っている引数の意味は以下。

  • geo_path: GeoJSON ファイルのパス
  • data: 色分けのための値をもつ DataFrame
  • columns: dataGeoJSON と紐づけるキー / 値 を含む列名
  • key_on: GeoJSON 側で紐付けに使うキー
  • threshold_scale: 色分けをする際の閾値
  • fill_color 色分けに使う color-brewer の名前
  • reset: 既存のレイヤがある場合に削除する
m = folium.Map(location=[10, 35], zoom_start=1.5)

geojson = r'countries.geo.json'
m.geo_json(geo_path=geojson, data=df,
           columns=['Code', 'Number'],
           key_on='feature.id',
           threshold_scale=[1, 100, 500, 1000, 2000, 4000],
           fill_color='BuPu', reset=True)
inline_map(m)

f:id:sinhrks:20150614211127p:plain

まとめ

folium を使えば リーフレット / コロプレス図の描画がカンタンにできる。Jupyter 上で Javascript を使用してのデータ可視化、結構使えるのでは。

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

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