BigQueryとGemini 1.5 Proによるラーメン店クチコミの定量分析

記事タイトルとURLをコピーする

G-gen の神谷です。本記事では、Google Maps API から取得したラーメン店のクチコミデータに対する定量分析手法をご紹介します。 従来の BigQuery による感情分析の有用性を踏まえつつ、Gemini 1.5 Pro の導入によって可能となった、より柔軟なデータの構造化や特定タスクの実行方法を解説します。

分析の背景と目的

近年、1 to 1 マーケティングや顧客分析の重要性が高まっています。しかし、すべての顧客から直接感想を聞くのは現実的ではありません。そこで注目したのが、Google Maps に自発的に投稿されたクチコミデータです。

クチコミ分析の主な目的は以下の通りです。

目的
顧客満足度の向上と製品改善 ラーメン店のクチコミで「麺が柔らかすぎる」という声が多い場合、茹で時間の調整を検討。
競合分析と市場ポジショニングの最適化 競合店との比較で「価格が高い」という指摘が多い場合、価格戦略の見直しや付加価値の訴求を検討。
効果的なマーケティング施策の立案と評価 「子連れでも利用しやすい」というクチコミが増加傾向にある場合、ファミリー向けキャンペーンの強化を検討。
トレンドの早期発見と迅速な対応 「ヴィーガンメニューが欲しい」という声が急増した場合、新メニューの開発を迅速に行う。
ビジネス指標との相関分析による戦略立案 クチコミの評価が高い店舗ほど売上が高いという相関が見られた場合、優良店舗の施策を他店舗に展開。

可視化イメージ

クチコミデータの感情分析やカテゴリ・キーワード分析は以前から有りましたが、生成 AI 技術の発展によって、より自由に非構造化データを構造化データに変えられ、既存の商品、営業、販売等のデータと掛け合わせて分析できる点がポイントです。

クチコミのネガポジや頻度を地図上にプロット

カテゴリごとのネガポジ、クチコミスコアとレイティングの時系列比較

カテゴリごとのネガポジと感情強度

分析の流れとアーキテクチャ

今回の検証の分析の流れとアーキテクチャは以下のとおりです。

  1. Google Maps API からクチコミデータを取得
  2. BigQuery に保存
  3. BigQuery のリモート関数で感情分析 & 生成 AI による多様な自然言語解析(※ Dataform でデータパイプラインを構築)
  4. BI ツール(Looker Studio 等)で可視化

分析の流れとシステムアーキテクチャ

クチコミデータ取得と BigQuery への保存

Google Maps API を使用してクチコミデータを取得し、BigQuery に保存する方法を説明します。

API キーの取得

Google Map API の中から「Places API」を使用しています。Google Cloud プロジェクトで API の有効化と API キーの取得が必要なため、手順は以下を参照してください。

データ取得のサンプルコード

以下は、Python を使用したサンプルコードです。(Colabで実行可能です)

1. ライブラリのインストール

!pip install googlemaps>=4.10.0 google-cloud-bigquery>=3.2.5

2. 認証処理

from google.colab import auth
import google.auth
  
auth.authenticate_user()
credentials, _ = google.auth.default()

3. Google Map API を使って、クチコミデータを取得し、BigQuery にアップロード

以下では、「東京都千代田区((35.68093847942352, 139.76703854373777))」から「半径 1,000 メートル」以内で「ラーメン」という検索キーワードにマッチする場所の情報や、それに紐づくクチコミ情報を取得しています。

import googlemaps
from google.cloud import bigquery
from datetime import datetime
import pandas as pd
  
class GoogleMapsReviewFetcher:
    def __init__(self, api_key, project_id, dataset_id, table_id):
        # Google Maps API クライアントの初期化
        self.gmaps_client = googlemaps.Client(key=api_key)
  
        # BigQuery クライアントの初期化
        self.bq_client = bigquery.Client(project=project_id)
  
        # BigQuery のデータセット ID とテーブル ID を保存
        self.dataset_id = dataset_id
        self.table_id = table_id
  
    def fetch_reviews(self, keyword, location, radius):
        reviews = []
  
        # Google Maps Places API を使用して、指定された条件で場所を検索
        places_result = self.gmaps_client.places_nearby(
            location=location,  # 検索の中心位置(緯度、経度)
            radius=radius,      # 検索範囲(メートル)
            keyword=keyword,    # 検索キーワード
            language='ja'       # 結果の言語を日本語に設定
        )
  
        # 検索結果の各場所について詳細情報を取得
        for place in places_result['results']:
            # 場所の詳細情報を取得
            place_details = self.gmaps_client.place(place_id=place['place_id'], language='ja')
            spot_name = place_details['result'].get('name')
            location_lat = place_details['result']['geometry']['location']['lat']
            location_lng = place_details['result']['geometry']['location']['lng']
  
            # 各場所のレビューを処理
            for review in place_details['result'].get('reviews', []):
                reviews.append({
                    'spot_name': spot_name,
                    'author': review.get('author_name'),
                    'rating': review.get('rating'),
                    'text': review.get('text'),
                    'time': datetime.fromtimestamp(review.get('time')),
                    'relative_time_description': review.get('relative_time_description'),
                    'location': f"POINT({location_lng} {location_lat})"  # 緯度経度を GEOGRAPHY 型の POINT 形式で保存
                })
  
        return reviews
  
    def save_to_bigquery(self, reviews):
        # レビューデータを Pandas DataFrame に変換
        df = pd.DataFrame(reviews)
  
        # BigQuery のテーブル参照を作成
        table_ref = self.bq_client.dataset(self.dataset_id).table(self.table_id)
  
        # BigQuery へのデータ書き込み設定
        job_config = bigquery.LoadJobConfig(
            schema=[
                bigquery.SchemaField("spot_name", "STRING"),
                bigquery.SchemaField("author", "STRING"),
                bigquery.SchemaField("rating", "FLOAT"),
                bigquery.SchemaField("text", "STRING"),
                bigquery.SchemaField("time", "TIMESTAMP"),
                bigquery.SchemaField("relative_time_description", "STRING"),
                bigquery.SchemaField("location", "GEOGRAPHY")
            ],
            write_disposition="WRITE_TRUNCATE"  # テーブルを上書き
        )
  
        # DataFrame を BigQuery にアップロード
        job = self.bq_client.load_table_from_dataframe(df, table_ref, job_config=job_config)
        job.result()  # ジョブの完了を待つ
  
        print(f"Loaded {len(reviews)} reviews into {self.dataset_id}.{self.table_id}")
  
if __name__ == "__main__":
    # 設定情報
    API_KEY = '上記で取得した API キー'  # Google Maps API キー
    PROJECT_ID = 'project_id'    # Google Cloud プロジェクト ID
    DATASET_ID = 'google_maps_review_analysis'  # BigQuery のデータセット ID
    TABLE_ID = 'reviews'  # BigQuery のテーブル ID
  
    # 検索パラメータ
    KEYWORD = "ラーメン"  # 検索キーワード
    LOCATION = (35.68093847942352, 139.76703854373777)  # 検索の中心位置(緯度、経度)
    RADIUS = 1000  # 検索範囲(メートル)
  
    # GoogleMapsReviewFetcher のインスタンスを作成
    fetcher = GoogleMapsReviewFetcher(API_KEY, PROJECT_ID, DATASET_ID, TABLE_ID)
  
    # レビューを取得
    reviews = fetcher.fetch_reviews(KEYWORD, LOCATION, RADIUS)
  
    # 取得したレビューを BigQuery に保存
    fetcher.save_to_bigquery(reviews)

BigQuery への格納結果は以下のとおりです。

クチコミデータの格納結果(BigQuery)

クチコミ数の制限と緩和策

「Places API」は一つの場所で最大5件しかクチコミを取得できない制約があります。その場所のオーナーであれば、「Business Profile API」に登録することで、すべてのクチコミを取得できるとされていますが、当社では未検証です。

詳細は以下を参照してください。

料金

API リクエストには料金が発生するため注意が必要です。筆者のサンプルコードでは、1件のクチコミで「0.87円」となりました。詳細な料金体系については以下を参照してください。

感情分析とデータパイプライン

Dataform を使用して BigQuery 上にデータパイプラインを構築し、感情分析を行います。

Dataform の利点

Dataform は、BigQuery を使用したデータ分析や機械学習のワークフローを効率化するツールです。以下の点で特に有用です。

  • BigQuery の多様な分析機能をフル活用
    • BigQuery ML:従来の機械学習アルゴリズムを使用可能
    • リモート関数:外部サービスとの連携が容易
    • オブジェクトテーブル:構造化されていないデータの処理が可能
    • 生成 AI:最新の AI 技術を BigQuery 内で直接利用可能
  • 複雑な処理の簡素化
    • 長いデータパイプラインや複雑な処理フローを効率的に管理
    • SQL ベースの操作で、複雑な処理を直感的に記述可能
  • コスト削減
    • 実装コストの低減:複雑な処理を簡単に構築可能
    • 運用保守コストの削減:一元管理による保守性の向上
  • 開発から本番環境への円滑な移行
    • PoC(概念実証)段階で作成したクエリを、本番環境のパイプラインに容易に統合可能

これらの利点により、Dataform は特に複雑なデータ分析プロジェクトや中〜大規模なデータパイプラインの構築に適しています。データ分析の効率と品質を向上させたい場合に、強力なツールとなります。

詳細は弊社ブログ記事をご参照ください。

blog.g-gen.co.jp

Dataform を使った感情分析のパイプライン定義例

以下は、Dataform を使った感情分析のパイプライン定義例です。

-- Dataform 設定:結果を保存するテーブルの定義
config {
  type: "table",
  schema: "google_maps_review_analysis",
  name: "reviews_sentiment",
  description: "クチコミの感情分析結果"
}
  
-- 感情分析を実行し、結果を整形
WITH analyzed_data AS (
  SELECT
    r.*,  -- 元のクチコミデータのすべての列を選択
    t.ml_understand_text_result,  -- 感情分析の生の結果を含む
    JSON_EXTRACT_ARRAY(t.ml_understand_text_result, '$.sentences') AS sentences,  -- 感情分析結果の sentences 配列を抽出
    -- JSON 形式の結果から感情スコアを抽出(-1から1の範囲、負の値はネガティブ、正の値はポジティブ)
    CAST(JSON_EXTRACT_SCALAR(t.ml_understand_text_result, '$.document_sentiment.score') AS FLOAT64) AS document_sentiment_score,
    -- JSON 形式の結果から感情の強さを抽出(0以上の値、大きいほど感情が強い)
    CAST(JSON_EXTRACT_SCALAR(t.ml_understand_text_result, '$.document_sentiment.magnitude') AS FLOAT64) AS document_sentiment_magnitude
  FROM
    ${ref("reviews")} AS r  -- reviews テーブルを参照
  JOIN (
    -- BigQuery の自然言語処理モデルを使用して感情分析を実行
    SELECT
      text_content,  -- テキストデータ
      ml_understand_text_result  -- 感情分析の生の結果
    FROM
      ML.UNDERSTAND_TEXT(
        MODEL `project_id.google_maps_review_analysis.nlp`,  -- 使用する自然言語処理モデル
        (SELECT text AS text_content FROM ${ref("reviews")}),  -- 分析対象のテキスト
        STRUCT('ANALYZE_SENTIMENT' AS nlu_option)  -- 感情分析オプションを指定
      )
  ) AS t
  ON
    r.text = t.text_content  -- テキストを基準に JOIN
),
  
-- 最終的な結果セットを選択し、中間テーブルに保存
intermediate_results AS (
  SELECT
    r.spot_name,  -- スポット名
    r.author,  -- 著者
    r.rating,  -- 評価
    r.text,  -- クチコミテキスト
    r.time,  -- クチコミ投稿時間
    r.relative_time_description,  -- クチコミ投稿の相対時間
    r.location,  -- クチコミの位置情報
    r.text AS `original_input`,  -- 元の入力テキスト
    r.document_sentiment_score,  -- ドキュメント全体の感情スコア
    r.document_sentiment_magnitude,  -- ドキュメント全体の感情の強さ
    -- 各文のテキスト内容
    JSON_EXTRACT_SCALAR(sentence, '$.text.content') AS sentence_text,
    -- 各文の感情スコア
    CAST(JSON_EXTRACT_SCALAR(sentence, '$.sentiment.score') AS FLOAT64) AS sentence_sentiment_score,
    -- 各文の感情の強さ
    CAST(JSON_EXTRACT_SCALAR(sentence, '$.sentiment.magnitude') AS FLOAT64) AS sentence_sentiment_magnitude
  FROM
    analyzed_data AS r,
    UNNEST(r.sentences) AS sentence  -- sentences 配列を展開
)
  
-- 中間テーブルから最終的な結果セットを選択
SELECT * FROM intermediate_results

Dataform ジョブの出力結果は以下のとおりです。

Dataform ジョブの出力結果

感情分析の結果解釈

このクエリでは、BigQuery の機械学習モデル ML.UNDERSTAND_TEXT を使用して感情分析を行っています。結果は以下の2つの値として返されます。

  • sentiment_score:-1 から 1 のスコアで、感情の肯定的/否定的な度合いを表します。
  • sentiment_magnitude:感情の強さを表します。

これらの値を組み合わせることで、クチコミの感情的な特徴を分析することができます。

ML.GENERATE_TEXT(Gemini 1.5 Pro) 関数を使用した高度な分析

BigQuery の ML.GENERATE_TEXT 関数は、テキスト生成や高度なマルチモーダルタスクを実行するための強力なツールです。詳細なセットアップ方法については以下をご参照ください。

以下の記事では BigQuery ML の基本を解説していますので、ご参照ください。

blog.g-gen.co.jp

ユースケースに応じた独自の評価観点によるクチコミの定量化

以下は、クチコミから「味」「提供スピード」「価格」「店の雰囲気」「その他」を三段階評価で抽出するクエリです。

config {
  type: "table",
  schema: "google_maps_review_analysis",
  name: "reviews_generate_text_analysis"
}
  
-- クチコミデータを準備し、AI モデルに渡すためのプロンプトを作成する
WITH review_data AS (
  SELECT
    spot_name, -- 店名
    author, -- クチコミの投稿者
    text, -- クチコミの内容
    rating, -- 店の評価
    CONCAT(
      '以下のラーメン店「', spot_name, '」のクチコミに基づいて、次の情報を JSON 形式で抽出してください:',
      '味(良い、普通、悪い、不明)、提供スピード(速い、普通、遅い、不明)、価格(高い、普通、安い、不明)、店の雰囲気(良い、普通、悪い、不明)、その他(良い、普通、悪い、不明)。',
      'それぞれの観点が総合評価(', rating, ')にどのように関連しているかも考慮してください。',
      'クチコミ: "', text, '"',
      'フォーマットは{"味": "良い", "提供スピード": "速い", "価格": "高い", "店の雰囲気": "不明", "その他": "良い"}。'
    ) AS prompt -- AIモデルに渡すためのプロンプト
  FROM ${ref("reviews")}
)
  
-- AI モデルを使用して、プロンプトに基づいて情報を抽出する
SELECT
  r.spot_name, -- 店名
  r.author, -- クチコミの投稿者
  r.text, -- クチコミの内容
  r.rating, -- 店の評価
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.味') AS flavor_text, -- 味に関する情報
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.提供スピード') AS service_speed_text, -- 提供スピードに関する情報
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.価格') AS price_text, -- 価格に関する情報
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.店の雰囲気') AS atmosphere_text, -- 店の雰囲気に関する情報
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.その他') AS other_text, -- その他に関する情報
  -- 各観点に対するスコアを計算する
  CASE JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.味')
    WHEN '良い' THEN 3
    WHEN '普通' THEN 2
    WHEN '悪い' THEN 1
    ELSE NULL
  END AS flavor_score, -- 味のスコア
  CASE JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.提供スピード')
    WHEN '速い' THEN 3
    WHEN '普通' THEN 2
    WHEN '遅い' THEN 1
    ELSE NULL
  END AS service_speed_score, -- 提供スピードのスコア
  CASE JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.価格')
    WHEN '高い' THEN 1
    WHEN '普通' THEN 2
    WHEN '安い' THEN 3
    ELSE NULL
  END AS price_score, -- 価格のスコア
  CASE JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.店の雰囲気')
    WHEN '良い' THEN 3
    WHEN '普通' THEN 2
    WHEN '悪い' THEN 1
    ELSE NULL
  END AS atmosphere_score, -- 店の雰囲気のスコア
  CASE JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.その他')
    WHEN '良い' THEN 3
    WHEN '普通' THEN 2
    WHEN '悪い' THEN 1
    ELSE NULL
  END AS other_score -- その他のスコア
FROM review_data r
LEFT JOIN ML.GENERATE_TEXT(
  MODEL `project_id.google_maps_review_analysis.gemini_1_5_pro`, -- 使用するAIモデル
  (SELECT prompt FROM review_data), -- プロンプトをAIモデルに渡す
  STRUCT(
    0.1 AS temperature, -- 出力のランダム性を制御する
    1000 AS max_output_tokens, -- 出力の最大トークン数
    0.1 AS top_p, -- ニュークリアスサンプリングのパラメータ
    10 AS top_k, -- トップKフィルタリングで保持する最も高い確率の語彙トークン数
    TRUE AS flatten_json_output -- JSON出力をフラットにする
  )
) AS t
ON r.prompt = t.prompt -- プロンプトを基にAIモデルからの生成結果を結合

生成結果は以下のとおりです。

独自の観点でクチコミを評価

クチコミに対する自動返信文作成

クチコミに対する店長からの返信文を自動生成するようにします。

config {
  type: "table",
  schema: "google_maps_review_analysis",
  name: "reviews_manager_responses"
}
  
-- Step 1: クチコミデータを準備し、AI モデルに渡すためのプロンプトを作成する
WITH review_data AS (
  SELECT
    spot_name, -- 店名
    author, -- クチコミの投稿者
    text, -- クチコミの内容
    rating, -- 店の評価
    time, -- クチコミの投稿時間
    relative_time_description, -- クチコミの相対的な時間
    CONCAT(
      '以下のラーメン店「', spot_name, '」のクチコミに対して、店長からの返信を作成してください。クチコミの内容に応じて感謝や改善点への対策などを含めてください。',
      'また、Google Map における店の評価(', rating, ')を踏まえて、お客様の期待値に応えているかどうかも考慮してください。',
      'クチコミ: "', text, '"',
      '返信は、以下のフォーマットでお願いします。',
      '{"response": "', author, '', '\\n', '店長からの返信メッセージ"}'
    ) AS prompt -- AIモデルに渡すためのプロンプト
  FROM ${ref("reviews")}
)
  
-- Step 2: AI モデルを使用して、プロンプトに基づいて店長からの返信を生成する
SELECT
  r.spot_name, -- 店名
  r.author, -- クチコミの投稿者
  r.text, -- クチコミの内容
  r.rating, -- 店の評価
  r.time, -- クチコミの投稿時間
  r.relative_time_description, -- クチコミの相対的な時間
  JSON_VALUE(REPLACE(t.ml_generate_text_llm_result, '```json', ''), '$.response') AS response -- 生成された返信
FROM review_data r
LEFT JOIN ML.GENERATE_TEXT(
  MODEL `project_id.google_maps_review_analysis.gemini_1_5_pro`, -- 使用するAIモデル
  (SELECT prompt FROM review_data), -- プロンプトをAIモデルに渡す
  STRUCT(
    0.3 AS temperature,              -- 出力のランダム性を制御する
    500 AS max_output_tokens,        -- 出力の最大トークン数
    0.9 AS top_p,                    -- ニュークリアスサンプリングのパラメータ
    10 AS top_k,                     -- トップKフィルタリングで保持する最も高い確率の語彙トークン数
    TRUE AS flatten_json_output      -- JSON出力をフラットにする
  )
) AS t
ON r.prompt = t.prompt -- プロンプトを基にAIモデルからの返信を結合

生成結果は以下のとおりです。クチコミの内容に対して、一つずつ丁寧に返信しています。

クチコミに対する自動返信文

神谷 乗治 (記事一覧)

クラウドソリューション部

クラウドエンジニア。2017 年頃から Google Cloud を用いたデータ基盤構築や ML エンジニアリング、データ分析に従事。クラウドエース株式会社システム開発部マネージャーを経て現職。Google Cloud Partner Top Engineer 2023,2024、Google Cloud Champion Innovators(Database)、著書:「GCPの教科書III【Cloud AIプロダクト編】」