いざとなれば会社に通える田舎を探す(その１駅情報取得)

# 背景

コロナで在宅勤務が続いているため、都会で手狭な家に住んでいるのが苦しくなってきました。週に数日や月に数日しか出社しないなら、通勤に片道2時間ぐらいかかる田舎で暮らすことも現実的かも。（定期代がフルに支給されない）新幹線通勤までも視野に入れれば結構遠くに住めるかも？

でもどの地域を狙うのといいのか？ 

しばらく前に[「東工大の一限に間に合う範囲の地図を作ってみました」](https://j-town.net/tokyo/column/gotochicolumn/312914.html?p=all)というのが話題になってました。

同じような地図を作ってみたいと思い、会社の最寄り駅に平日朝9時に到着するための乗車時間を地図にマッピングしてみました。

# 手順

1. 日本全国の駅名と緯度経度を取得
2. 各駅から最寄り駅までの経路を検索し所要時間を取得
3. 各駅の位置と所要時間を地図にプロット

これらをできるだけオープンソースのツールか、フリーミアムのサービスの無料枠でなんとかしたい。

今回は駅情報取得について。

# 経路検索サービスの検討

今回一番ネックになりそうなのが経路検索のところ。hideki@鉄分多め さんの[「Google Directions APIで乗り換え案内情報を取ろうとしたが。。。？」ノート](https://note.com/jinka1997/n/n182603e52c3e)を参考に検討。
Google Mapsの路線検索が使えないのは残念。

有料路線検索サービス提供業者の中で、[駅すぱあと](https://docs.ekispert.com/v1/le/)、[ジョルダン](https://norikae.jorudan.co.jp/openapi/)、[Navitime](https://api-sdk.navitime.co.jp/api/)は無料枠や無料APIの用意があるようです。

今回はユーザー登録後すぐにアクセスキーを送ってきてくれた「駅すぱあとWebサービス フリープラン」を利用することにします。

# 駅情報取得

Pythonを利用。外部ライブラリとしてrequestとpandasを使います。

```python:ekispert-get-all-stations.py
import requests
import pandas as pd

# security tokens are defined in secret.py file
# ekispert_token = ""
from secret import ekispert_token

ekispert_endpoint = 'http://api.ekispert.jp'
eki = '/v1/json/station'

from collections import MutableMapping 
def flatten(d, parent_key='', sep='_'):
    """
    flatten dictionary
    https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys
    """
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

offset = 1
#print("offset: {}".format(offset))
payload = {'key': ekispert_token, 
           'offset': offset,
           'limit': 100,
           'gcs': 'wgs84'}
r = requests.get(ekispert_endpoint+eki, params=payload)
result_all = r.json()
offset += 100
n_max = int(result_all['ResultSet']['max'])
#print("Max: {}".format(n_max))

while offset <= n_max:
    #print("offset: {}".format(offset))
    payload = {'key': ekispert_token, 
               'offset': offset,
               'limit': 100,
               'gcs': 'wgs84'}
    r = requests.get(ekispert_endpoint+eki, params=payload)
    result_all['ResultSet']['Point'].extend(r.json()['ResultSet']['Point'])
    offset += 100

stations_flat = [flatten(result) for result in result_all['ResultSet']['Point']]
df = pd.DataFrame(stations_flat)

df['GeoPoint_longi_d'] = df['GeoPoint_longi_d'].apply(float)
df['GeoPoint_lati_d'] = df['GeoPoint_lati_d'].apply(float)

fname = "ekispert_stations_" + result_all['ResultSet']['engineVersion'] + ".pkl"
df.to_pickle(fname)
```

## 一部補足説明

```python
offset = 1
#print("offset: {}".format(offset))
payload = {'key': ekispert_token, 
           'offset': offset,
           'limit': 100,
           'gcs': 'wgs84'}
r = requests.get(ekispert_endpoint+eki, params=payload)
result_all = r.json()
offset += 100
n_max = int(result_all['ResultSet']['max'])
#print("Max: {}".format(n_max))
```
とりあえず一度駅情報を100件分取得。'gcs'を'wgs84'としているのは、地図へのプロットをmapboxのサービスを利用するつもりなので。
2020年1月11日時点では全部で9,274件分のデータが存在するらしい。

```python
while offset <= n_max:
    #print("offset: {}".format(offset))
    payload = {'key': ekispert_token, 
               'offset': offset,
               'limit': 100,
               'gcs': 'wgs84'}
    r = requests.get(ekispert_endpoint+eki, params=payload)
    result_all['ResultSet']['Point'].extend(r.json()['ResultSet']['Point'])
    offset += 100
```
残りデータをオフセットしながら取得。駅の情報はr.json()['ResultSet']['Point']にリスト形式で存在するので、最初に取得したresult_all['ResultSet']['Point']のリストに追加(extend)していく。

```python
stations_flat = [flatten(result) for result in result_all['ResultSet']['Point']]
```

駅情報は入れ子構造の辞書形式になっている。扱いづらいので、flatten関数にかけて単純な辞書形式に変換しておく。

flatten前

```python
{'Station': {'code': '26242',
  'Name': '相生(兵庫県)',
  'Type': 'train',
  'Yomi': 'あいおい'},
 'Prefecture': {'code': '28', 'Name': '兵庫県'},
 'GeoPoint': {'longi': '134.28.26.46',
  'lati': '34.49.5.91',
  'longi_d': '134.474019',
  'lati_d': '34.818309',
  'gcs': 'wgs84'}}
```

flatten後

```python
{'Station_code': '26242',
 'Station_Name': '相生(兵庫県)',
 'Station_Type': 'train',
 'Station_Yomi': 'あいおい',
 'Prefecture_code': '28',
 'Prefecture_Name': '兵庫県',
 'GeoPoint_longi': '134.28.26.46',
 'GeoPoint_lati': '34.49.5.91',
 'GeoPoint_longi_d': '134.474019',
 'GeoPoint_lati_d': '34.818309',
 'GeoPoint_gcs': 'wgs84'}
```

```python
df = pd.DataFrame(stations_flat)
df['GeoPoint_longi_d'] = df['GeoPoint_longi_d'].apply(float)
df['GeoPoint_lati_d'] = df['GeoPoint_lati_d'].apply(float)
```

Pandas.DataFrameオブジェクトを作成し、緯度経度情報を文字列からfloatに変換。

```python
fname = "ekispert_stations_" + result_all['ResultSet']['engineVersion'] + ".pkl"
df.to_pickle(fname)
```

バージョン名をつけてpickle形式で保存。

# 分析

せっかくなので全国の駅情報をちょっと覗いてみる。

```python
df = pd.read_pickle("ekispert_stations_202101_02a.pkl")
```

先程保存したファイルからpandas.DataFrameを読み戻す。

```python
df[df['Station_Type']=='train'].groupby('Prefecture_Name').count()['Station_code'].sort_values(ascending=False).plot(kind='bar', figsize=(15,5))
```
![train.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138680/9e1c1934-5d91-a646-b5b4-9492516721af.png)

鉄道の駅の数を都道府県別に並べてみた。当然東京・大阪・愛知の大都市圏が多いですね。そんな中北海道も面積が大きいからか駅数が多い。（でも人は少ないだろうから経営は大変だろうなぁ）

```python
df[df['Station_Type']=='plane'].groupby('Prefecture_Name').count()['Station_code'].sort_values(ascending=False).plot(kind='bar', figsize=(15,5))
```
![plane.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138680/9e8daf4a-045c-aef1-151a-b8f15a993e39.png)

空港の数でみると、北海道が多いのは納得。東京が10個もあるのかと一瞬驚きますが、大島や三宅島なども東京都だからですね。

```python
pt = df.groupby(['Prefecture_Name', 'Station_Type']).count()['Station_code'].unstack(fill_value=0).stack()
pt[pt==0].index
```

逆に空港がゼロの県はこれら。

```python
MultiIndex([( '三重県', 'plane'),
            ( '京都府', 'plane'),
            ( '埼玉県', 'plane'),
            ( '奈良県', 'plane'),
            ( '山梨県', 'plane'),
            ( '岐阜県', 'plane'),
            ( '栃木県', 'plane'),
            ( '滋賀県', 'plane'),
            ('神奈川県', 'plane'),
            ( '福井県', 'plane'),
            ( '群馬県', 'plane')],
           names=['Prefecture_Name', 'Station_Type'])
```

全駅をmapboxを使って地図上にプロットしてみる。

```python
from mapboxgl.utils import df_to_geojson, create_color_stops
from mapboxgl.viz import CircleViz
# security tokens are defined in secret.py file
# mapbox_token = ""
from secret import mapbox_token
points = df_to_geojson(df, 
                      properties=['Station_code', 'Station_Name', 'Station_Type', 'Station_Yomi', 'Prefecture_code', 'Prefecture_Name'],
                      lat='GeoPoint_lati_d',
                      lon='GeoPoint_longi_d',
                      precision=6)
map_center = df[df['Station_Name']=='日本橋(東京都)'].to_dict('records')[0]
viz = CircleViz(points,
                access_token=mapbox_token,
                height='400px',
                radius=10,
                stroke_color='black',
                opacity=0.6,
                label_property='Station_Name',
                center=(map_center['GeoPoint_longi_d'],map_center['GeoPoint_lati_d']),
                style='mapbox://styles/mapbox/outdoors-v11',
                zoom=3,
                )
viz.create_html("all_stations.html")
viz.show()
```
[mapboxgl-jupyter](https://github.com/mapbox/mapboxgl-jupyter)を使うとたったこれだけのコードでぐりぐりできるマップが作れちゃいます。

![all_stations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138680/41811d16-efc2-eedd-1093-bfe6864330a9.png)

[こちらにHTML置いておいた](https://s3inaka.s3-ap-northeast-1.amazonaws.com/all_stations.html)のでぐりぐりしてみてください。

Powered by 駅すぱあとWebサービス