【Python】グルメな人になりたくて〔Data Drivenなアプローチ〕年末年始のお店選び

# 1. モチベーション
**『スクレイピングを駆使して、食べログの点数に関係なく美味しくて実力のあるレストランを探す！』**

* 外食をする際に食べログをよく利用させてもらっているが、食べログの点数には**口コミが少ない間は点数が低く出る**という特徴がある。実際そういう記載がある。（https://tabelog.com/help/score/）

> ユーザーの皆様の声を反映させた指標として、影響度を持つユーザーからのより多くの高い評価が集まることで点数が上がる仕組みになっています。例えば、仮に同じ影響度の場合、5点の評価が2件しかないお店よりも、5点の評価が100件集まっているお店の方が高い点数になります。

* でも開店したばかりだから**口コミが少ないだけで食事もサービスも素晴らしいというお店は存在する**はず。
* ただし僕のようにランキング検索に頼ってばかりでは永遠にこのようなお店と巡り会うことはない。
* そこでスクレイピングを駆使して、開店して日の浅い実は高評価なレストランを抽出していく。
* 美味しいご飯にありつけること自体嬉しいし、~~「こんなお店知ってるのね！素敵！」ってなるに違いない。~~

# 2. 実装したコード
<details><summary>Pythonで食べログから情報取得するコード</summary><div>

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import requests
from bs4 import BeautifulSoup
import math
import time

root_url = 'https://tabelog.com/'
res = requests.get(root_url)
soup = BeautifulSoup(res.content, 'html.parser')
a = soup.find_all('a', class_='rsttop-area-search__target js-area-swicher-target')

area_url = {}
for area in a:
    area_dict = {}
    splitted = area.get('data-swicher-area-list').split('"')
    for i in range(int((len(splitted)-1)/8)):
        area_dict[splitted[i*8+3]] = splitted[i*8+7]
    area_url[area.get('data-swicher-city').split('"')[3]] = area_dict

visit_areas = ['渋谷・恵比寿・代官山']
url_dict = {}
for visit_area in visit_areas:
    url = area_url['東京'][visit_area]
    time.sleep(1)
    res = requests.get(root_url + url[1:])
    soup = BeautifulSoup(res.content, 'html.parser')
    a = soup.find_all('a', class_='c-link-arrow')
    for area in a:
        href = area['href']
        if href[-21:-8]!=url:
            continue
        else:
            url_dict[area.text] = href

max_page = 20
restaurant_data = []
for area, url in url_dict.items():
    time.sleep(1)
    res_area = requests.get(url)
    soup_area = BeautifulSoup(res_area.content, 'html.parser')
    rc_count = int(soup_area.find_all('span', class_='list-condition__count')[0].text)
    print('There are ' + str(rc_count) + ' restaurants in ' + area)
    for i in range(1,min(math.ceil(rc_count/20)+1,max_page+1,61)):
        print('Processing...  ' + str(i) + '/' + str(min(math.ceil(rc_count/20)+1,max_page+1,61)-1))
        url_rc = url + 'rstLst/RC/' + str(i) + '/?Srt=D&SrtT=nod'
        res_rc = requests.get(url_rc)
        soup_rc = BeautifulSoup(res_rc.content, 'html.parser')
        for rc_div in soup_rc.find_all('div', class_='list-rst__wrap js-open-new-window'):
            rc_name = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name').text
            rc_url = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name')['href']
            rc_score = rc_div.find('span', class_='c-rating__val c-rating__val--strong list-rst__rating-val')
            if rc_score is None:
                rc_score = -1.
            else:
                rc_score = float(rc_score.text)
            rc_review_num = rc_div.find('em', class_='list-rst__rvw-count-num cpy-review-count').text
            if rc_review_num != ' - ':
                page = 1
                score = []
                while True:
                    rc_url_pg = rc_url + 'dtlrvwlst/COND-2/smp0/?smp=0&lc=2&rvw_part=all&PG=' + str(page)
                    time.sleep(1)
                    res_pg = requests.get(rc_url_pg)
                    soup_pg = BeautifulSoup(res_pg.content, 'html.parser')
                    if 'お探しのページが見つかりません' in soup_pg.find('title').text:
                        break
                    try:
                        station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('span', class_='linktree__parent-target-text').text
                    except:
                        try:
                            station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('dd', class_='rdheader-subinfo__item-text').text.replace('\n','').replace(' ','')
                        except:
                            station = ''
                    genre = '/'.join([genre_.text for genre_ in soup_pg.find_all('dl', class_='rdheader-subinfo__item')[1].find_all('span', class_='linktree__parent-target-text')])
                    price = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[2].find('p', class_='rdheader-budget__icon rdheader-budget__icon--dinner').find('a', class_='rdheader-budget__price-target').text
                    score = score + [score_.next_sibling.next_sibling.text for score_ in soup_pg.find_all('span', class_='c-rating__time c-rating__time--dinner')]
                    page += 1
                    if page == math.ceil(int(rc_review_num)/100)+1:
                        break
                restaurant_data.append([area, rc_count, rc_name, rc_url, rc_score, rc_review_num, station, genre, price, score])
```

</div></details>

また、コードの説明や詳細については[スクレイピングと食べログ　〜いいレストランを探したい！〜　（作業編）](https://qiita.com/asys/items/a5efa27d6563a7f6ca14)にまとめてあるので興味があればご覧ください。

# 3. 気になる結果は・・・？？
## データをプロットして可視化
結果は以下のようになりました！
渋谷・恵比寿・代官山のレストランでニューオープン順それぞれ上位400件のレストランが対象です。
![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191326/878613f0-04ac-fef7-849a-4cb3bdb601fb.png)

* 縦軸が食べログのスコア、横軸が口コミのスコアの平均値となります。
* 口コミの少ないレストランを赤、口コミが多くなるに従って黒くなるように色分けしています。
* 食べログのスコア未付与のレストランがあるのでそれらについてはスコア2.9としてプロットしました。

## インプリケーション
この散布図から読み取れることは・・・

* **口コミスコア平均と食べログのスコアには正の相関**があり、特に口コミの多い（マーカーの色が黒に近い）お店でそれが顕著である。
* 口コミの少ない（マーカーの色が赤に近い）お店は総じて食べログのスコアは低いものの、口コミスコア平均はばらつきが大きい。
* 口コミが少ないため**食べログのスコアは低いものの口コミの評価は高い掘り出し物のお店は確かに存在している！！** 散布図の右下のエリアのお店がそれに該当します。
* 実際に各レストランのページを見てみると、非常に好意的な口コミが載っている。
* ただし、これらがレストラン側のマーケティングの可能性もあるため実際に行く場合は考慮が必要である。中には他の場所で既に営業しており食べログのスコアも高いレストランの系列店が新しく渋谷にオープンしたというようなケースもあり、このような場合はかなり信頼性が高いと思われる。
* 申し訳ないのですが、食べログのデータを私的利用以外に使っていると誤解されたくないので、具体的なレストラン名をここに載せることは控えます。

# 4. より深い考察
ここからは僕の好奇心のおもむくまま、もう少し考察して行く。
## スコアの分布
先ほどの散布図を見て気になったのは食べログスコアの分布がいびつで、特定の点数にレストランが集中しているということ。ということで分布を見てみると
![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191326/bcb2bc7f-91d9-6de8-35c4-3c2140970f61.png)
特定の点数にレストランが集中していることも気になりますが、特定の点数にレストランが極端に少ないことも気になります。

* 3.04 (41件)　→　3.05 (7件)
* 3.09 (83件)　→　3.10 (10件)
* 3.29 (20件)　→　3.30 (2件)
* 3.34 (40件)　→　3.35 (7件)

口コミ件数との絡みを調べてみたものの、これを説明できる結果は得られず・・・
もしかしたらオープンからの日数など今回取得していないデータが鍵なのかもしれない。
噂ベースではレストランが食べログに支払う年会費によって食べログスコアの上限があるという説もあり、これも影響している可能性がある。
ただこれも考え方によっては、点数の上限設定によって食べログスコアは低いものの口コミのスコアは高いレストランを発生させる要因となっており、今回紹介したアプローチを使えば**必要以上に人気化していない優良店を見つけることができる**わけである。

# 5. まとめ
* スクレイピングで食べログからデータを集めることができた。
* 食べログスコアと口コミのスコアに相関はあるものの、口コミのスコアは高いのに食べログのスコアは低いというお店は確かに存在する。
* そういったお店の本当の実力については、これから実際に食べに行って確認します！笑
* 今回の目的からは外れるが、もう少し時間をかけてスコアの仕組みをより深く解明していけば、**レストランの効率的なマーケティング戦略**を導き出すことができそうである。それはまた気が向いた時に〜