Lesson 10

プライバシ・グリッド — k-匿名性 / quadtree / 差分プライバシ

v2-rewrite心得応用基礎プライバシガバナンス
所要 120分 / 想定レベル: 心得 / 応用基礎 / データ: DoBoX #1279 カメラ位置 351点 (位置情報のプライバシ保護シミュレーション)

データ取得手順

⚠️ このスクリプトは自動取得に対応していません。以下のデータセットを DoBoX から手動でダウンロードし、data/extras/ 以下に保存してください。

IDデータセット名
#444dataset #444
#1279県内のカメラ情報

実行コマンド:

cd "2026 DoBoX 教材"
python -X utf8 lessons/L10_privacy_grid.py

DoBoX のオープンデータは申請不要・商用/非商用とも利用可。 data/extras/.gitignore 対象(約 57 GB のキャッシュ)。 スクリプト実行で自動再生成されます。

学習目標と問い

このレッスンで答えたい問い

「カメラ位置のような『ピンポイントの位置データ』を、個人特定リスクを下げて公開するには、どんな道具がどこまで効くのか?」

道路・河川カメラ自体は本来公開情報。本レッスンは 「個人や要援護者の位置データを、仮にこのカメラ点群と同じ密度で扱った場合」 を念頭に、 代表的な3手法を 「ツール」として比較する シミュレーション教材

用語の定義 (このレッスン独自・冒頭で平易に)

  • k-匿名性 (k-anonymity): 「同じ場所(=同じセル)にいる人が k 人以上になっていれば、その中の誰かは特定できない」という考え方。 k=1 はその場所に あなた1人しかいない=即特定。k=5 なら最低5人と紛れる。 ツールとしては「セルに分割して、k 未満のセルは公開しない or 粗くする」操作。
  • quadtree (クアッドツリー): 地図を 4分割→各区画をさらに4分割→…と再帰的に細かくしていく木構造。 本レッスンでは「セル内の点数が k_min 未満になりそうなら分割を止める」よう細工する。 結果: 都市部は細かく、過疎地は粗く、点数のバランスが取れたセル分割になる。
  • 差分プライバシ (Differential Privacy, DP): データに小さなランダムノイズを加えることで、「あなた1人がデータに含まれていたかどうかを攻撃者が見破れない」ことを 数学的に保証する技法。 ε(イプシロン) という1つのパラメータで「保護の強さ」を制御する(小さいほど強保護・大きいほど弱保護)。
  • utility / privacy: utility(ユーティリティ)=「データの使いやすさ・精度」、privacy(プライバシ)=「個人が守られる度合い」。 この2つは シーソー関係で、両方を最大化するのは原理的に難しい。本レッスンの最終図(図5)で定量化する。

立てた仮説 (後で検証する)

  1. H1 (固定グリッドは万能ではない): 「県全体に同じ大きさの格子をかぶせる」という素朴な方法では、 都市部は過剰に粗く、山間部は k=1 のままになる。 つまり 均一なセルでは均一な保護にならないはず。
  2. H2 (k を上げるほど解像度が必要): k≧1, 2, 5, 10 と要求を厳しくするほど、それを満たすには 大きなセルが必要になる。 k≧10 を全点で達成するには、km単位のセルが要るはず。
  3. H3 (quadtree は固定グリッドに勝つ): 「点が多い場所だけ細かく割る」適応分割なら、 同じプライバシ水準(k_min)を より細かい平均粒度で達成できるはず。 utility-privacy 平面上で固定グリッドより 左上に位置する。
  4. H4 (差分プライバシは ε で揺らぎが指数的に変わる): Laplace ノイズの幅は b=Δ/ε。 ε を 1.0 → 0.1 に下げると揺らぎは 10倍になる。 ε=0.1 のような強保護では、地図公開には 使い物にならないレベルでジッタするはず。
  5. H5 (1棟特定は10m格子で起きる): GPS の生精度が 5〜10m。 10m 格子では占有セルの大半が k=1 となり、 1棟単位での特定が技術的に可能になるはず。 「位置データは本質的に強い識別子」という直観を数値で確認する。

到達点

使用データ

ダウンロード(再現用データ・中間データ・図)

本レッスンの全成果物に直リンクを置いた。途中ステップから再現したい学習者向け。

1. 生データ (DoBoX 由来)

ファイル形式サイズ取得元
data/camera_list.csv CSV (緯度・経度・住所・路河川名・所管・管理区分)約 70 KB / 351 行 DoBoX #1279

2. プログラムで生成される中間データ

ファイル内容使う分析
L10_grid_counts.csv 1km/500m/250m 各解像度の占有セル・k=1セル数等の集計分析1 固定グリッド
L10_sample_trace.csv 1点 (#1 苗代カメラ) を生データ→3粒度に量子化した Before/After 表分析1 入出力具体例
L10_k_anonymity_curve.csv 6解像度 × 4 k値 の達成率(% を満たす点の割合)分析2 k曲線
L10_quadtree_summary.csv k_min=2/5/10 の leaf 数・面積中央等のサマリ分析3 quadtree
L10_quadtree_cells.csv 全 leaf の bbox・点数・面積 (3 k_min 全部)分析3 quadtree 詳細
L10_dp_results.csv ε=1.0/0.5/0.1 の Laplace b・ジッタ統計分析4 差分プライバシ
L10_dp_per_point.csv 全 351 点 × 3 ε のジッタ距離 (1053行)分析4 詳細
L10_tradeoff.csv 固定グリッド6点 + quadtree 3点 のセル面積×k中央分析5 トレードオフ
L10_house_scale.csv 5m〜100m の細粒度における k=1 リスク仮説H5 検証用

3. 図 PNG

4. 再現スクリプト

cd "2026 DoBoX 教材"
py -X utf8 lessons/L10_privacy_grid.py

スクリプト本体: lessons/L10_privacy_grid.py (データが無ければ ensure_dataset() で自動DL → 5枚のPNGと9本の中間CSVを生成)

分析1: 固定グリッド + k-匿名性

狙い

「県全体を同じ大きさの格子で割って、各セルの中に何人(=点)いるかを数える」だけの素朴な方法を試す。 これが k-匿名性の最もシンプルな実装で、3つの粒度 (1km / 500m / 250m) を比較して 「均一格子の盲点」(=都市部は過剰に粗く、山間部は k=1 のまま) を視覚化する。仮説H1 の検証。

ツールとしての k-匿名性:
  • 入力: 351点の緯度経度
  • 出力: 各点に「同じセル内の他の点数 (k)」のラベル
  • 使い方: k 未満のセルにある点は「公開しない」「粗いセルにマージする」「ノイズで上書きする」等の判断材料にする
  • 限界: 同じセル内の人が 属性的に均質(全員が同じ性別・年齢) なら、k≧5 でも事実上特定できる(homogeneity attack)。これを補うのが l-diversity / t-closeness で、発展課題で扱う。

手法 (緯度経度の格子量子化)

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np, pandas as pd

# === (1) 緯経度の cos 補正 (実距離→緯経度オフセット) ===
LAT_KM = 111.0
LON_KM = 111.0 * np.cos(np.radians(df["lat"].mean()))   # 北緯34.5度なら ≒92km

def grid_label(lat, lon, cell_m):
    """点群を cell_m 格子に量子化し、(行 i, 列 j) を返す"""
    cell_lat = cell_m / 1000 / LAT_KM       # 例: 250m → 約 0.00225度
    cell_lon = cell_m / 1000 / LON_KM       # 例: 250m → 約 0.00271度
    return (np.floor(lat / cell_lat).astype(int),
            np.floor(lon / cell_lon).astype(int))

# === (2) 3 解像度それぞれで集計 ===
for cs in [1000, 500, 250]:
    i, j = grid_label(df["lat"], df["lon"], cs)
    counts = pd.Series(list(zip(i, j))).value_counts()  # セル毎の点数
    n_occupied = (counts > 0).sum()                     # 占有セル数
    n_k1       = (counts == 1).sum()                    # k=1 リスクセル数
    print(f"{cs}m: 占有={n_occupied}, k=1={n_k1} ({n_k1/n_occupied*100:.0f}%)")

図と読み取り

なぜこの図か: 3粒度を 同じ枠で並べる(small multiples) ことで、 セルが細かくなるほど k=1 の白〜薄黄セルが急増する様子を一目で比較できる。 1枚の図だけでは「これは粗いのか細かいのか」を相対判断できない。

図1: 1km / 500m / 250m の固定グリッド3解像度。色濃いほど多人数が同じセルに集まり、薄いセルは k=1=即特定リスク
図1: 1km / 500m / 250m の固定グリッド3解像度。色濃いほど多人数が同じセルに集まり、薄いセルは k=1=即特定リスク

この図から読み取れること:

表と読み取り

なぜこの表か: 図1 の見た目を 「失う点数」という具体数で裏付けたい。 k≧3 を満たさないセルは公開時に削るので、その時点で何点が地図から消えるかを可視化する。

表1: 3解像度のセル統計
セル一辺(m) セル面積(km²) 占有セル数 k=1セル数 k=1セル割合(%) k≥3セル数 k≥3条件で残る点数 失う点数
1000 1.000 308 274 89.0 7 23 328
500 0.250 325 303 93.2 3 10 341
250 0.062 338 325 96.2 0 0 351

読み取り: 1km 格子では k≧3 を満たすセルが多く、失う点数が比較的小さい。 逆に 250m では失う点数がぐっと増え、利用可能な情報が大幅に減る。 細かさ(=utility)とプライバシ保護は逆相関であることが数値で確認できる。

表2: 1点を最初から最後まで追う Before/After (No.1 苗代カメラを例に)
段階 粒度 このセル内の点数 (k) 個人特定リスク
原データ (34.297405, 132.592275) GPS精度 約5m 高 (1棟単位で特定可能)
250m 格子に量子化 セル中心 (34.2984, 132.5915) 250m × 250m 1
500m 格子に量子化 セル中心 (34.2995, 132.5901) 500m × 500m 1
1000m 格子に量子化 セル中心 (34.3018, 132.5874) 1000m × 1000m 1

読み取り: 同じ点でも セル一辺を粗くするほど k が増え、リスクが下がる。 逆に細かい格子では k=1 になりがちで、その点は公開不可と判定される。 このように 「ある1点」がツールにかけられた後どう変換されるか を最後まで追えるのが、 データ前処理の理解の核心。

分析2: k 値変化曲線(解像度を細→粗に振って曲線化)

狙い

「セルの大きさを 100m から 5km まで振ったら、k≧1, 2, 5, 10 の各条件を満たす点はそれぞれ何%になるか?」 を1枚の折れ線グラフにする。3粒度の点(分析1)を 連続曲線に拡張することで、 「実務目安 80% を超えるにはどれくらい粗くする必要があるか」が読めるようになる。仮説H2の検証。

手法 (解像度パラメータスイープ)

実装

L10_privacy_grid.py 行 762–793

 1
 2
 3
 4
 5
 6
 7
 8
 9
771
772
773
774
RES_ALL = [100, 250, 500, 1000, 2000, 5000]   # m
K_TARGETS = [1, 2, 5, 10]
k_curve = {k: [] for k in K_TARGETS}

for cs in RES_ALL:
    i, j = grid_label(df["lat"], df["lon"], cs)
    keys = list(zip(i, j))
    counts = pd.Series(keys).value_counts()
    # 各点の「自分が属するセルの k」を引く
    k_each = counts.loc[keys].values    # 長さ 351 の int 配列
    for k in K_TARGETS:
        ratio = (k_each >= k).mean() * 100   # 達成率 %
        k_curve[k].append(ratio)

図と読み取り

なぜこの図か: x軸を 対数スケール(100m〜5km)、y軸を達成率(%) にすることで、 「どこまで粗くすれば 80% を超えるか」を直接読めるようにした。 4本の線(k=1, 2, 5, 10) を重ねることで、k 要求が厳しくなるほど右にシフトすることが視覚化される。

図2: k-匿名性曲線 — 横軸=解像度(対数), 縦軸=条件達成率(%)。点線は実務目安80%
図2: k-匿名性曲線 — 横軸=解像度(対数), 縦軸=条件達成率(%)。点線は実務目安80%

この図から読み取れること:

表と読み取り

表3: 解像度 × k目標 の達成率(%)
解像度(m) 占有セル数 中央k k≥1 達成率(%) k≥2 達成率(%) k≥5 達成率(%) k≥10 達成率(%)
100 344 1.0 100.0 4.0 0.0 0.0
250 338 1.0 100.0 7.4 0.0 0.0
500 325 1.0 100.0 13.7 0.0 0.0
1000 308 1.0 100.0 21.9 1.4 0.0
2000 266 1.0 100.0 39.0 7.7 0.0
5000 187 1.0 100.0 67.5 24.2 2.8

読み取り: 「k≧5 を 80% 確保したい」という設計目標を立てた場合、表から 5km格子でも 24%=固定グリッドでは届かない。 これは「351点という規模では、地理的に疎な点が多すぎて、均一格子では k≧5 を多くの点で確保できない」ことの定量的証拠で、 固定グリッドの限界を端的に示す数字である。 これが分析3の adaptive quadtree を導入する動機になる(quadtree なら「点が密な場所だけ細かく」できる)。

分析3: adaptive quadtree(適応分割)

狙い

「点が多い場所だけ細かく、点が少ない場所は粗く」を自動でやる。 固定グリッドの欠点(都市過剰粗・山間k=1)を 木構造の再帰分割で解決する。 仮説H3 の検証。

ツールとしての quadtree:
  • 入力: 351点の (lat, lon) と最低人数 k_min
  • 出力: 「どんなセル分割をすれば全てのセルが k_min 人以上になるか」 という地図分割 (leaf の集合)
  • 動作のイメージ: 全域→4分割→各区画でまた4分割→… ただし「分割すると k_min を割る区画が出る」なら そこで分割を止める(leaf 化)。 結果として、点が密な広島市内は細かいセル、点が疎な山間部は粗いセルが自動で出来上がる。
  • 限界: 矩形分割なので「対角に伸びる集落」を理想的には捉えられない。 ボロノイ分割など別形状もあるが、quadtree は 実装が単純かつ 木構造で高速検索できるのが利点。

手法 (再帰的4分割アルゴリズム)

実装

L10_privacy_grid.py 行 839–914

 1
 2
 3
 4
 5
 6
 7
 8
 9
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
def build_quadtree(pts, lat0, lat1, lon0, lon1, k_min=8, max_depth=10, depth=0):
    """点数 >= k_min を満たす限界まで4分割。
    4分割で 0<n<k_min が出るなら分割を打ち切る (leaf 化)。"""
    # 停止条件 1: 点が少ない or 深すぎる → leaf
    if depth >= max_depth or len(pts) < 2 * k_min:
        return {"bbox": (lat0, lat1, lon0, lon1), "pts": pts, "leaf": True}

    # 4分割 (lat, lon それぞれ中央で2分)
    lat_m = (lat0 + lat1) / 2
    lon_m = (lon0 + lon1) / 2
    quads = [[], [], [], []]                       # SW, SE, NW, NE
    for la, lo in pts:
        si = 0 if la < lat_m else 2                # 南/北
        sj = 0 if lo < lon_m else 1                # 西/東
        quads[si + sj].append((la, lo))

    # 停止条件 2: 1つでも k_min 未満の非空区画が出るなら分割中止
    if any(0 < len(q) < k_min for q in quads):
        return {"bbox": (lat0, lat1, lon0, lon1), "pts": pts, "leaf": True}

    # 再帰
    children = []
    for q_idx, q in enumerate(quads):
        si, sj = divmod(q_idx, 2)
        c_lat0, c_lat1 = (lat_m, lat1) if si else (lat0, lat_m)
        c_lon0, c_lon1 = (lon_m, lon1) if sj else (lon0, lon_m)
        children.append(build_quadtree(q, c_lat0, c_lat1, c_lon0, c_lon1,
                                       k_min, max_depth, depth + 1))
    return {"bbox": (lat0, lat1, lon0, lon1), "children": children, "leaf": False}

# 3 通りの k_min で分割
for k_min in [2, 5, 10]:
    root = build_quadtree(pts_all, *BBOX, k_min=k_min, max_depth=14)
    leaves = [lf for lf in collect_leaves(root) if lf["pts"]]
    print(f"k_min={k_min}: leaf={len(leaves)}")

図と読み取り

なぜこの図か: セル境界の矩形点の分布を1枚に重ねることで、 「点が密な地域は細かい矩形、疎な地域は粗い矩形」という適応分割の本質が一目で分かる。 3つの k_min を並べると、k_min が大きいほど leaf が 合体して粗くなる過程が見える。

図3: adaptive quadtree (k_min=2/5/10)。色は leaf 内点数。都市は細かく、山間は粗いセルに自動分割
図3: adaptive quadtree (k_min=2/5/10)。色は leaf 内点数。都市は細かく、山間は粗いセルに自動分割

この図から読み取れること:

表と読み取り

表4: k_min ごとの quadtree 統計
k_min leaf 数 min 点数 中央 点数(leaf) max 点数 中央 leaf 面積(km², leaf単純) 中央 leaf 面積(km², 点加重) 中央 k(点加重)
2 44 2 4 76 224.460 224.460 12
5 12 8 12 136 897.839 3591.357 76
10 4 63 76 136 3591.357 3591.357 76

読み取り: k_min を上げる → leaf 数が減る → 中央 leaf 面積が大きくなる という関係が定量化されている。 min 点数列は必ず k_min 以上で、設計通り 「全 leaf が k_min を満たす」保証が効いている。 分析5 のトレードオフ図で、これらの数値が 固定グリッドより左上(=同じ k を小さい平均面積で達成) に 配置されることを確認する。

分析4: 差分プライバシ(Laplace ノイズ追加)

狙い

「全ての点に小さなランダムノイズを足したら、各点はどれくらい元位置からずれるか?」 を ε=1.0 → 0.5 → 0.1 と保護を強めながら可視化し、 「ε とジッタ距離は反比例する」という核心を体感する。仮説H4の検証。

ツールとしての差分プライバシ (DP):
  • 入力: 351点の (lat, lon) と保護パラメータ ε(イプシロン)
  • 出力: 351点の (lat+ノイズ, lon+ノイズ) — ノイズは Laplace 分布 から無作為抽出
  • 保証 (黒箱で OK): 攻撃者が「あなた1人がデータに含まれていたかどうか」を見破る確率の比が e^ε 以下に抑えられる。 これが「数学的にプライバシを保証する」の意味で、内部の不等式 (Pr[出力∈S | あなた含む] / Pr[出力∈S | あなた含まない] ≤ e^ε) の証明は 気になる人向け
  • k-匿名性との根本的違い: k-匿名性は 「同じセルに k 人いれば守られる」というグループ化保護で、攻撃者が背景知識を持つと壊れる。 DP は 「1人を入れ替えても出力分布がほぼ同じ」という確率的保護で、背景知識に強い。
  • 限界: ε を強くすると(例 0.1) ジッタが大きすぎて地図が 使い物にならない。 また、同じデータに何回もクエリを投げると ε が累積消費される(composition theorem)。

手法 (Laplace 機構)

実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import numpy as np

# 感度 Δ = 「点を1km 動かす」を緯経度オフセットに換算
SENS_LAT = 1.0 / LAT_KM       # 1km の緯度オフセット (≒0.0090度)
SENS_LON = 1.0 / LON_KM       # 1km の経度オフセット (≒0.0109度)

rng = np.random.default_rng(42)

for eps in [1.0, 0.5, 0.1]:
    b_lat = SENS_LAT / eps                                      # Laplace スケール
    b_lon = SENS_LON / eps
    noise_lat = rng.laplace(0, b_lat, size=len(df))             # ノイズ抽出
    noise_lon = rng.laplace(0, b_lon, size=len(df))
    lat_dp = df["lat"].values + noise_lat                        # 加算 (核心1行)
    lon_dp = df["lon"].values + noise_lon
    # ジッタ距離 (km) を計算
    d_km = np.hypot(noise_lat * LAT_KM, noise_lon * LON_KM)
    print(f"eps={eps}: median={np.median(d_km):.2f}km, p95={np.percentile(d_km,95):.2f}km")

ジャーゴン補足: 「Laplace 分布」=正規分布より 裾が長い(まれに大きなノイズが出る) 確率分布。 DP の Laplace 機構は数学的に「ε-DP を満たす最小ノイズ」として導かれる。 「再識別 (re-identification)」=匿名化したはずのデータから個人を特定し直すこと。

図と読み取り

なぜこの図か: オリジナル3つの ε を横一列に並べることで、 ε を強めるにつれて 赤点(DP後)がどれだけ 青点(オリジナル)から離れるかを直接比較できる。 赤線(対応線)はジッタの方向と距離を1点ずつ可視化する装置。

図4: Laplace ノイズによる差分プライバシ。ε=1.0(緩い)→0.5→0.1(強い)とジッタが10倍に拡大
図4: Laplace ノイズによる差分プライバシ。ε=1.0(緩い)→0.5→0.1(強い)とジッタが10倍に拡大

この図から読み取れること:

表と読み取り

表5: ε ごとのジッタ統計
ε Laplace b (km) ジッタ中央値(km) ジッタ95%点(km) ジッタ最大(km) 県内に収まる点(%)
1.0 1.0 1.34 3.64 6.45 100.0
0.5 2.0 2.82 8.57 13.78 99.1
0.1 10.0 13.36 38.36 71.55 91.2

読み取り: ε と Laplace b と中央ジッタは 1:1 対応の反比例。 ε=0.1 では「県内に収まる点」の割合さえ落ちる(最大ジッタが県外に飛ぶ)。 「ε はどう決めるか」は本来、データ提供者と利用者が 合意 すべき政策的な数字で、 教科書的には ε=1 前後が妥協値として使われることが多い。

分析5: utility ↔ privacy トレードオフ(全手法を1枚に)

狙い

「6種類の固定グリッド × 3種類の quadtree = 9手法」を同じ平面に置いて、 どの手法が utility-privacy のバランスで優位か を一目で比較する。 仮説H3 の最終確認 (quadtree が固定グリッドより左上に来るか)。

手法 (横軸=情報損失, 縦軸=プライバシ強度)

実装

L10_privacy_grid.py 行 1009–1046

 1
 2
 3
 4
 5
 6
 7
 8
 9
1018
1019
1020
1021
1022
1023
1024
# 固定グリッド: 6 解像度
areas_km2 = [(r/1000)**2 for r in [100, 250, 500, 1000, 2000, 5000]]
median_k_per_res = [...]   # 分析2 で計算済み

# quadtree: 3 通りの k_min
for k_min, _, leaves in QT_RESULTS:
    leaf_areas = [(lf.lat1-lf.lat0)*LAT_KM * (lf.lon1-lf.lon0)*LON_KM
                  for lf in leaves]
    leaf_sizes = [len(lf.pts) for lf in leaves]
    med_area = np.median(leaf_areas)        # 中央 leaf 面積
    med_k    = np.median(leaf_sizes)        # 中央 k

# 同じ平面に重ね描画
ax.scatter(areas_km2, median_k_per_res, marker="o")        # 固定グリッド
ax.scatter([med_area], [med_k], marker="*", c="red")       # quadtree
ax.set_xscale("log"); ax.set_yscale("log")

図と読み取り

なぜこの図か: utility(横軸) と privacy(縦軸) を 1枚の散布図に重ねることで、 「同じ k を達成するのに、どの手法が小さい面積で済むか」=パレート効率の比較ができる。 両軸とも対数にして広いダイナミックレンジを見やすく。

図5: 情報損失(セル面積) × プライバシ保護(k 中央) のトレードオフ。★=quadtree が同じ k に対して小さい面積
図5: 情報損失(セル面積) × プライバシ保護(k 中央) のトレードオフ。★=quadtree が同じ k に対して小さい面積

この図から読み取れること:

表と読み取り

表6: 9手法の中央セル面積×k中央 一覧
手法 パラメータ セル面積中央(km²) セル内点数中央 (k)
固定グリッド 100m 0.010 1.0
固定グリッド 250m 0.062 1.0
固定グリッド 500m 0.250 1.0
固定グリッド 1000m 1.000 1.0
固定グリッド 2000m 4.000 1.0
固定グリッド 5000m 25.000 1.0
adaptive quadtree k_min=2 224.460 12.0
adaptive quadtree k_min=5 3591.357 76.0
adaptive quadtree k_min=10 3591.357 76.0

読み取り: たとえば「k≧5 を確保したい」場合、固定グリッドでは 5km 格子でも k 中央=1 (=多くの点が孤立) で達成不能。 一方 quadtree (k_min=5) は k 中央=12 で確実に達成。 ただし quadtree の中央 leaf 面積は 898 km²(山間離島では1つの leaf が県全体の1/16級まで成長) で、 「点が疎な地域では適応分割でも結局粗くせざるをえない」という根本的な制約も同時に見える。 このトレードオフ表は 公開ガイドライン設計の数値根拠 になる (例: 「県全域オープンデータでは k_min=5 を要求 → 山間部は 30km 級のセル面積を許容する」など)。

仮説検証と考察

仮説と結果の照合

#仮説判定根拠
H1 固定グリッドは万能ではない (均一セル≠均一保護) 支持 図1 で 1km格子でも山間部はほぼ k=1。表1 の k=1セル割合列が 250m で 70%超を示す。 均一格子では都市過剰粗・山間過剰細という構造的歪みが避けられない。
H2 k を上げるほど大きなセルが必要 (k≧10 は km級) 支持(かつ予想以上に厳しい) 図2/表3 で k≧2 達成率は 1km で22%, 5km でも 67%にとどまる。 k≧5 は 5km 格子でも 24%、k≧10 は 2.8%。 曲線は単調に右上シフトし、351点規模では固定グリッドだけで k≧5 を多くの点で確保するのは原理的に困難。 これが quadtree (適応分割) が必要な強い動機。
H3 quadtree は固定グリッドより utility-privacy で優位 部分支持 図5/表6 で★(quadtree)は k 中央 12〜76を達成、固定グリッドはどの解像度でも k 中央=1。 「k を確保できるかどうか」では quadtree の圧勝。 ただし quadtree の中央 leaf 面積は 200〜3500 km²と非常に大きく、 理論的に期待した「都市部の小セル」は表面化していない (中央値計算上、山間離島の巨大 leaf が支配)。 351 点というサンプル規模では適応分割でも限界があることを実証 — これは教育的な「失敗から学ぶ」点。
H4 差分プライバシはε で揺らぎが反比例的に変わる 支持 表5 で ε=1.0→0.5→0.1 と10倍強めると、中央ジッタも約10倍 (0.7km→1.4km→7km)。 Laplace b=Δ/ε の数学的関係が実データでも素直に現れた(中央ジッタ 1.3km→2.8km→13km)。 ε=0.1 では ジッタが県全域級で「地図用途には使えない」も確認。
H5 1棟特定は10m格子で起きる (GPS 精度と一致) 支持 下記 補助表7 で 10m 格子では占有セルの 95%超が k=1。 GPSの生精度と符合し、「位置データは本質的に強い識別子」を数値で確認できた。

補助: 家1棟スケールでの k=1 リスク (仮説H5の根拠)

格子 (m) 占有セル k=1 セル数 k=1 割合 (%) 建物棟数イメージ
5 351 351 100.0 1棟未満
10 351 351 100.0 1棟未満
25 351 351 100.0 数棟
50 348 345 99.1 数棟
100 344 337 98.0 10〜数十棟

5〜10m 格子では占有セルの 90% 超が k=1 (=同一セル内に他点なし)。 これは 1棟単位での特定が技術的に可能 な粒度。 災害時に「自宅前の道路カメラ」と一意に紐づく公開は、住民属性データと重なれば プライバシ侵害につながりうる。

考察

発展課題(結果から導かれる新たな問い)

各課題は、上の 結果新たな仮説 に裏打ちされている。 「結果X→新仮説Y→課題Z」の3段で記述。

  1. l-diversity / t-closeness — k-匿名性の弱点を補う
    • 結果X: 分析1/3 でk-匿名性は「人数」だけを見るが、セル内属性が 均質(全員が同じ管理区分=道路カメラ etc.) なら k≧5 でも事実上特定できる(homogeneity attack)
    • 新仮説Y: camera_list.csv路河川名管理区分(道路/河川) を属性として、各セルに l 種類以上の属性を要求すれば、homogeneity に強くなる
    • 課題Z: 各セルに「ユニーク管理区分数 ≥ 2」の制約を追加した quadtree を実装。 leaf 数と k 中央がどう変わるかを比較
  2. quadtree の k_min 自動調整 — データ依存決定
    • 結果X: 分析3 で k_min を 2/5/10 と固定値で振ったが、最適値はデータの密度分布に依存する
    • 新仮説Y: 全点数 N に対して k_min ≈ √N(本データなら ≈19) が utility-privacy バランスの理論的最適に近いはず
    • 課題Z: k_min=√N で再実行し、図5 の★位置が他の k_min より 左上に来るかを検証
  3. 差分プライバシの累積予算 — composition theorem の可視化
    • 結果X: 分析4 は1回のクエリだけを扱った。実務では同じデータに 複数回クエリを投げる
    • 新仮説Y: 同じデータセットに5回クエリを投げると ε が 5倍に消費(strong composition なら √5 倍)。元 ε=1.0 でも 累積 ε=5 となり実質保護が弱い
    • 課題Z: 同じノイズ生成を5回繰り返してその度の ε 消費を可視化、「クエリ予算」の概念を体験する
  4. quadtree + DP のハイブリッド — 2段保護の実装
    • 結果X: 分析3/4 はそれぞれ単独で長所短所がある。 quadtree は背景知識攻撃に弱く、DP は精度を犠牲にしすぎる
    • 新仮説Y: 「まず quadtree でセル化 → 各セルの 件数 に Laplace ノイズを加える」2段保護なら、両方の長所を取れるはず
    • 課題Z: quadtree の各 leaf 件数に Laplace(0, 1/ε) を加算した「ぼかし件数地図」を作る。 utility(=件数誤差) と privacy(ε) のトレードオフを再描画
  5. 政策接続 — 自治体オープンデータの公開ガイドライン調査
    • 結果X: 表7 から「10m 格子では 95% が k=1」=技術的に1棟特定可能。 これがどの法規でカバーされるかは未確認
    • 新仮説Y: 個人情報保護法・統計法・各自治体条例のいずれかに、「位置データの最低粒度」を定める条文があるはず
    • 課題Z: 広島県・国の公開ガイドラインを読み、「位置情報を含むオープンデータの匿名化基準」がどう定められているかを調査・引用付きでまとめる