きっかけはツイート
突然だがラーメン屋は何曜日に閉まる店が多いのか、統計を取りたくなった。
— ラーメン大好きトムスクさん (@tomsk7) 2019年2月20日
ラーメン屋は何曜日に休みのお店が多いのかを調べてみようと思ったきっかけは、フォロワーのツイートだった。調べることは好きだけど、これまでは人力で調べてばかりだった。Pythonを勉強してから、ずっとウェブスクレイピングやWeb APIに興味があったので、折角なので挑戦してみた。
ラーメン屋は月曜日が休みが多い
HOT PEPPERとぐるなびがAPIを提供しているので、そこからラーメン店の情報を取得し、定休日を集計した。
データは2019年2月21日に取得し、HOT PEPPERからは2212件、ぐるなびからは4767件の店舗情報が集められた。これを元に、定休日を集計した。データはJSON形式で、定休日もキーとして割り振られているので、抽出は簡単だった。
HOT PEPPER、ぐるなびともに、年中無休がもっと多く全体の30%から40%を占めている。曜日としては、月曜日が休みの店が多くHOT PEPPERでは15%、ぐるなびでは11%であった。次いで、火曜、水曜、日曜が10%前後である。金土休みの店はほとんどなく、0.5%程度である。それを除けば、木曜休みが5%と、他の曜日に比べて半分程度であった。
無休と曜日別の定休日はそれぞれのサービスで似たような傾向にあったが、その他は異なる結果になった。それぞれのサービスに登録する店舗の傾向を反映しているのだろう。たとえば、不定休の店はHOT PEPPERでは6%だが、ぐるなびでは14%と倍近い違いがある。一方で、HOT PEPPERでは祝日前が定休日の店舗が2%程度なのに、ぐるなびには殆どなかった。
曜日別でも割合を算出した。何曜日に休みが多いのかは、こちらの方が適当だろう。月曜日が定休日の店が25%から30%で、火曜、水曜、日曜が大体20%くらいとなる。月曜休みの未成は多いが、突出して多いわけではない。火、水、日はHOT PEPPERとぐるなびとでばらつきがあったので、月曜日の次に多い曜日を特定するのは難しい。一方で、木曜日は10%と下がる。金土は稼ぎ時のためか、定休日のお店は1%くらいである。むしろ、数%でも存在していることが意外であった。
日曜日が休みの店舗はオフィス街などにあるのだろう。平日に休む場合は、月曜日が多く、次いで火曜日と水曜日となる。月曜日が休みなのは多客日の後に休みたい心理的な面と、日曜に市場が開いていないため食材調達上の問題もありそうだ。木曜日が定休の店が少ないのは、金曜日の仕込みなどがあるからだろうか。
他の業態の飲食店と比較すべきだが、今回は本筋ではないのでやらない。恐らく、他の飲食店でも似たような傾向になるだろう。
Pythonによる定休日の集計方法
HOT PEPPERとぐるなびのAPIを利用し、Pythonにてラーメン店の情報を取得しJSONとして保存した。そのデータから定休日の情報を抜き出し、集計を行った。
取得したJSONデータから直接定休日を集計した方が手順は省けるが、定休日以外の統計処理を見据えて店舗情報を保存することにした。
APIの叩き方は経験が無かったので少々戸惑ったけども、一度取得できれば試行錯誤は用意だった。むしろ、JSONデータや定休日を集計しCSV形式で保存したデータが文字化けしまくって四苦八苦した。
HOT PEPPERとぐるなびのAPI仕様の所感
食べログのAPIがあると便利そうだと思ってググったら、残念ながらかなり前に廃止されていることを知った。そこで、食べログAPI提供終了していたので代わりのAPIを探した(一覧) - エンジニアステップ を参考に、HOT PEPPERとぐるなびのAPIを使うことにした。それぞれに一長一短がある。
HOT PEPPER はメールアドレスのみでAPIキー取得でき、お試しにはもってこいだ。検索できる店舗数に上限がないのも扱いやすい。また、店舗のジャンルコードとして「ラーメン店」を指定できるため、今回の用途と相性もよい。ただし、ぐるなびと比較すると登録店舗数が圧倒的に少ない。
データ形式はXMLがデフォルトだが、JSONやJSONPも選択できる。
ぐるなびは、APIを取得するために氏名などを入力する必要があるため、お試しで使う身としてはちょっと敷居が高い。一方で、自身でサービスを立ち上げる際にはぐるなびの方が案内がしっかりしており信頼感が高い。データはJSON形式で出力される。
業態コードとして「ラーメン」が用意されているものの、それを指定して叩いても居酒屋のようなラーメン専門店以外もひっかかる。特定の業態を抽出するには工夫が必要となる。
一方でテストツールをウェブから利用でき、お試しで結果を得やすい。また登録店舗数が圧倒的に多い。たとえば、ラーメン店で検索するとHOT PEPPERでは2,600店しかヒットしないが、ぐるなびだとその10倍の26,000店もヒットする。統計を取りたいならぐるなびの方に分があるだろう。ただし、取得上限が1,000件までのため、必要な情報を網羅的に得るのにも工夫が必要となる。
PythonでAPIを叩いてみれば
- 【Python3】WebAPIを叩く【requests】 - Qiita
- https://note.nkmk.me/python-requests-usage/
Python の requests を用いて API を叩き、JSON形式で店舗情報を取得した。先にも述べたが、ラーメン店を絞り込むのにやや工夫が必要である。
HOT PEPPER はラーメン店のジャンルコードが G013 と指定されており、絞りこみが楽である。約2,600店がヒットした。取得上限はないが1ページ当たり100店舗までしか表示できないので、26回叩いて全店舗の情報をJSONとして出力した。
ぐるなびにもラーメン店の業態コードがあるものの、それだけではラーメン店以外もヒットする。お好み焼きやしゃぶしゃぶ店なども引っかかるため、ぐるなびの分類方法は難がありすぎに思う。対処は比較的簡単で、フリーワードに「ラーメン」を入れることでほぼラーメン店のみに絞れた。
ぐるなびでは約26,000件ものラーメン店がヒットした。取得上限が1,000件なので、それ以上の店舗情報を得るには叩き方に工夫が必要となる。今回は、都道府県毎に取得した。
東京には3,000店舗以上、都市圏では1,000店舗以上のラーメン店が登録されている。 1,000件以上ある都道府県では上限の1,000件を取得するように設定した。ぐるなびの掲載順に取得すると、ラーメン店以外は検索結果の後ろの方に表示される。上位1,000件を取得した方が、ラーメン店以外を除外しやすい。
APIの叩き方に少々手間取ったが、それ以上に四苦八苦したのがJSONデータの保存である。エンコードにUTF-8を指定しているのに、なぜか日本語がASCIIで描き込まれ uXXXXなどど保存される。それを回避するには、open() ではなく codecs.open() で開き、dumps() の引数に ensure_ascii=False と指定する必要があった。
# HOT PEPPER のラーメン店情報をJSON形式で取得する import requests import json import codecs # APIキーの指定 apikey = ”{個々人のAPIKEYを入力する}” # APIのURL api = 'http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={key}&order=3&genre={genre}&count={count}&start={start}&format=json' # APIにリクエストを送信して店舗数を取得する url = api.format(key=apikey, count=1, start=1, genre='G013') response = requests.get(url) response.raise_for_status() data = json.loads(response.text) count_range = data['results']["results_available"] # ラーメン店の検索結果をJSON形式で取得する ramen_json = [] # 100件ごとにAPIにリクエストを送信して店舗情報を取得する for i in range(0, count_range, 100): url = api.format(key=apikey, count=100, start=i, genre='G013') response = requests.get(url) response.raise_for_status() data = json.loads(response.text) data = data['results']['shop'] ramen_json += data # 取得したラーメン店のJSONデータを保存 with codecs.open('ramen_hot.json', 'w', 'utf-8') as obj: dump = json.dumps(ramen_json, indent=4, ensure_ascii=False) obj.write(dump)
# ぐるなびのラーメン店情報をJSON形式で取得する import requests import json import codecs # APIキーの指定 apikey = ”{個々人のAPIKEYを入力する}” # APIのURL api = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid={key}&category_s={categoly}&freeword={freeword}&offset={start}&hit_per_page={count}&pref={pref_code}' # 業態コードとフリーワードの指定 w_ramen = '%e3%83%a9%e3%83%bc%e3%83%a1%e3%83%b3' # ラーメン c_ramen = 'RSFST08008' # ラーメン # 都道府県毎に店舗情報を取得する for num in range(1, 48): num = str(num).zfill(2) pref_num = 'PREF' + num # APIにリクエストを送信して店舗数を取得する url = api.format(key=apikey, count=1, start=1, freeword=w_ramen, categoly=c_ramen, pref_code=pref_num) response = requests.get(url) response.raise_for_status() data = json.loads(response.text) count_range = data['total_hit_count'] # 上限が1000件なので、1000を超える場合は count_range を1000に書き換える if count_range > 1000: count_range = 1000 # ラーメン店の検索結果をJSON形式で取得する ramen_json = [] # 100件ごとにAPIにリクエストを送信して店舗情報を取得する for i in range(1, count_range, 100): url = api.format(key=apikey, count=100, start=i, freeword=w_ramen, categoly=c_ramen, pref_code=pref_num) # 実際にAPIにリクエストを送信して結果を取得する response = requests.get(url) response.raise_for_status() data = json.loads(response.text) data = data['rest'] ramen_json += data # 取得したラーメン店のJSONデータを保存 with codecs.open('ramen_guru' + pref_num + '.json', 'w', 'utf-8') as obj: dump = json.dumps(ramen_json, indent=4, ensure_ascii=False) obj.write(dump)
定休日を抽出する
HOT PEPPER、ぐるなびともに定休日がそれぞれ close や holiday としてキー登録されているので定休日の抽出は非常に簡単だ。
また、ラーメン専門店を抽出できているかを確認するため、店舗業態も抽出した。都道府県毎の傾向を見ようと思い都道府県も抽出したが、今回はそこまでやらなかった。
ぐるなびの定休日情報は細かく書かれており、たとえば「毎週月曜日、*元日は休み」など備考が含まれている。今回は定期的な休業日を知りたかったので備考を省くように処理した。
ちなみに、ぐるなびの店舗形態を確認すると「こだわりラーメン」に分類されているラーメン店があった。気になって調べてみると、北海道を拠点する山岡家が用いている業態名称である。チェーン店なのにこだわりラーメンと銘打つのは強気すぎないか。
結果を、1列目に都道府県、2列目に店舗業態、3列目に定休日となるCSVファイルとして保存した。
ぐるなびは都道府県毎にCSVファイルを出力した後に結合したファイルを作成した。
ちなみに、CSVファイルを結合する時にもエンコードの問題が発生。結合したファイルを出力する際に encoding='utf-8' と指定しているにも拘わらず、なぜかJIS形式で保存されるため、UTF-8で開こうとして文字化けした。結合するファイルを開く際に、engine='python' と指定することで文字化けを回避した。
# HOT PEPPERとぐるなびの店舗データから定休日を抽出しCSVとして出力 import os import glob import json import codecs import csv def holiday_pickup_hot(path): # HOT PEPPERのJSONデータを処理する関数 # 指定したpath内のJSONファイルを取得 # 地域、店舗ジャンル、定休日を抽出 # CSV形式で保存 # フォルダ中のパスを取得 All_Files = glob.glob('{}*.json'.format(path)) for file in All_Files: #JSON ファイルを開く with codecs.open(file, 'r', 'utf-8') as obj: df = json.load(obj) # 地域、店舗ジャンル、定休日を抽出する ramen_csv = [] for i in range(len(df)): holiday_list = [] area = df[i]['large_area']['name'] genre = df[i]['genre']['name'] holiday = df[i]['close'] holiday_list = [area, genre, holiday] ramen_csv.append(holiday_list) # 抽出した地域、店舗ジャンル、定休日のデータをCSV形式で保存 name = file.rstrip('.json') with open(name + '_holiday.csv', 'w') as obj: writer = csv.writer(obj, lineterminator='\n') writer.writerows(ramen_csv) def holiday_pickup_guru(path): # ぐるなびのJSONデータを処理する関数 # 指定したpath内のJSONファイルを取得 # 地域、店舗ジャンル、定休日を抽出 # CSV形式で保存 # フォルダ中のパスを取得 All_Files = glob.glob('{}*.json'.format(path)) for file in All_Files: #JSON ファイルを開く with codecs.open(file, 'r', 'utf-8') as obj: df = json.load(obj) # 地域、店舗ジャンル、定休日を抽出する ramen_csv = [] for i in range(len(df)): holiday_list = [] area = df[i]['category'] genre = df[i]['code']['prefname'] holiday = repr(df[i]['holiday']) #repr() holiday = holiday.split(r'\n') holiday = holiday[0].split("'") holiday_list = [area, genre, holiday[1]] ramen_csv.append(holiday_list) # 取得した地域、店舗ジャンル、定休日のデータをCSV形式で保存 name = file.rstrip('.json') with open(name + '_holiday.csv', 'w') as obj: writer = csv.writer(obj, lineterminator='\n') writer.writerows(ramen_csv) holiday_pickup_hot(path='./ramen_hot/') holiday_pickup_guru(path='./ramen_guru/')
# フォルダ内にあるCSVファイルを結合し出力 import os import glob import csv import pandas as pd # フォルダ中のパスを取得 DATA_PATH = "./ramen_guru/" All_Files = glob.glob('{}*.csv'.format(DATA_PATH)) # フォルダ中の全csvをマージ list = [] for file in All_Files: list.append(pd.read_csv(file, engine='python', header=None)) df = pd.concat(list, sort=False) # 結合しcsvを出力 df.to_csv('ramen_guruPREF01to47_holiday.csv', encoding='utf-8')
定休日を集計する
EXCELでもできそうだが、折角なのでPythonを使って集計した。CSVファイルの3列目を読み込み、定休日をキーとする辞書形式でカウントした。定休日と頻出数のセットとしたリストを作り、それをソートした後に、結果をCSVファイルとした出力した。
曜日は意図通りに集計されるが、「無」、「無休」などは別々に集計される。また、週に二日以上定休日がある場合も、異なるキーとなる。そのため、出力されたデータを手動で再集計した。集計方法には工夫の余地があるものの、結果としては大きな差異はないと思われる。
ぐるなびは定休日が記載されていない店舗が多かった。約20,000店舗の情報を取得したが、その内約16,000件は定休日の情報が「記載無し」であった。これは「null」や「nan」の意味での「記載無し」である。定休日がなく年中無休なのか、そもそも情報が記載されていないのか判断できない。「記載無し」の店舗は省き、残った4,000店舗から定休日を集計した。
# CSVファイル内の指定した列にある単語の出現数をカウントする import pandas as pd def counter(file_name, read_column): # CSVファイルの指定した列の単語の出現数をカウントする関数 # file_name でCSVファイルを指定 # read_column で列を指定 df = pd.read_csv(file_name, engine='python', header=None) data = df.iloc[:, read_column] # 単語を辞書のキーとしてカウントする words = {} for word in data: words[word] = words.get(word, 0) + 1 # 単語と出現数をタプルとしてリスト化しソートする dic = [(v,k) for k,v in words.items()] dic.sort() dic.reverse() # 結果をCSV形式で保存 # utf-8 を指定しない with open(file_name.strip('.csv') + '_count.csv', 'w') as obj: for count, word in dic: obj.write(str(count) + ',' + str(word) + '\n') counter(file_name='ramen_holiday_hot.csv', read_column=2) counter(file_name='ramen_guruPREF01to47_en.csv', read_column=2)
APIを叩いて集計してみた感想
ブログに内容をまとめるのに、コードを書いて結果を得るまでと同じくらいの労力がかかってしまった。
APIそのものは簡単に取り扱えるので、欲しい情報を絞り込むのに検索力が必要に感じた。データを取得でにさえすれば、集計自体も難しくない。エンコードには苦しめられたが……。
最終的に人力で集計した部分もあるが、今回はそこを自動化するのに労力かかりそうだったので、自動化するメリーはないだろう。より多くのデータを集計するならより自動化をすべきだろう。自然言語系の処理は面倒そうだ。
コードを他人に見せることを意識すると、コメントやプログラミングのロジックなどを他の人が読んでもわかりやすいようにしようと自然に思えたので、勉強に向いてるなと感じた。