ARCHININGEN TECH BLOG

千億行の位置データを Databricks で集計する — Uber H3 の実践と運用設計

H3 の解像度と階層構造。解像度 n の六角形セルは約 7 個の解像度 n+1 の子セルに分割され、子セル群は親の輪郭から少しはみ出す。解像度は 0〜15 の 16 段階で、1 段階細かくなるごとに平均セル面積は約 1/7 になる

車両や端末が発し続ける緯度経度のログは、気づけば数十億〜数千億行に膨らみます。この規模になるとジオメトリ演算での空間結合は選択肢に入らず、「位置データをどう持ち、どう集計するか」自体が設計課題になります。本記事では、この課題を GROUP BY と等値 JOIN の世界に持ち込む Uber H3 の仕組みと、Databricks の組み込み関数による実践パターン・運用設計を紹介します。

Uber H3 とは

H3 は Uber が開発したオープンソース (Apache 2.0) の地理空間グリッドシステムで、地球全体を六角形のセルで敷き詰めて各セルに 64 ビット整数の ID を割り当てます。緯度経度をセル ID に変換してしまえば、「この点はどのエリアに属するか」「近くに何があるか」といった空間的な問いを、ID の GROUP BY と等値 JOIN だけで扱えるようになります。

一言でいえば「空間結合を等値結合に変える変換器」です。この一点が、後半で紹介する集計パターンと運用設計すべての土台になります。

グリッドの形に六角形を選んだのが H3 の設計判断です。平面を隙間なく敷き詰められる正多角形は三角形・四角形・六角形の 3 つですが、四角形グリッドには「辺で接する隣接セル 4 つと、角で接する隣接セル 4 つで中心間距離が違う」という問題があります。

六角形なら 6 つの隣接セルすべてが辺で接し、中心間距離も等しくなります。「k セル以内」といった近傍探索や移動の流れの分析が素直に書けるため、配車データを扱う Uber がこの性質を必要としたのは自然な流れです。

なお球面は六角形だけでは覆えないため、各解像度に 12 個だけ五角形のセルが存在します。H3 は正二十面体を海上に頂点が来る向きで地球に投影しているので、五角形はすべて海上にあり、陸上のデータを扱う限り実務で遭遇することはまずありません。

解像度と階層構造

セルの細かさは解像度 0〜15 の 16 段階です。1 段階細かくなるごとに各セルは約 7 個の子セルに分割され、平均セル面積は約 1/7 になります (aperture 7 と呼ばれる構造です)。代表的な解像度の目安は次のとおりです。

解像度 平均セル面積 粒度のイメージ
0 約 436 万 km² 地球全体を 122 セルで分割
2 約 8.7 万 km² 国・地方
5 約 253 km² 市区町村
7 約 5.2 km² 地区
10 約 1.5 万 m² 街区
13 約 44 m² 建物
15 約 0.9 m² 人の立ち位置

街区スケールの分析なら 10、建物スケールなら 13、広域の集計なら 5〜7 あたりが出発点になります。解像度の選び方の指針は後半の「運用の勘どころ」にまとめました。

セル ID は解像度の情報を内包した 64 ビット整数で、8928308280fffff のような 15 桁の 16 進文字列としても表現できます。

親子関係にはひとつ注意があります。冒頭の図に描いたとおり、親子は「子セル 7 個が親を近似的にカバーする」関係であって厳密な包含ではなく、子セルは親の輪郭から少しはみ出します。集計キーとして使う分には実害はありませんが、厳密な境界判定を H3 の階層だけに頼らないでください。

Geohash・Google S2 との比較

「座標をグリッドの ID に変換する」仕組みは H3 だけではなく、Geohash と Google S2 がよく比較対象になります。それぞれ設計思想が違うので、先に位置関係を整理しておきます。

観点 Geohash Google S2 H3
セル形状 長方形 (緯度で縦横比が変化) 球面に投影した四角形 六角形 (+ 五角形 12 個)
階層 文字列長 1〜12 レベル 0〜30 解像度 0〜15
親子の包含 厳密 (文字列の前方一致) 厳密 近似 (はみ出しあり)
ID 表現 base32 文字列 64 ビット整数 64 ビット整数
強み 単純さ・prefix 検索 厳密な階層と被覆 近傍の一様性

Geohash は最古参で最も単純です。緯度経度を交互にビット分割して base32 文字列にしたもので、文字列の前方一致がそのまま空間の包含になります。LIKE 'xn76u%' のような検索が書け、Elasticsearch や Redis が標準サポートしている手軽さが強みです。

弱点は近傍探索です。セルは長方形で緯度によって形が歪み、境界をまたぐと隣どうしなのに文字列がまったく変わる箇所があります。

Google S2 は球面を立方体に投影した四角形セルで地球を覆います。最大の特徴は親子の階層が厳密な包含関係になっていることで、任意の領域を粗いセルと細かいセルの混合で正確に覆う covering が得意です。ジオフェンスのような「含まれるか否か」を厳密に扱う用途に強く、BigQuery の GEOGRAPHY クラスタリングも S2 ベースです。

H3 はこの 2 つと比べると「近傍の一様性」に全振りした設計です。前述のとおり親子の厳密な包含を捨てる代わりに、隣接セルとの関係が均質で k-ring や流れの分析が素直に書けます。prefix 検索なら Geohash、厳密な包含なら S2、近傍・密度・移動の分析なら H3、というのが素性ベースの使い分けです。

ただし実務では、使うプラットフォームが最適化済みの関数を持っているかが素性と同じくらい効きます。Databricks の組み込みグリッド関数は H3 だけです。

Geohash は pygeohash などを Python UDF で回すことになり、ネイティブ関数と比べて性能が一段落ちます。S2 に至っては組み込みサポートがなく、Apache Sedona のような外部ライブラリを持ち込む構成になります。Databricks 上で選ぶなら、素性の議論に入る前に H3 が第一候補になる、というのが正直なところです。

標準地域メッシュとの関係

日本の位置データ分析では、もうひとつ比較したい相手がいます。総務省の標準地域メッシュ (JIS X 0410) です。国内のデータ基盤では「H3 と地域メッシュのどちらで持つべきか」が実際によく論点になります。

観点 標準地域メッシュ H3
セル形状 緯度経度で区切った四角形 六角形
階層 1 次 (約 80km) 〜 1/4 地域 (約 250m) 解像度 0〜15
親子の包含 厳密 (コードの前方一致) 近似 (はみ出しあり)
強み 公的統計との結合キー 近傍・密度・レイアウト設計

地域メッシュは緯度経度を等間隔に区切った四角形グリッドで、メッシュコードは桁を増やすほど細かくなり、前方一致がそのまま包含になります。素性としては Geohash に近い設計です。

ただし最大の価値はグリッドの形ではなく、データの側にあります。国勢調査や経済センサスといった公的統計がメッシュ単位で集計・配布されているため、人口・世帯・事業所数と自前のデータを繋ぐならメッシュコードがそのまま共通キーになります。これは H3 にはない資産です。

使い分けはシンプルです。公的統計と結合する集計は地域メッシュ、自前の位置データの密度・近傍・データレイアウト設計は H3 に寄せます。両者を橋渡ししたいときは、細かいメッシュの中心点を h3_longlatash3 に通して対応表を作る近似変換が実務的です。

Databricks で使う

Databricks では H3 関数がランタイムに組み込まれており、ライブラリのインストールは不要です。Databricks Runtime 11.3 LTS 以降で利用できます (内部では H3 の Java ライブラリ 3.7.0 を使用)。

登場当初は Photon 有効なクラスタか Pro / Serverless の SQL ウェアハウスが必要でしたが、この制約は緩和済みです。Databricks Runtime 17.0 以降は、classic SQL ウェアハウスを除く全コンピュートでサポートされます (非 Photon クラスタを含む)。

SQL ならそのまま関数を呼べます。動作確認を兼ねて 1 クエリ投げてみます。

SQL
-- 東京駅の座標を解像度 10 のセル ID に変換する (引数は経度が先)
SELECT h3_longlatash3(139.7671, 35.6812, 10) AS cell,
       h3_h3tostring(h3_longlatash3(139.7671, 35.6812, 10)) AS cell_str;

BIGINT のセル ID と 16 進文字列が返ってくれば準備完了です。引数は経度 (longitude) が先である点にだけ注意してください。関数名の longlat がその順序を表しています。

Python (PySpark) から使う場合は専用モジュールをインポートします。

PYTHON
# PySpark 用の H3 関数をインポートする
from pyspark.databricks.sql import functions as dbf
df = df.withColumn("cell", dbf.h3_longlatash3("lng", "lat", 10))

基本の関数パターン

関数は 30 個以上ありますが、役割で分けると次の 5 グループを覚えれば十分です。

  • 入口 (ジオメトリ → セル): h3_longlatash3 (経度緯度 → セル)、h3_polyfillash3 (WKT / WKB / GeoJSON のポリゴン → セル配列)、h3_coverash3 (ポリゴンを完全に覆うセル配列)
  • 出口 (セル → ジオメトリ): h3_boundaryasgeojson / h3_boundaryaswkt (セルの六角形境界)、h3_centerasgeojson (セル中心)。可視化ツールへ渡すときに使います
  • 表現の変換: h3_h3tostring (BIGINT → 16 進文字列)、h3_stringtoh3 (その逆)
  • 階層の移動: h3_toparent / h3_tochildren (解像度の上げ下げ)、h3_resolutionh3_ischildof
  • 近傍: h3_kring (グリッド距離 k 以内のセル)、h3_hexring (距離ちょうど k のセル)、h3_distance (2 セル間のグリッド距離)

このほか h3_try_polyfillash3 のような try_ 系が用意されており、不正なジオメトリでエラーの代わりに NULL を返します。外部から受け取ったデータの品質が保証できないパイプラインでは try_ 系を使うとジョブが落ちなくなります。

実践レシピ

どのレシピも「座標をセル ID に変換し、あとは GROUP BY と等値 JOIN で処理する」という次の流れの変奏です。

位置データを H3 セルで集計する流れ。経度・緯度のポイントを h3_longlatash3 でセル ID に変換し、同じセルに落ちる点をまとめ、GROUP BY でセル単位の件数に集計する

ポイントの密度集計

最頻出のパターンです。座標をセル ID に変換して GROUP BY するだけで、ヒートマップの元になる密度データができます。

SQL
-- 乗車地点を解像度 10 のセル単位で集計する
SELECT h3_longlatash3(pickup_lng, pickup_lat, 10) AS cell, count(*) AS cnt
FROM rides
GROUP BY cell
ORDER BY cnt DESC;

ポリゴン × ポイントの空間結合

「各エリアに含まれるポイントを数える」典型的な空間結合は、ポリゴンを h3_polyfillash3 でセル配列に展開しておけば等値 JOIN に置き換えられます。

SQL
-- エリアポリゴンをセルに展開し、ポイントと等値 JOIN で突き合わせる
WITH area_cells AS (
  SELECT area_id, explode(h3_polyfillash3(area_wkt, 10)) AS cell
  FROM areas
)
SELECT a.area_id, count(*) AS cnt
FROM rides r
JOIN area_cells a
  ON h3_longlatash3(r.pickup_lng, r.pickup_lat, 10) = a.cell
GROUP BY a.area_id;

ST_Contains のような空間述語を全行 × 全ポリゴンで評価する代わりに、Spark が最も得意とするシャッフル付き等値 JOIN で処理されるため、データ量が増えてもそのままスケールします。セル単位の近似で精度が足りるかは解像度次第なので、ここは後述の使い分けを参照してください。

近傍セルの検索

「この地点の周辺」は h3_kring で表現します。半径をメートルではなくグリッド距離 (セル何個分) で指定する感覚です。

SQL
-- 東京駅のセルからグリッド距離 2 以内のセル群に含まれるポイントを取る
SELECT *
FROM rides
WHERE h3_longlatash3(pickup_lng, pickup_lat, 10) IN (
  SELECT explode(h3_kring(h3_longlatash3(139.7671, 35.6812, 10), 2))
);

解像度のロールアップ

細かい解像度で集計済みのテーブルは、h3_toparent で粗い解像度に集計し直せます。

SQL
-- 解像度 13 の集計結果を解像度 10 に巻き上げる
SELECT h3_toparent(cell, 10) AS parent_cell, sum(cnt) AS cnt
FROM cell_counts_r13
GROUP BY parent_cell;

細かい方から粗い方へは後からいつでも巻き上げられますが、逆は再計算になります。この非対称性が、次章で述べる「迷ったら細かめに持つ」の根拠です。

可視化への受け渡し

集計結果は h3_h3tostring で文字列にして書き出せば、kepler.gl のように H3 文字列を直接解釈できるツールでそのまま六角形メッシュとして描画できます。GeoJSON が必要なツールへは h3_boundaryasgeojson で境界ポリゴンを添えます。

点を面に抽象化する — H3 が効く本当の理由

ここまでのレシピの背後にある考え方を、一度立ち止まって言葉にしておきます。位置情報ビッグデータに対して H3 が効く理由は、この 1 枚に集約されます。

数千億の点から目的の点を探す絞り込みの流れ。膨大な点をセルで面に集約して対象の面まで絞り込み、その面に属する少数の点だけを精査する

位置情報データは、とにかく行数が膨らみます。車両や端末が発し続ける緯度経度ポイントは、数十億〜数千億行のスケールに達することが珍しくありません。この規模になると、Databricks のようなビッグデータ基盤であっても ST_Contains のようなジオメトリ関数を全行に評価する処理は成立せず、そもそも選択肢に入らないのが実情です。

H3 の役割は、この膨大な点の集まりを面に抽象化することです。緯度経度ポイントを解像度 10 のセルに変換すると、値の種類は数百分の 1 のオーダーに集約されます。個々の点を突き合わせる代わりに「同じ面に属するか」だけを見ればよくなるため、絞り込み・結合・集計のすべてが桁違いに軽くなります。

数千億の点から目的の 1 点を探すのは、砂漠から特定の砂粒を見つけるような作業です。セルで砂丘まで絞ってから砂粒を探す、と段階を分ければ現実的な時間で終わります。この「絞り込みを段階に分ける」発想が、次章の運用設計の背骨です。

運用の勘どころ

実務で使い込むうえで効いてくるポイントをまとめます。

巨大テーブルは「粗い H3 + 細かい H3」の 2 列で設計する

前章の「点を面に抽象化する」を検索性能に直結させる、緯度経度を含むビッグデータテーブルの設計パターンです。

  • 書き込み時にセル列を 2 つ追加しておく: 大きめの解像度 (例: 解像度 10) と、分析に必要な最小粒度 (例: 解像度 15) の 2 列です。クエリのたびに h3_longlatash3 を評価するのではなく、列として事前計算しておくのがポイントです
  • 粗い方のセル列で Z-ORDER (または Liquid Clustering) しておく: H3 のセル ID は近い場所ほど近い値になる性質があるため、空間的に近い行がファイル上も近くに配置され、セル指定のファイルスキップが効くようになります

検索はこの 2 列を粗い方から順に使います。

  1. 粗いセル (解像度 10) で絞る — Z-ORDER が効いてスキャン量が激減します
  2. 必要なら緯度経度の範囲条件を重ねる — 矩形での絞り込みが必要な場合は Range Join で挟みます (オプション)
  3. 細かいセル (解像度 15) で厳密に絞る — 対象領域を解像度 15 で polyfill したセル集合との突き合わせで仕上げます

建物のような任意形状の領域でも、細かい解像度のセル集合は輪郭に沿って領域を覆うため、形に沿った検索が精度高くできます。最初から最後までジオメトリ演算は登場しません。H3 を「集計の道具」で終わらせず「データレイアウトと検索の道具」として使うのが、Databricks で H3 を使う一番の妙味です。

解像度は「分析の最小粒度より 1〜2 段細かく」持つ

前述のとおり、H3 の集計は細→粗の巻き上げが自由な一方、粗→細は元データからの再計算になります。保存するテーブルは想定する分析粒度より 1〜2 段細かい解像度で持っておき、用途ごとに h3_toparent でロールアップするのが柔軟です。

ただし 1 段階でセル数は約 7 倍になるので、無闇に細かくするとセル数が爆発します。ポリゴンの polyfill なら、ポリゴンの大きさに対してセルが十分小さくなる解像度 (目安としてポリゴン 1 つあたり数十〜数百セル) で止めるのが現実的です。

また、JOIN や h3_distance は両辺が同じ解像度であることが前提です。複数のデータセットを H3 で突き合わせる予定があるなら、組織内で「標準解像度」を先に決めておくと後の手戻りがなくなります。

セル ID は BIGINT で持つ

セル ID は BIGINT と 16 進文字列の 2 表現がありますが、保存・結合はすべて BIGINT に統一してください。公式ドキュメントも BIGINT 表現の方が高性能と明言しており、ストレージ効率も比較の速度も有利です。文字列 (h3_h3tostring) は kepler.gl などへ渡す最後の出口だけで使います。

polyfill / cover / tessellate の使い分け

ポリゴンをセルに変換する関数は 3 系統あり、境界の扱いが異なります。

  • h3_polyfillash3: セルの中心がポリゴン内にあるセルだけを返します。隣接ポリゴン同士でセルが重複しないため集計 (按分なしの数え上げ) 向きですが、境界ぎわのポイントを取りこぼす可能性があります
  • h3_coverash3: ポリゴンを完全に覆う最小のセル集合を返します。漏れが許されない判定 (ジオフェンス等) 向きですが、境界の外側を含むため重複や過剰包含が起きます
  • h3_tessellateaswkb: セルとポリゴンの交差ジオメトリまで返します。境界セルを面積按分したい厳密な集計はこちらです

「取りこぼしを許すか、はみ出しを許すか」で polyfill と cover を選び、どちらも許せないときだけ tessellate に進む、という順で考えると迷いません。

関数名は H3 v3 世代 — 本家ドキュメントとの対応に注意

Databricks の H3 関数は H3 Java ライブラリ 3.7.0 ベースのため、関数名も v3 世代の命名です。一方、本家 H3 は v4 で主要 API の名前を変えており、公式ドキュメントや h3-py / h3-js の最新版は新名称で書かれています。読み替えの対応は次のとおりです。

Databricks (v3 世代) 本家 H3 v4
h3_kring gridDisk
h3_polyfillash3 polygonToCells
h3_toparent / h3_tochildren cellToParent / cellToChildren
h3_distance gridDistance

もうひとつの罠が引数の順序です。Databricks の h3_longlatash3経度が先、h3-py の latlng_to_cell緯度が先です。

ノートブックで両方を併用すると、高確率で一度は取り違えます。結果が明らかに変な場所 (海上など) を指したら、まず引数順を疑ってください。

面積の歪みを前提に置く

同じ解像度でも、セルの実面積は場所によって最大 2 倍近く変わります。投影の性質上、赤道付近と高緯度で歪みが出るためです。

日本国内の分析のように範囲が限られていれば、実用上は無視できます。広域で「面積あたり密度」を厳密に比較するときだけ、セルごとの実面積で正規化してください。五角形セル (h3_ispentagon で判定可能) はすべて海上にあるため、通常は気にする必要はありません。

まとめ

H3 の要点は「空間結合を等値結合に変換する」の一点に尽きます。座標をセル ID にした瞬間から、位置データは GROUP BY・JOIN・クラスタリングキーというデータ基盤の通常装備で扱えるようになります。

Databricks なら関数は組み込み済みです。まず h3_longlatash3h3_polyfillash3 の 2 つから試し、解像度の感覚が掴めたら BIGINT 統一・標準解像度・「粗い H3 + 細かい H3」の 2 列設計という運用面に手を広げる順番をおすすめします。

参考リンクをまとめておきます。