日本全国で二郎系ラーメンが食べられる場所を可視化するマップを作った物語

## TL;DR

~~いつでもどこでもカロリーを摂取できるように、~~ 位置情報データの可視化にめっちゃ便利なライブラリ[folium](https://github.com/python-visualization/folium)を布教するために、こういうマップを作って遊んでみました。
<img width="1298" alt="東京都心部" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143008/bdf7fb5f-3026-18ef-099e-cf9df99410d7.png">
東京都心部はどこでも万遍なく二郎系ラーメンが食べられそうですが、特に神田・秋葉原・御茶ノ水エリアあたりには二郎系ラーメンのお店がいっぱいですね。
ちなみに引きのviewはこんな感じ。
<img width="1298" alt="日本全国(と一部海外)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143008/146dffb9-9b5f-8a3e-4a79-1d1567320945.png">


下のリンクにアクセスいただくと、ブラウザ上からグリグリ動かして操作もできます。
https://moyashi-ramen-map.herokuapp.com/
※ページだけで1.2MBあるのでやや重いです…。スマホだと性能にもよると思いますがかなりもっさり動作になってしまうと思います。

ここからこのマップの作成プロセスについて書いていきます。

#### 追記

大変ありがたいことに、[GIGAZINE](https://gigazine.net/)様にて本記事を紹介いただきました。操作説明まで詳細に書いていただきありがとうございます…!
[
最寄りの二郎系ラーメン店が一目で確認できるマップが登場、二郎ヒートマップ機能も](https://gigazine.net/amp/20190430-ramen-jiro-map)

## 1.　データ収集

[ラーメンデータベース](https://ramendb.supleks.jp/)にて、「二郎系」タグがついているラーメン屋の情報をスクレイピングにより取得してみました。
のちに、店舗ごとの緯度経度ベースの位置情報が必要になるため、住所情報を個別の店舗ページから取得しました。なお、一部「閉店中」や「移転済み」の店舗が含まれていたため、後段のプロセスではそれらを除いています。
ページに特殊な仕様などはないため、オーソドックスなスクリプトで無事取得できたのでコードは省略します。


得られたデータを集計すると、日本全国(とあと一部海外も)に二郎系ラーメンを出すラーメン屋は **861** 店舗あるそうです。
ラーメンデータベースの「二郎系」タグには本家ラーメン二郎や二郎系インスパイア専門店だけでなく、二郎系ラーメンをメニューとして出す普通のラーメン屋も含まれていることを考えるとやや少ないような気もしますが、そのまま進めます。

ちなみに、サクッと都道府県別に店舗数をカウントすると、店舗数TOP10の顔ぶれはこんな感じ。
トップは圧倒的に東京、全体の4分の1近い店舗が東京にあることになります。
それに周辺の埼玉県神奈川県千葉県、あと愛知大阪あたりがはいるのは予想してましたが、茨城栃木長野にも以外に店舗が多いんですね

|順位|都道府県|店舗数|
|---|---|---|
|1:trophy:|東京都|203|
|2|埼玉県|91|
|3|神奈川県|74|
|4|千葉県|59|
|5|愛知県|50|
|6|茨城県|41|
|7|大阪府|33|
|8|群馬県|24|
|9|栃木県|21|
|10|長野県|21|


## 2. 店舗の住所から緯度経度を取得

取得した店舗の情報から、[geocoding API](https://www.geocoding.jp/api/)にて、取得した店舗の住所情報を緯度経度に変換しました。いつも便利で助かってます。
無料のAPI提供者様側に迷惑をかけないように間隔を空けながらアクセスする必要があり、また一時的にアクセス制限がかけられても、続きから再開できるように、という観点で過去に私が書いた記事[^1]のコードをちょっとだけいじって下記のようにアクセスしてみました。

```.py
import requests
from bs4 import BeautifulSoup
import time
from tqdm import tqdm

def coordinate(address, url='http://www.geocoding.jp/api/'):
    """
    addressに住所を指定すると緯度経度を返す。緯度経度が取得できなかった場合は便宜的に 
    ['0','0']を返す。
    >>> coordinate('東京都文京区本郷7-3-1')
    ['35.712056', '139.762775']
    """
    payload = {"v": 1.1, 'q': address}
    html = requests.get(url, params=payload)
    soup = BeautifulSoup(html.content, "html.parser")
    if not soup.find('lat'):
        print(f"Invalid address submitted. {address}")
        return '0', '0'
    latitude = soup.find('lat').string
    longitude = soup.find('lng').string
    return latitude, longitude


with open('address_lat_lon.tsv', 'w') as f:
    # 予め緯度経度を取得したい住所のlistを変数に格納しておく(addresses)
    for address in tqdm(addresses):
        lat, lon = coordinate(address)
        f.write(f"{address}\t{lat}\t{lon}\n")
        time.sleep(10)

```

※実際にはこの後、緯度経度が取得できなかったデータの住所をキレイにして(住所の末尾にビル名とかが入っていたり、住所先頭の郵便番号を除いてみたり)再チャレンジしたり、といった地味な処理をしてます。

そのプロセスを経ても4店舗だけ住所から緯度経度を正しく取得できないケース[^2]があり、後段の可視化プロセスではそれらを除いた857店舗を可視化に使っています。

## 3. 集めたデータを地図上に描画していく

で、緯度経度データを可視化していきます
そこでfoliumの出番ですね。
foliumはleaflet.jsのPythonラッパーで、JSの知識がなくてもleaflet.jsベースのインタラクティブな地図が作成できます。

今回、実際に利用したコードはこんな感じ。
マップの見た目に反して少ないコード量でヒートマップが作成できることが理解いただけたかと思います。

```.py
from folium import Map, Marker, CustomIcon, LayerControl
from folium.plugins import HeatMap

# いらすとやから「もやしがたくさん乗ったラーメンのイラスト」をアイコン用に拝借
icon_ramen = "https://2.bp.blogspot.com/-09XFbYdTmLs/VwIgXThmQWI/AAAAAAAA5bg/3TdIOG1frrEsHFWOGi5GTqD4X5k8qrxJQ/s400/ramen_moyashi.png"

# ラーメン屋の店舗名、URL、レビュースコア、緯度経度が入ったpandasデータフレーム
df = pd.read_csv('active_shops_with_latlon.csv')

# foliumのMapオブジェクトを作成。初期位置は、データに含まれる緯度経度を利用
m = Map(location=[df['lat'].mean(), df['lon'].mean()], zoom_start=5)
# Mapオブジェクトに、緯度経度ベースのヒートマップを追加。
# なお、ラーメンデータベース上のレビュースコアによる重みをつけている(スコアが高いと色が濃くなる)
HeatMap(df[['lat', 'lon','review_score']].values.tolist(),name="ヒートマップ").add_to(m)
for row in df.itertuples():
    # 一店舗ずつマーカーをMapオブジェクトに追加していく。
    Marker(location=(row.lat, row.lon),
                 # ポップアップに表示する項目をhtmlタグで設定
                 popup='<a href="{url}" target="_blank">{name}</a>'.format(url=row.url, name=row.name),
                 # CustomIconを使うことで、任意の画像をマーカーのアイコンに設定可能
                 icon=CustomIcon(icon_ramen,
                                 icon_size=(20, 20),
                                 popup_anchor=(0, 0)),
                 ).add_to(m)
# 描画したレイヤーをコントロールするパネルを追加。作成されたファイルの右上に追加できる。
LayerControl().add_to(m)
# 作成したMapオブジェクトをhtmlとして保存
m.save("heatmap.html")
```

### 1分で読めるfoliumの使い方

* まず、何より先に `folium.Map()` オブジェクトを作成します。
* 次に、`Marker()` や `Heatmap()` を用いて、地図上に描画したいオブジェクト(レイヤー)を定義し、 `.add_to(<作成したmapオブジェクト>)` で、最初に作成した地図に描画することができます。
* `LayerControl()` を使うことで、これまで作成したレイヤーの表示/非表示を切り替えることができるようになります。

簡単でしょ?



## 4. 作成したマップをherokuでデプロイ

foliumで作成したhtmlファイルは、ローカルファイルとして存在するので、ブラウザから開いてあげることで正常に動作します。
でも、せっかくなのでweb上からアクセスできるようにしたいですよね?

ということで、[こちらの記事](https://qiita.com/kanza0awa/items/b8d475e67a147cd54453)を参考に、作成したhtmlファイルをherokuを使ってデプロイしました。
ここまで難なく読み進められた方なら10分かからずに終わるでしょう。

## 5. まとめ

今までfoliumを使うときにはアドホック分析がメインだったのですが、htmlファイルなのでデプロイも簡単にできますね(文字にしてみると当たり前過ぎてちょっと恥ずかしい。。。)
foliumで作成したマップ全般にいえることではありますが、だいぶ動作はもっさりした感じになってしまいます。
言い訳がましくてアレですが、普段はデータアナリストをしておりwebアプリ開発の知識はほぼゼロなので、どなたか知見があれば教えてください。

なお、利用したコードは[github](https://github.com/paulxll/pj-moyashi-ramen-map)にて公開しています。

[^1]: [住所から緯度経度を取得するpythonスニペット](https://qiita.com/paulxll/items/7bc4a5b0529a8d784673)
[^2]: ラーメンデータベースに登録されている住所が間違っている?と思しきケースと、海外の店舗など