こんにちは。那須野です。
今日はデータ分析の話です。Google製のDWHであるBigQueryにデータを貯めていったときに「どう活用を広げるか」は課題として挙がりがちですが、その一つの解決策として最近流行りの生成AIが出てくるだろうなと思ったので、実際にやってみた内容を備忘録としてまとめておきます。
なお、本記事は2025年6月2日時点での内容です。投稿時にはClaudeという生成AIがMCPクライアントに対応していてBigQueryデータ分析の先行事例が多数投稿されるようになっていますが、ChatGPTでも頑張ればできなくもないので、今日はその工夫について書いてみようと思います。
そもそもどんなことをやろうとしているのかを図解すると、こんなイメージですね。ChatGPTの通常のチャット画面からやりたい分析を書くと、BigQueryにアクセスして分析して、結果を返してくれます。
BigQueryにアクセスするためのハブとしてCloud Runを使っているのがポイントで、あとは最低限のセキュリティを担保するために、アプリケーションとAPIの制限をかけたAPIキーを認証に使っています。
ちなみにCloud Runは、無料枠が以下のように用意されているので、BigQueryと同様にちょっとした利用であれば無料で使えるところがポイントです。
最初の 180,000 vCPU 秒/月
最初の 360,000 GiB 秒/月
200 万リクエスト/月
※最新情報はこちらを確認しましょう。
それでは実際の環境構築を見ていきましょう。
まず前提として以下の条件が揃っていることが重要です。
それでは具体的な手順です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import json import os import traceback from google.cloud import bigquery import functions_framework API_KEY = os.getenv("EXPECTED_API_KEY") MAX_JSON_CHARACTERS = 7000 @functions_framework.http def run_bq(request): if request.headers.get("x-api-key") != API_KEY: return ("Unauthorized", 401) try: body = request.get_json(silent=True) or {} sql = body.get("query") if not sql: return (json.dumps({"error": "'query' is required"}), 400, {"Content-Type": "application/json"}) # クエリ実行とDataFrame変換 client = bigquery.Client() df = client.query(sql).result().to_dataframe() df_json = df.to_json(orient="records", force_ascii=False) df_json_len = len(df_json) if df_json_len > MAX_JSON_CHARACTERS: payload = { "error": "response_records_exceeds_size_limit", "message": "responseのレコードサイズがChatGPTの受け取り上限を超えました。列を減らす、行を絞り込む、またはLIMITを設定してください。", "stats": { "rows": int(df.shape[0]), "cols": int(df.shape[1]), "characters": df_json_len, "max_characters": MAX_JSON_CHARACTERS } } return (json.dumps(payload, ensure_ascii=False), 413, {"Content-Type": "application/json"}) return (df_json, 200, {"Content-Type": "application/json"}) except Exception as e: # Cloud Loggingで確認できるようトレースバックを出力 print("Error during BigQuery execution:", str(e)) traceback.print_exc() return (json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}) |
|
1 2 3 |
google-cloud-bigquery>=3.20.0 pandas>=2.2.0 db-dtypes>=1.0.0 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
{ "openapi": "3.1.0", "info": {"title": "BigQuery Runner", "version": "1.0.0"}, "servers": [ { "url": "https://chatgpt-bq-connector-{hash}.asia-northeast1.run.app" } ], "paths": { "/run": { "post": { "operationId": "run_bq", "x-openai-isConsequential": false, "summary": "Run SQL on BigQuery", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Standard SQL" } }, "required": ["query"] } } } }, "responses": { "200": { "description": "Rows as JSON", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object" } } } } }, "400": { "description": "Bad request (e.g., missing 'query')", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } }, "401": { "description": "Unauthorized (invalid or missing API key)" }, "413": { "description": "Payload too large (response exceeds MAX_JSON_CHARACTERS)", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" }, "message": { "type": "string" }, "stats": { "type": "object", "properties": { "rows": { "type": "integer" }, "cols": { "type": "integer" }, "characters": { "type": "integer" }, "max_characters": { "type": "integer" } } } }, "required": ["error", "message", "stats"] } } } }, "500": { "description": "Internal server error during BigQuery execution", "content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } } } } } } } |
簡単ですね!
トグルを押せば実行したSQLも確認できるのもよい感じです!
追伸:グラフが文字化けしているのは、日本語が文字化けするPythonチャートライブラリの欠点です。このあたりもカスタムプロンプトで制御しないといけませんね。。
チャットで分析できるとはいっても、実務として上手く機能させるようにするにはGPTの指示(カスタムプロンプト)を中心に細かい部分での最適化が重要になってきます。ここでは特に気を付けた方が良いことを洗っておきましょう。
デフォルトのプロジェクトやデフォルトのデータセットを定義してあげると、都度のチャットで細かく指定しなくても分かってくれることが多いです。利用方法に合わせて検討しましょう。
GPTが分析する際に、データセットやテーブルの名前、カラムの名前をミスしてエラーを起こすと非常に面倒です。事前にしっかりとスキーマを調べてから分析SQLを実行するよう指示することで回避しやすいです。
なお、保守性は低下しますが、スキーマやサンプルデータをGPTのカスタム指示に記載することで、わざわざBigQueryに都度アクセスしなくて済むようになり、レスポンスが1テンポ早くなるので便利です。
特に指示しないとアウトプットは文章や表で返ってきます。グラフが望ましい場合はグラフを指定しましょう。なお、漏れなく日本語が文字化けするので英訳させるなどしてちゃんと制御させましょう。
良くも悪くもサービスアカウントのアクセスできるデータセットやテーブルが分析対象になります。余計なデータセットには権限を付与しないことでセキュリティリスクを軽減するだけでなく、見るべきデータだけが見えるようにすることで分析の正確性を高めることができます。
なお、BIツールなどに向けて加工した似たようなフィールドが乱立していると分析のパフォーマンスが落ちるため、必要なフィールドのみに絞ることも重要です。元テーブルのフィールドを絞るか、カスタム指示のスキーマ定義で絞るかなどの対策も考えましょう。
スキーマをカスタム指示に書かない場合、ChatGPTがデータを理解するのに使えるのはデータセットやテーブル、カラム名に次いでdescriptionが有効です。徹底して書いておくことで、ChatGPTが適切にデータを把握して期待通りの分析をしてくれやすくなるでしょう。
これらを踏まえたGPTの指示は、例えば以下のような形です。ご参考までにどうぞ。
|
1 2 3 4 5 6 7 8 |
BigQuery環境にアクセスしてデータ取得ができます。 default-project: {project_id} default-dataset: {dataset_name} BigQueryのデータセットやテーブルのスキーマを調べ、ユーザーの指示に適切な1つ以上のテーブルを特定し、SQLを作成して実行し、適度に可視化したうえで分析コメントと共にユーザーに返してください。 なお、Pythonでグラフを作成する場合、2バイトコードが文字化けしてしまうので、日本語は英語に訳してからグラフ化するようにしてください。 |
