top of page

[前編] 新入社員の歓迎ランチが不安すぎて、Google Maps APIで最強の店リストを作って武装した話

  • 執筆者の写真: Ryota Isono
    Ryota Isono
  • 1月21日
  • 読了時間: 8分

更新日:1月22日


正直に言おう。私はいま、2月が来るのがとても不安だ。 リベルスカイには、中途入社のメンバー初日はチーム全員で歓迎ランチに行くという「慣わし」があるからだ。もちろん仲間が増えること自体はとてもうれしいことなのだが、問題は「ランチ」だ。


今回入社してくれるのは、20代の若手女性社員。 対する私は、グルメにとんと疎く、行列に並ぶのが大嫌いな「データおじさん」である。 普段のランチといえば、デパ地下の惣菜か、コンビニ飯で済ませてしまうタイプだ。


「〇〇さん、今日の歓迎ランチはどこへ行きますか?」


チームのメンバーから普段は絶対に聞かれることのない質問が飛んできた瞬間、私の頭の中は真っ白になるだろう。

私の手札にあるのは近場の「ファミマ」とオフィスビルと繋がっている「高島屋のデパ地下惣菜」だけ。 キラキラした20代の転職初日・初ランチに、イートインスペースを提案して「社会的死」を迎えるわけにはいかない。


「このままではマズい……」


絶望の中で、私は一つの事実に気づく。 私には「グルメや店の知識」はない。だが、「データを集める技術」はある。Googleの集合知を根こそぎデータ収集することで、情報は手に入れられるではないか!

…こうして、ランチへの不安に突き動かされた私のプロジェクト『Project Lunch』は静かに(勝手に)幕を開けたのだった。



第1章:Google Places API からの店舗情報取得


「よく使うGoogle Mapからならば、店名や評価、位置情報など必要な情報がすぐに揃うのでは?」そう悟った私は、迷わずGCPコンソールで "Places API"を有効化する。


"Nearby Search" を利用することで、指定した条件に合致する場所のリストが取得可能だ。そう。Google Mapを開いて「ランチ」と検索したら、店舗情報が出てくるあの動きだ。


注:Google Maps APIの利用には、APIキーの発行が必要です。APIキーの発行には Google Cloud Platform(GCP)の利用がマストです。詳細はこちらから。


ちなみに今回利用するAPIは、以下の " Places API "(青色枠線の方) ※.Pythonのクライアントライブラリ(googlemaps)を使って手軽に実装したかったため、今回はLegacy(無印)版のAPIを使用しています。後述しますが、Newの方が何かと便利。




準備は整った。まずは情報(データ)の調達だ。私は この Google Places API をPythonで叩くことにした。 「オフィスから半径500m以内の店情報を私に恵んでください」とGoogle先生に祈りながら問い合わせる。 Google 先生はいつものように優しく教えてくれると信じていたのだが、意外とクセがあり、手間取った。細かい部分は省略するが、以下のような事象にぶち当たった。 今後、同じ境遇に立たされてしまった同志が困らないよう、幾つか紹介しておく。...というかちゃんとAPIリファレンスを読みましょう。


  1. 距離の罠(Radius vs Distance)

    半径(Radius)「500m以内」と指定したのに、なぜか1km先の水天宮の店がリストにある。Google先生は「ちょっと遠いけど人気店だよ?」と指定した範囲外の人気の高い店から紹介してくるなんともなお節介を焼いてくるのだ。オカンみたいなやつだ。 今回、近場であることはマストなので、「距離順(rank_by='distance')」を指定するが、今度は「距離順がいいなら、半径を指定するな」とエラーで怒られてしまった。 今回は、距離順を優先し、データを取得したあとで、口コミ評価などで人気店に絞れば良いと考えた。 ※. 今回は選択しなかった "Places API (New)"を利用すれば、双方の指定が可能です。


  2. 言語の壁

    そうこうして取得したCSVを見て違和感を感じた。「Ramen Shop Jiro……誰なんだお前は。」 language='ja' を忘れると、日本橋の店も英語表記になる。危うく「ラーメンショップ・ジローに行こう」と謎の発言をするところだった。出社初日に「欧米か」と突っ込ませてしまうところだった、危ない危ない。

    パラメータに language='ja' を追加し、店名は無事に「ラーメン二郎」になった。

    ※日本橋オフィス近くにラーメン二郎はありません。フィクションです。ちなみにリベルスカイはラーメン好きが多い。


  3. 60件の壁

    半径を絞ったり、いくつか条件を変えて実行していて気付いた。どの条件でもデータが60件しか取れないのだ。「この近辺で毎回ジャスト60件なんてことはないだろう。」と思って調べてみると、どうもこれがAPI仕様らしい。 まあいい。優柔不断な私には、選択肢は少ない方が助かる。



第2章:Google Translation API で日本語化

よしよし、これでオフィスから半径500mに帳(とばり)を降ろすことができたぞ...(?) と、改めて取得したcsvを見てみた。

name: ラーメン二郎, cuisine_type: ramen_restaurant

「そこは翻訳してくれないんかい!」


どうやら language='ja' が効くのは「店名」や「住所」などの表示テキストだけで、システム的なカテゴリを示す types 配列は、言語設定に関係なく**英語のキー**で返ってくる仕様らしい。


このままでもいいのだが、ジャンルで検索したい要望もあろうと考えると、日本語に翻訳しておきたい、いや、せねばならない。

ということで、さきほどのPlaces APIと同じ要領で Google Translation APIを有効化し、さきほどのPythonに「cuisine_type」の日本語化処理を加えた。

注:APIキーのスコープにTranslation APIを加えるのをお忘れなく。


APIの不足は別のAPI(≒金)で解決する、これが「データおじさん」のポリシー(マネーパワー)だ。

※.今回の利用量では課金は発生しません。カッコつけたかっただけ。すみません。



第3章:BigQueryへの投入


あとはこのデータをBigQueryに投入するのみ!今回はスポット的な投入であるため、BigQueryのコンソールからダイレクトに投入。



今回取得したデータのスキーマ情報はこちら。


Discription(説明)欄には論理名称をいれておくのが、データおじさんとしての嗜み。

これらの属性情報があれば、どんなリクエストにも答えられるはず。


投入した結果がこちら。

注:データの中身はサンプルデータに置き換えています。




ようやくデータがBigQueryに格納された。 これで必要なものは揃った。


しかし、ここで冷静になって考えてみてほしい。

初出社の緊張で震える20代の若手社員に突如、「この画面(BigQueryコンソール)を見てくれ。そして、好きな店を SELECT してくれ」 などと言い放ったらどうなるか?


きっと彼女は愛想笑いで「すごいですね…」と言ってくれるだろう。だがその心の底では、**「関わってはいけない人種」**というタグ付けがなされるはずだ。 初日からそんなことがあってはいけない。


そして 私が目指すのは、 裏側の苦労(SQL)は一切見せず、ただ洗練されたUIだけを差し出す。それこそがこのリベルスカイを選んでくれた若手社員に対する最高のホスピタリティではないのか。


こうして戦いの舞台は、BI(可視化)ツールへ。 次回、後編「Looker Studio編」へ続く。



[参考] Google Places APIからランチ情報を引き出すためのPythonコード

import os
import time
import requests
import pandas as pd
import googlemaps
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む(セキュリティ対策)
load_dotenv()

# ==========================================
# 設定・定数
# ==========================================
# Google Cloud Consoleで取得したAPIキー
API_KEY = os.getenv('GOOGLE_MAPS_API_KEY')
OUTPUT_FILE = 'lunch_data.csv'

# 検索の中心地点(今回はオフィスの座標)
CENTER_LOC = {'lat': 35.680347, 'lng': 139.774393} # 日本橋周辺

# 検索キーワード
KEYWORD = 'ランチ'

# 除外したい汎用的なジャンルタグ(これらが先頭に来ないようにする)
GENERIC_TYPES = {
    'food', 'point_of_interest', 
    'establishment', 'store', 'cafe'
}

# 翻訳APIのエンドポイント
TRANSLATE_URL = "https://translation.googleapis.com/language/translate/v2"

# ==========================================
# 関数定義
# ==========================================
def translate_text(text, target='ja'):
    """
    Google Cloud Translation APIを使ってテキストを翻訳する
    """

    if not text or text == 'unknown':
        return text
    params = {
        'q': text,
        'target': target,
        'key': API_KEY
    }

    try:
        response = requests.post(TRANSLATE_URL, data=params)
        result = response.json()

        if 'data' in result:
            return result['data']['translations'][0]['translatedText']
        else:
            print(f"[Warn] 翻訳失敗: {result}")
            return text

    except Exception as e:
        print(f"[Error] 翻訳通信エラー: {e}")
        return text

def fetch_lunch_data():
    """
    Google Maps Places API (Legacy) を使用して周辺のランチ情報を取得する
    """

    if not API_KEY:
        raise ValueError("APIキーが設定されていません。.envファイルを確認してください。")

    gmaps = googlemaps.Client(key=API_KEY)
    places_result = []

    print(f"--- 検索開始: 中心{CENTER_LOC} ---")

    # 初回リクエスト
    # 【注意】rank_by='distance' を指定する場合、radius(半径)は指定できません。
    # 指定すると INVALID_REQUEST エラーになります。
    results = gmaps.places_nearby(
        location=CENTER_LOC,
        rank_by='distance', # 近い順に取得
        keyword=KEYWORD,
        type='restaurant',
        language='ja'
    )

    places_result.extend(results.get('results', []))

    # ページネーション処理(最大60件まで取得可能)
    while 'next_page_token' in results:
        # 【重要】次のページトークンが有効になるまで数秒ラグがあるため待機
        time.sleep(2) 

        token = results['next_page_token']
        try:
            results = gmaps.places_nearby(page_token=token)
            places_result.extend(results.get('results', []))
            print(f"追加取得中... 現在 {len(places_result)} 件")
        except Exception as e:
            print(f"[Error] ページネーションエラー: {e}")
            break

    print(f"--- 検索終了: 合計 {len(places_result)} 件 ---")
    return places_result

def process_to_csv(places_data):
    """
    取得した生データを整形し、CSVに保存する
    """
    formatted_data = []

    print("データ整形と翻訳処理を開始します...")

    for place in places_data:
        # 1. ジャンル抽出ロジック
        raw_types = place.get('types', [])
        # 汎用的なタグを除外し、より具体的なジャンル(例: ramen, italian)を探す
        specific_types = [t for t in raw_types if t not in GENERIC_TYPES]
      
        # 具体的なタグがあればそれを採用、なければ先頭のタグを使う
        raw_genre = specific_types[0] if specific_types else (raw_types[0] if raw_types else 'unknown')

        # 2. 翻訳処理(英語のジャンル名を日本語へ)
        # アンダースコアをスペースにしてから翻訳APIに投げる
        english_genre = raw_genre.replace('_', ' ')
        japanese_genre = translate_text(english_genre)

        # 3. 必要なカラムだけ抽出
        item = {
            'id': place.get('place_id'),
            'name': place.get('name'),
            'latitude': place['geometry']['location']['lat'],
            'longitude': place['geometry']['location']['lng'],
            'rating': place.get('rating', 0.0),
            'user_ratings_total': place.get('user_ratings_total', 0),
            'price_level': place.get('price_level', None),
            'vicinity': place.get('vicinity'),
            'cuisine_type': japanese_genre
        }
        formatted_data.append(item)

    # DataFrame化して保存
    df = pd.DataFrame(formatted_data)
    df.to_csv(OUTPUT_FILE, index=False, encoding='utf-8')
    print(f"CSV保存完了: {OUTPUT_FILE}")

# ==========================================
# メイン処理
# ==========================================
if __name__ == '__main__':
    try:
        data = fetch_lunch_data()
        if data:
            process_to_csv(data)
        else:
            print("データが見つかりませんでした。")
    except Exception as e:
        print(f"予期せぬエラーが発生しました: {e}")
    
















コメント


Image by George Kedenburg III

NEWS

bottom of page