BigQuery ハンズオン教科書|2026-08 版
第 15 章 サンドボックスで完走可

高度な分析 — 地理空間・近似集計・ノートブック

GEOGRAPHY 型で地図を描き、HLL で兆行を数え、Python から BigQuery を操る

この章で学ぶこと

前提: 全ハンズオンをサンドボックスで完走可・無料枠内(合計処理量の目安は数十 MB 程度。各ハンズオンで dry run を確認)。

地理空間分析の基礎 — GEOGRAPHY 型と ST_ 関数

BigQuery の GEOGRAPHY 型は、地球表面上の点集合(点・線・多角形、およびそれらの集合)を表します。座標系は WGS84 の準拠楕円体で、辺は平面の直線ではなく球面測地線(geodesic edge)として扱われます。GEOGRAPHY 列を持つテーブルを作ると、各行が 1 つの「空間フィーチャ」(ジオメトリ + 属性)になり、テーブル全体が空間フィーチャの集合になります。

点を作る基本関数は ST_GEOGPOINT(longitude, latitude) です。経度が先、緯度が後という引数順序に注意してください。多くの地図サービスは緯度を先に表示する慣習があるため、取り違えやすいポイントです。

関数用途戻り値
ST_GEOGPOINT(lng, lat)経度・緯度から点を作るGEOGRAPHY
ST_DISTANCE(g1, g2)2 つの GEOGRAPHY 間の最短距離FLOAT64(メートル)
ST_DWITHIN(g1, g2, d)距離 d メートル以内にあるか判定BOOL
ST_INTERSECTS(g1, g2)少なくとも 1 点が重なるか判定BOOL
ST_CONTAINS(g1, g2)g1 が g2 を内包するか判定(境界線上は含まない)BOOL
ST_AREA(g)多角形の面積FLOAT64(平方メートル)

WKT(Well-Known Text)や GeoJSON との相互変換には ST_GEOGFROMTEXT / ST_ASTEXT、ST_GEOGFROMGEOJSON / ST_ASGEOJSON を使います。

SELECT
  ST_ASTEXT(ST_GEOGPOINT(-73.9855, 40.7580)) AS wkt,
  ST_ASGEOJSON(ST_GEOGPOINT(-73.9855, 40.7580)) AS geojson;
-- wkt: POINT(-73.9855 40.758)
-- geojson: {"type":"Point","coordinates":[-73.9855,40.758]}
ST_CONTAINS(a, b) 領域 a 点 b TRUE ST_INTERSECTS(a, b) 領域 a 領域 b TRUE ST_DWITHIN(a, b, d) 距離 d 以内 TRUE
図 15-1 ST_CONTAINS は点が多角形の内部にあるか、ST_INTERSECTS は 2 つの領域の境界が重なるか、ST_DWITHIN は 2 点の距離が閾値以内かを判定する。破線は ST_DWITHIN が判定に使う仮想の距離範囲で、実際に円を作るわけではない。
実務の視点: 「どの行政区画に属するか」を厳密に判定したいなら ST_CONTAINS(多角形への内包判定)、「半径 5 km 圏内」のような近さを判定したいなら ST_DWITHIN(距離ベース)を選びます。行政境界のすぐ外側にある地点(埋立地・河川敷など)を ST_CONTAINS だけで判定すると取りこぼしが出るため、「最寄りの行政区画」のような曖昧な要件では両者を組み合わせる設計が実務では珍しくありません。
ハンズオン 15-1|最寄りの Citi Bike ステーションを検索する

NYC Citi Bike のステーション一覧(約 1,800 件の小テーブル。件数は変動します)から、タイムズスクエア付近の地点に最も近い 5 件を ST_DISTANCE で検索します。サンドボックスで完走可・無料枠内(処理量は数百 KB 程度、実測は dry run で確認)。

  1. タイムズスクエア(経度 -73.9855、緯度 40.7580)に近い順にステーションを検索します。
-- 処理: 数十 KB 程度(実測は dry run で確認)
SELECT
  name,
  ST_DISTANCE(
    ST_GEOGPOINT(longitude, latitude),
    ST_GEOGPOINT(-73.9855, 40.7580)
  ) AS distance_m
FROM `bigquery-public-data.new_york.citibike_stations`
ORDER BY distance_m
LIMIT 5;

期待される出力: distance_m が小さい順に 5 件のステーション名が並びます(先頭は数十〜数百メートル程度)。ORDER BY に ST_DISTANCE の計算結果を直接使うことで、「近い順」を素直に SQL で表現できます。

後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。

ハンズオン 15-2|ステーションを郡(county)単位でエリア集計する

Citi Bike ステーションが NY 州のどの郡(county)に属するかを ST_CONTAINS で判定し、郡ごとの件数を集計します。サンドボックスで完走可・無料枠内(geo_us_boundaries.counties は全米で 3,000 件強、州で絞ってもさらに小さい)。

  1. NY 州(state_fips_code = '36')の郡ポリゴンとステーションの位置を突き合わせます。
-- 処理: 数 MB 程度(county_geom はポリゴンなので points より大きい。dry run で確認)
SELECT
  c.county_name,
  COUNT(*) AS station_count
FROM `bigquery-public-data.new_york.citibike_stations` AS s
JOIN `bigquery-public-data.geo_us_boundaries.counties` AS c
  ON ST_CONTAINS(c.county_geom, ST_GEOGPOINT(s.longitude, s.latitude))
WHERE c.state_fips_code = '36'
GROUP BY c.county_name
ORDER BY station_count DESC;

期待される出力: New York(マンハッタン)・Kings(ブルックリン)・Queens(クイーンズ)といった郡名と、それぞれのステーション数が並びます。最寄り検索が「距離でランキング」だったのに対し、エリア集計は「多角形への内包判定で行を紐付ける」という別の仕組みであることを確認してください。

後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。

最寄り検索(15-1) 検索地点 ST_DISTANCE 最小 エリア集計(15-2) county_geom ST_CONTAINS で判定 圏外
図 15-2 最寄り検索は検索地点からの距離を計算してランキングする(点と点)。エリア集計は多角形の内部に点があるかを判定して行を郡に紐付ける(点と面)。同じ「位置」を扱っていても仕組みが異なる。

近似集計と HLL++ スケッチ — 精度とコストのトレードオフ

APPROX_COUNT_DISTINCT(expression) は COUNT(DISTINCT expression) の近似値を返します。統計的な推定値であり厳密な値と一致するとは限りませんが、大量の入力に対しては COUNT(DISTINCT) より高速に処理できます。他にも近似分位数を求める APPROX_QUANTILES、頻度上位を求める APPROX_TOP_COUNT / APPROX_TOP_SUM があります。

裏側で使われているのが HLL++(HyperLogLog++)というカーディナリティ推定アルゴリズムです。BigQuery では HLL_COUNT.INIT でスケッチ(BYTES 型の圧縮表現)を作り、HLL_COUNT.MERGE / HLL_COUNT.MERGE_PARTIAL でスケッチ同士を合成し、HLL_COUNT.EXTRACT で単一スケッチからカーディナリティを取り出します。MERGE は集約関数で最終的な件数(INT64)を返すのに対し、MERGE_PARTIAL は合成後の新しいスケッチ(BYTES)を返す点が異なります。

関数入力戻り値用途
HLL_COUNT.INIT(x[, precision])任意の値の列BYTES(スケッチ)スケッチを新規作成
HLL_COUNT.MERGE(sketch)スケッチの列INT64合成して最終件数を取得
HLL_COUNT.MERGE_PARTIAL(sketch)スケッチの列BYTES(スケッチ)合成して新しいスケッチを作成
HLL_COUNT.EXTRACT(sketch)単一のスケッチINT641 つのスケッチから件数を取得

精度(precision)は 10〜24 の範囲で指定でき、既定値は 15 です。精度を上げるほどストレージ消費と誤差のトレードオフが変わります。例えば精度 10 はスケッチ 1 個あたり約 1 KiB で 95% 信頼区間の誤差が ±6.50% ですが、既定の精度 15 では約 32 KiB で ±1.15% まで縮まります。

precision = 10 ストレージ 約 1 KiB 95% CI 誤差 ±6.50% precision を上げる precision = 15(既定) ストレージ 約 32 KiB 95% CI 誤差 ±1.15%
図 15-3 precision を上げるとスケッチ 1 個あたりのストレージ消費は増えるが、誤差の信頼区間は狭くなる。既定値 15 は多くの用途で十分な精度とストレージのバランス。
実務の視点: 第 8 章では dry run でスキャン量そのものを削る最適化を扱いましたが、HLL スケッチは「同じ集計を何度も再計算するコスト」を消す最適化です。日次のユニークユーザー数を毎回生ログから COUNT(DISTINCT) で計算し直すのではなく、日次スケッチを 1 回作って保存しておけば、任意の期間のユニークユーザー数を HLL_COUNT.MERGE だけで求められます。ダッシュボードのように同じ粒度の集計が繰り返し参照される場面で効果が大きい手法です。
ハンズオン 15-3|APPROX_COUNT_DISTINCT と COUNT(DISTINCT) の差を実測する

Citi Bike の乗車ログから使用された自転車の台数(bikeid の distinct 数)を、正確な COUNT(DISTINCT) と近似の APPROX_COUNT_DISTINCT の両方で求めて誤差を確認します。サンドボックスで完走可・無料枠内(bikeid 列のみを読むため処理量は数十 MB 程度、実測は dry run で確認)。

  1. 同じデータに対して両方の関数を実行し、値と誤差率を比較します。
-- 処理: 数十 MB 程度(bikeid 列のみをスキャン。実測は dry run で確認)
SELECT
  COUNT(DISTINCT bikeid) AS exact_count,
  APPROX_COUNT_DISTINCT(bikeid) AS approx_count,
  ROUND(
    ABS(APPROX_COUNT_DISTINCT(bikeid) - COUNT(DISTINCT bikeid))
      / COUNT(DISTINCT bikeid) * 100, 3
  ) AS error_pct
FROM `bigquery-public-data.new_york.citibike_trips`;

期待される出力: exact_count と approx_count はごく近い値になり、error_pct は既定精度(15)の理論誤差 ±1.15%(95% CI)に収まる範囲であることが多いです。実行のたびに乱数的な誤差が乗るため、毎回まったく同じ error_pct にはなりません。

後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。

ハンズオン 15-4|HLL スケッチを事前集計し、期間集計を MERGE で高速化する

乗車開始日ごとの HLL スケッチをテーブルに保存し、後から任意の期間のユニーク bikeid 数を HLL_COUNT.MERGE だけで取り出します。CREATE TABLE は DDL なのでサンドボックスで完走可・無料枠内(処理量は数十 MB 程度)。

  1. 演習用データセットを作成します(ロケーションは US)。
bq mk --location=US --dataset PROJECT_ID:handson_ch15
  1. 日ごとの HLL スケッチを事前集計してテーブルに保存します。
-- 処理: 数十 MB 程度(starttime と bikeid のみをスキャン)
CREATE TABLE `PROJECT_ID.handson_ch15.daily_bike_sketches` AS
SELECT
  DATE(starttime) AS ride_date,
  HLL_COUNT.INIT(bikeid) AS bike_sketch
FROM `bigquery-public-data.new_york.citibike_trips`
GROUP BY ride_date;
  1. 2015 年 1 月分だけを HLL_COUNT.MERGE で合成し、期間内のユニーク bikeid 数を求めます。生ログを読み直さず、日次スケッチだけをスキャンする点に注目してください。
-- 処理: 1 MB 未満(daily_bike_sketches の該当行だけをスキャン)
SELECT HLL_COUNT.MERGE(bike_sketch) AS unique_bikes_jan2015
FROM `PROJECT_ID.handson_ch15.daily_bike_sketches`
WHERE ride_date BETWEEN '2015-01-01' AND '2015-01-31';

期待される出力: 1 月に使用された自転車の推定台数(1 つの INT64 値)。日をまたいだ期間を変えるたびに生ログへ戻る必要がなく、WHERE 句で対象の ride_date を絞ってスケッチを合成するだけで再集計できることを確認してください。

後片付け:

bq rm -r -f -d PROJECT_ID:handson_ch15
生ログ(乗車履歴) 日次 INIT 日次スケッチ テーブルに保存 期間で絞る HLL_COUNT .MERGE 取得 件数 → 生ログを読み直さず、スケッチだけを何度でも再集計できる
図 15-4 生ログから日次で HLL スケッチを作って保存し、後から必要な期間だけ HLL_COUNT.MERGE で合成する。再集計のたびに生ログをスキャンし直す必要がない。

BigQuery Studio ノートブックと BigQuery DataFrames — Python × BigQuery

BigQuery Studio のノートブックは Colab Enterprise 上で動作し、SQL セルと Python セルを 1 つのインターフェースで組み合わせられます。ノートブックの実行には Compute Engine の VM が割り当てられる「ランタイム」が必要で、複数のノートブックが同じランタイムを共有できますが、各ランタイムは 1 人のユーザー専用です。ノートブックのスロット使用量は Cloud Billing レポートで goog-bq-feature-type ラベルの値 BQ_STUDIO_NOTEBOOK を使って追跡できます。

ノートブックの中心的な価値がBigQuery DataFrames(bigframes.pandas)です。pandas 互換の API を追加設定なしで使え、実際の計算はローカルではなく BigQuery のクエリエンジン側で実行されます。

import bigframes.pandas as bpd

bpd.options.bigquery.project = "your_gcp_project_id"
bpd.options.bigquery.ordering_mode = "partial"

df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins")
df.peek()

# pandas と同じ書き方だが、計算は BigQuery 側で実行される
average_body_mass = df["body_mass_g"].mean()

ordering_mode は "partial"(効率重視、行の順序は明示的にソートするまで保証されない)と "strict"(既定値、pandas 互換性重視)を選べます。df.peek() は結果を効率的にプレビューするための専用メソッドです。

df["x"].mean() pandas ライク操作 遅延評価 GoogleSQL に コンパイル 実行 BigQuery クエリエンジン 返却 DataFrame として結果
図 15-5 bigframes.pandas の操作はその場で実行されず、遅延評価でクエリプランに積み上がる。実際の計算は GoogleSQL にコンパイルされて BigQuery のクエリエンジンが担い、結果だけが DataFrame として手元に返る。

ノートブックや DataFrames を使わない、より薄い選択肢がクライアントライブラリです。Python では google-cloud-bigquery パッケージを使い、クエリを投げて結果を受け取るだけの最小構成を書けます。

from google.cloud import bigquery

client = bigquery.Client()
results = client.query_and_wait(
    "SELECT name, num_bikes_available "
    "FROM `bigquery-public-data.new_york.citibike_stations` "
    "WHERE num_bikes_available > 30"
)
for row in results:
    print(f"{row.name}: {row.num_bikes_available}")

DataFrames が「pandas ライクな変換をクエリに翻訳する」レイヤーであるのに対し、クライアントライブラリは「SQL 文字列を投げて結果を受け取る」だけの薄いレイヤーです。既存のアプリケーションに組み込むならクライアントライブラリ、データ探索や特徴量エンジニアリングを対話的に進めるならノートブック + DataFrames、という使い分けになります。

実務の視点: ノートブックは Compute Engine の VM ランタイムを起動するため課金プロジェクトが前提です。サンドボックスの制限一覧にノートブックという名前は明記されていませんが、「課金を有効にしたプロジェクトを検証してください」がノートブックの前提条件として明示されているため、実質的にサンドボックスだけでは完結しません。この章のノートブック・DataFrames の内容を「読むだけで理解が完結する」構成にしているのはこのためです。

つまずきポイント

ST_GEOGPOINT の引数順序を逆にしてしまう: ST_GEOGPOINT(longitude, latitude) は経度が先です。緯度を先に渡すと、経度・緯度の範囲チェック(緯度は -90〜90)に引っかかってエラーになるか、まったく違う地点を指す点になります。地図サービスの「緯度, 経度」表記の慣習と逆であることを覚えておいてください。
ST_DISTANCE と ST_DWITHIN の距離はメートル: 度数(degree)ではなくメートル単位です。「半径 0.01 くらい」のような度数感覚の値をそのまま渡すと、意図とまったく異なる範囲になります。ST_DWITHIN の use_spheroid パラメータは現時点では FALSE のみサポートで、球面上の距離として計算されます。
HLL スケッチの MERGE と MERGE_PARTIAL を混同する: HLL_COUNT.MERGE は最終件数(INT64)を返しますが、HLL_COUNT.MERGE_PARTIAL は合成後の新しいスケッチ(BYTES)を返します。さらに集計を続けたい中間結果としてテーブルに保存するなら MERGE_PARTIAL、その場で件数が欲しいなら MERGE です。異なる precision で作られたスケッチを合成すると、精度は低い方に自動的に切り下げられます。
ノートブックがサンドボックスで動かない: 「地理空間も近似集計もサンドボックスで動いたのだから、ノートブックも動くはず」とは限りません。ノートブックのランタイムは Compute Engine の VM であり、課金が有効なプロジェクトが前提です。サンドボックスのままノートブックを開こうとした場合は、まず課金の有効化を確認してください。

理解度チェック

Q1. ST_GEOGPOINT の引数の順序はどちらが正しいですか?

ST_GEOGPOINT(longitude, latitude)、つまり経度が先で緯度が後です。多くの地図サービスの「緯度, 経度」表記とは逆なので、取り違えに注意が必要です。

Q2. ST_CONTAINS と ST_DWITHIN の使い分けの基準は何ですか?

「どの行政区画に属するか」のような多角形への内包を厳密に判定したいなら ST_CONTAINS、「半径◯ km 圏内」のような距離ベースの近さを判定したいなら ST_DWITHIN を使います。境界のすぐ外側にある地点を漏らしたくない場合は、両者を組み合わせる設計を検討します。

Q3. HLL_COUNT.MERGE と HLL_COUNT.MERGE_PARTIAL の戻り値の違いは何ですか?

HLL_COUNT.MERGE はスケッチを合成した最終的なカーディナリティ(INT64)を返します。HLL_COUNT.MERGE_PARTIAL はスケッチを合成した新しいスケッチ(BYTES)を返し、さらに別のスケッチと合成したり、後で HLL_COUNT.EXTRACT や HLL_COUNT.MERGE にかけたりできます。

Q4.(シナリオ)日次のアクティブユーザー数を毎日集計しており、任意の期間(週次・月次・四半期)のユニークユーザー数をダッシュボードで高速に表示したい。生ログは巨大で、期間ごとに毎回 COUNT(DISTINCT) をかけると重い。最適な設計は?
A. 生ログに対して毎回 COUNT(DISTINCT) を実行するクエリをそのままダッシュボードに使う B. 日次で HLL_COUNT.INIT のスケッチを事前集計してテーブルに保存し、期間ごとに HLL_COUNT.MERGE で合成する C. APPROX_TOP_COUNT で上位ユーザーだけを集計する D. 生ログをすべて BigQuery DataFrames にロードしてローカルで集計する

B。 日次スケッチを事前集計しておけば、期間を変えるたびに生ログへ戻らずスケッチだけを合成でき、ダッシュボードの応答が速くなります。A は生ログを毎回スキャンし直すため遅く高コストです。C は上位頻度を求める関数でユニークユーザー数の推定には向きません。D は DataFrames が pandas ライクな API でクエリを組み立てる仕組みであり、ローカルに全データを持ってくる設計ではありません。

Q5.(シナリオ)BigQuery サンドボックスだけでノートブックを開いて BigQuery DataFrames を試そうとしたところ、想定通りに進められなかった。最も可能性が高い原因は?
A. GEOGRAPHY 型がサンドボックスで使えないため B. ノートブックのランタイムが Compute Engine の VM であり、課金が有効なプロジェクトが前提のため C. APPROX_COUNT_DISTINCT がサンドボックスで使えないため D. HLL スケッチの precision が範囲外のため

B。 ノートブックはColab Enterpriseのランタイム(Compute Engine VM)を必要とし、DataFrames のクイックスタートでも課金の有効化が前提条件として明示されています。A・C は地理空間・近似集計ともにこの章のハンズオンで実際にサンドボックスで完走しており誤りです。D は精度の指定範囲(10〜24)とは無関係な原因です。

まとめと次章へ