集計値だけで個人のデータは復元できるか

公開された集計値だけを使って、個人単位のデータベースを再構築するデモの結果です。攻撃側は元データを一度も読みません。使用しているデータはすべて人工的に生成したもので、実在の人物の情報は含みません。

回答者 20 人(属性の組み合わせは 288 通り)の人工データから 483 個の集計値を公開したところ、元のデータベースは候補 1 通りに確定し、全員の回答が復元されました。

表を重ねて、公表されていない値を絞り込む

全校児童 40 人の小規模校です。こういう学校は各地に実在し、 大規模校と同じ種類の表を同じように公表します。ここで使うのは学校要覧・安全計画・ 就学支援状況報告に載る種類の2 属性クロス集計 10 枚 (120 セル)だけ。加工は一切なく、就学援助と掛けるのは学年・地区までです。

出典
表1学年 × 性別学校要覧「学年別児童数(男女別)」
表2学年 × 地区学校要覧「通学区域別の在籍者数」
表3地区 × 通学方法安全計画「地区別の通学手段」
表4学年 × 通学方法安全計画「学年別の通学手段」
表5学年 × きょうだい学校評価資料「在校生にきょうだいがいる児童数」
表6性別 × 通学方法安全計画「男女別の通学手段」
表7性別 × 地区学校要覧「男女・地区別」
表8地区 × きょうだい学校評価資料「地区別のきょうだい在籍」
表9学年 × 就学援助就学支援状況報告「学年別の対象者数」
表10地区 × 就学援助就学支援状況報告「地区別の対象者数」

絞り込みの筋道

絞り込む値は「学年=5年・性別=男・地区=港」の就学援助受給者数です。 該当する児童は 1 人。 この値はどの表にも載っていません。 0 人から順に仮定して、ありえないものを潰していきます。

仮定判定根拠にした公表統計
0 人矛盾「学年 × 地区」より 学年=4年・地区=港 = 1 人
「学年 × 地区」より 学年=5年・地区=港 = 1 人
「学年 × 地区」より 学年=6年・地区=港 = 2 人
「性別 × 地区」より 性別=女・地区=港 = 0 人
ほか 3 件
1 人矛盾しない唯一残る候補

→ 受給者数は 1 人に確定。 つまり全員が受給です。 この条件に当てはまる児童は校内でただ 1 人なので、「その 1 人が誰か」を知っている人にとって、本人の就学援助の状況がそのまま分かったことになります。

ここでいう「矛盾」は比喩ではありません。 ある値を仮定して制約を全部並べ、それを満たす児童の配置が 1 つも存在しないこと (INFEASIBLE)をソルバが証明しています。さらに、その矛盾を導くのに実際に要った統計だけを 残すよう削り込んでいるので、上の表の「根拠」は名指しできる最小の集合です。

同じことが何件起きているか

同じやり方で洗い出すと、この学校では 86 通りの組み合わせについて値が確定します。 組み合わせは互いに重なるので、実際に状況が割れたのは 29 / 40 人 (72%)です。 うち受給していることが割れた児童は 1 人 (2 通りの組み合わせが同じ児童を指しています)。 公表セルそのものと、単一の公表セルから自明に導けるものは除いた数です。

のべ人数で数えると 134 人になりますが、これは 全校人数を超えます。同じ児童が複数の組み合わせで確定するためで、実人数ではありません。

下の「手数」は、矛盾で潰した候補の数です。人数の多いグループほど候補が多く、 何段階も場合分けして絞り込むことになります。

条件人数結果手数使った表
学年=5年・性別=男・地区=港1全員が受給1地区 × 就学援助、学年 × 地区、学年 × 就学援助、性別 × 地区
学年=5年・地区=港・きょうだい=なし1全員が受給1地区 × きょうだい、地区 × 就学援助、学年 × 地区、学年 × 就学援助
学年=2年・地区=中央・きょうだい=あり4全員が非受給4地区 × 就学援助、学年 × 地区、学年 × 就学援助
学年=2年・性別=男・地区=中央3全員が非受給3地区 × 就学援助、学年 × 地区、学年 × 就学援助
学年=2年・地区=中央・通学方法=徒歩3全員が非受給3地区 × 就学援助、学年 × 地区、学年 × 就学援助
学年=5年・地区=中央・通学方法=徒歩3全員が非受給3地区 × 就学援助、学年 × 地区、学年 × 就学援助
学年=4年・性別=男・地区=中央3全員が非受給3地区 × 就学援助、学年 × 就学援助
学年=4年・地区=中央・通学方法=徒歩3全員が非受給3地区 × 就学援助、学年 × 就学援助
学年=3年・地区=東・きょうだい=なし3全員が非受給3地区 × 就学援助、学年 × 就学援助
学年=4年・地区=中央・きょうだい=なし3全員が非受給3地区 × 就学援助、学年 × 就学援助
学年=4年・性別=男・きょうだい=なし3全員が非受給3学年 × 就学援助
学年=3年・通学方法=徒歩・きょうだい=あり3全員が非受給3学年 × 就学援助

データ生成の seed は選んでいます。受給側が割れる例のほうが実害が伝わりやすいためです。複数の seed で同じことが起きるかは uv run python experiments/deduction.py --sweep 3,5,7,11,13,16,18,19,20,21,22,23,24 で確かめられます(時間がかかるので CI では回していません)。

学校の例 — 何が「安全そう」で、どこで崩れるのか

生徒 400 人の中学校です。 統計への加工は一切ありません— 丸めもノイズも入れず、正確な人数を そのまま公表します。公表するのは学校要覧・部活動報告・就学支援状況報告などに実際に 載っていそうな表だけです。センシティブな属性は就学援助の受給有無としました。

全校で公表する 15 枚

#出典
1学年 × クラス学校要覧「学級編制」
2学年 × クラス × 性別学校要覧「学級編制(男女別)」
3学年 × 出身小学校学校要覧「入学者の状況」
4学年 × クラス × 出身小学校学級編成資料
5学年 × 通学方法安全指導計画「通学手段の状況」
6出身小学校 × 通学方法安全指導計画「校区別の通学手段」
7学年 × クラス × 通学方法安全指導計画「学級別の通学手段」
8学年 × 部活動部活動報告「加入状況」
9性別 × 部活動部活動報告「男女別加入状況」
10学年 × クラス × 部活動部活動報告「学級別加入状況」
11学年 × 性別 × 部活動部活動報告「学年・男女別加入状況」
12学年 × 就学援助就学支援状況報告「学年別」
13性別 × 就学援助就学支援状況報告「男女別」
14出身小学校 × 就学援助就学支援状況報告「地域別」
15通学方法 × 就学援助就学支援状況報告「通学手段別」

就学援助が出てくるのは、学年別・男女別・地域別・通学手段別という粗い表だけ です。個人名も、個人を指す欄もありません。

結果1: この 15 枚からは、誰も特定できません

4 属性以内で校内に 1 人しかいない生徒——つまり周囲の人が個人を言い当てられる 生徒——は 121 人います。それでも 検査した 121 人の受給状況は 1 人も確定しません (反例で棄却 94 人 / 上下限を解いて証明 27 人 / 判断できなかった 0 人)。

「これなら安全そう」という直感は、この粒度では正しい のです。

結果2: 粒度を学級まで細かくしても、学校の文書の範囲では復元できません

同じ学校の 3年B組(33 人)について、担任や 学年団が実際に作りそうな表を 12 枚 (77 セル)集めます。就学援助は人数だけで、他の属性と 掛けた表は入れません—そんな表は作らないし、作れば問題になるからです。

結果は候補 >200 通り、 正解率 0.22。 復元できません。

結果3: 網羅的に集計を出すと復元されます(これは学校の話ではありません)

同じ 33 人に対して、属性の組み合わせを 3 個まで網羅的に 公表した場合(41 枚 / 771 セル)、 候補 1 通り・ 正解率 100% となり、全員の回答が 就学援助の受給状況まで含めて確定します。

ただし学校がこんな集計を作ることはありません。効いている表を調べると 「通学方法 × 委員会」「部活動 × 委員会 × 就学援助」といったものが並びます。誰も作らない表です。 (771 セルのうち実際に効いているのは 71 セル・26 枚でした。)

では現実にどこが危ないのか。統計機関の小地域集計です。 国勢調査の町丁字別集計や米国センサスのブロック別集計は、数百人規模の地域に対して属性の 組み合わせを網羅的に公表します。2020 年の米国センサスで再構築攻撃が現実の問題になったのは、 まさにこの「網羅的な公表」が実際に行われているためでした。

まとめ

公表の仕方人数セル結果
全校(学校要覧など)40015267誰も特定できず
3年B組(学級資料)331277復元できず
3年B組(網羅的な集計)3341771全員の回答が確定

危険なのは「小さい集団」そのものではなく、 小さい集団に対して網羅的な集計を出すことです。学校が出す程度の枚数なら、 粒度を細かくしても復元されません。一方、統計機関の小地域集計は網羅性の側で危険域に 入ります。

どう解いているのか

公開されているのは「〜な人が何人」という集計値だけです。それがなぜ 個人単位のデータに戻ってしまうのか、手順は 3 つしかありません。

  1. 人ではなく「レコード型」を変数にする。 属性の組み合わせは全部で 288 通りあります。その各々に何人いるかを未知数 n1, …, n288 と置きます。人を 1 人ずつ変数に すると、並び替えただけの同じ答えが大量に出てしまうためです。
  2. 集計値をそのまま方程式に直す。 「region=Chiba かつ occupation=Engineer が 3 人」なら、その条件に当てはまるレコード型の 変数をすべて足して = 3 と書きます。公開された集計値の数だけ式ができ (このページでは 483 本)、さらに「全員で 20 人」という式が 1 本 加わります。
  3. ソルバに整数解を列挙させる。 すべての式を同時に満たす非負整数の組を OR-Tools CP-SAT で探します。答えが 1 通りしか なければ、元のデータベースが確定したことになります。

ポイントは、集計値を 1 つ公開することは、方程式を 1 本渡すことと同じ だということです。式が変数の数に追いついてくると、解は一気に絞られます。

4 人・2 属性まで縮めて手計算で追える最小例を uv run python -m src.tutorial で実行できます。

実験の設定

項目
回答者数20
スキーマdemo6
データ生成の seed20240501
統計生成の seed7
公開したクロス集計の次数1 属性、2 属性、3 属性
公開した集計値の数483
クロス集計表の数41
うち人数 0 のセル142 個(29%)
統計への加工加工なし(正確な集計値)
ソルバortools-cpsat

クロス集計を深くすると、どこまで漏れるか

同じデータを、公開するクロス集計の深さだけ変えて 3 通り試した結果です(実験 E1〜E3)。確定した人数は、どのレコード型に属するかがソルバによって証明された人の割合です。

実験公開範囲集計値の数候補DB数確定した人数正解率一意か
E11 属性のみ16>1000%0.032いいえ
E21〜2 属性121>1005%0.451いいえ
E31〜3 属性4831100%1.000はい

1 属性の集計(いわゆる単純集計)だけならまだ安全に見えますが、3 属性のクロス集計まで出した時点で候補は 1 通りになり、全員の回答が復元されます。

集計値が増えると、候補はどう減るか

「候補DB数」は、公開されたすべての集計値と矛盾しないデータの作り方の数です。これが 1 になった時点で元データが確定します。数え尽くせた場合は厳密な数、そうでない場合は証明済みの下限(>N)だけを載せています— 推測値は一切出していません。

低い次数から順に公開した場合

1 属性 → 2 属性 → 3 属性の順に公開していく、いちばん自然な出し方です。

公開した集計値の数候補DB数復元の進み方正解率
0>1000%0.000*
9>1000%0.078*
27>1000%0.072*
53>1000%0.190*
83>1000%0.307*
119>1005%0.422*
159>1005%0.431*
204765%0.536
2521050%0.740
305855%0.750
361670%0.800
4201100%1.000
4831100%1.000

集計値 420 個の時点で一意に確定

ランダムな順に公開した場合

同じ集計値を、順番だけランダムにしたものです。

公開した集計値の数候補DB数復元の進み方正解率
0>1000%0.000*
9>1000%0.025*
27>1000%0.086*
53>10010%0.522*
831855%0.783
1191100%1.000
1591100%1.000
2041100%1.000
2521100%1.000
3051100%1.000
3611100%1.000
4201100%1.000
4831100%1.000

集計値 119 個の時点で一意に確定

何が確定し、何がまだ曖昧なのか

候補が複数残っているとき、「複数あります」で終わらせません。どの候補でも一致する事実と、候補によって変わる事実を機械的に切り分けます。候補データベース 1 通りすべてにわたる範囲。レコード型まで確定した人数は 20 人(100%)。

確定した事実

条件人数出どころ
disease_b=013公開済み
gender=F13公開済み
age_group=30s10公開済み
disease_a=010公開済み
disease_a=110公開済み
occupation=Admin8公開済み
occupation=Sales8公開済み
disease_b=17公開済み

曖昧さは残っていません。公開統計だけでデータベースが完全に確定します。

効いていたのはどの統計か

「統計をたくさん出しすぎたから」なのか、「特定の組み合わせが効いたから」なのかを切り分けます。

公開統計から 1 つずつ取り除いて解き直しました(483 個を検証)。結果: 単独で決定的な統計は 1 つもありません。どれか 1 つを外しても、残りだけで一意に再構築できてしまいます。つまり「この 1 表さえ出さなければ安全だった」という表は存在しません。

逆に、貪欲に削れるだけ削ると、483 個の統計は 46 個(1 属性の表 9 個、2 属性の表 33 個、3 属性の表 4 個)まで減らせて、それでも一意に再構築できます(検証済み: はい)。この部分集合はどれ 1 つ外しても成立しなくなります。

つまり原因は「数が多かったこと」でも「特定の 1 表」でもなく、ある特定の組み合わせです。公開する表の数を数えているだけでは気づけません。

よくある開示制御は効くのか

公開する数値を丸めたり、ノイズを載せたり、人数の少ないセルを伏せたりする定番の対策を、同じ攻撃にかけて比べました。平均誤差は公開値が真の値からどれだけずれたか(=利用者が被る不便さ)、正解率は攻撃者が復元できた割合です。

正解率は、ソルバが列挙した候補の平均です。候補を数え尽くせた行に限り、攻撃者が残った候補から 1 つを無作為に選んだときの期待値にあたります。数え尽くせなかった行(* 印)は、ソルバが先に出した候補だけの平均であり、候補空間上の期待値ではありません。最良は最も当たっている候補ですが、元データを持たない攻撃者にはどれが最良かを見分ける手段がありません。

ケース B — 丸め

公開方式平均誤差候補DB数正解率最良完全一致備考
加工なし0.0011.0001.000はい
2 の倍数に丸め0.4511.0001.000はい
3 の倍数に丸め0.5840.6130.650いいえ
5 の倍数に丸め1.0010.1500.150いいえ一意だが不正解
10 の倍数に丸め1.47>500.093*0.150いいえ

ケース C — ランダムノイズ

公開方式平均誤差候補DB数正解率最良完全一致備考
加工なし0.0011.0001.000はい
ガウスノイズ σ=0.50.3011.0001.000はい
ガウスノイズ σ=10.7110.5000.500いいえ一意だが不正解
ガウスノイズ σ=21.4530.3170.350いいえ
ガウスノイズ σ=42.9260.3000.300いいえ

ケース D — 差分プライバシー(Laplace 機構)

公開方式平均誤差候補DB数正解率最良完全一致備考
加工なし(ε = ∞)0.0011.0001.000はい
Laplace ε=0.1392.3310.0000.000いいえ一意だが不正解
Laplace ε=0.578.45100.0450.050いいえ
Laplace ε=139.24>500.078*0.100いいえ
Laplace ε=57.85>500.162*0.200いいえ
Laplace ε=201.9540.4500.450いいえ
Laplace ε=1000.3011.0001.000はい

プライバシーモデル: 隣接関係は「add/remove one record (unbounded DP)」、1 つの表の L1 感度は 1、予算は 41 個の表へ逐次合成で均等配分。注意: 回答者数 N は正確に公開しており、この ε の保証には含まれません。再構築攻撃の研究では標準的な設定ですが、前提であることに変わりはありません。

ケース E — 小セル秘匿

公開方式平均誤差候補DB数正解率最良完全一致備考
秘匿なし0.0011.0001.000はい
1 人以下を秘匿(ルール非公表)0.0011.0001.000はい
1 人以下を秘匿(ルール公表)0.0011.0001.000はい
2 人以下を秘匿(ルール非公表)0.0090.8561.000はい
2 人以下を秘匿(ルール公表)0.0020.9501.000はい
3 人以下を秘匿(ルール非公表)0.00>500.326*0.550いいえ
3 人以下を秘匿(ルール公表)0.00>500.512*0.800いいえ
5 人以下を秘匿(ルール非公表)0.00>500.125*0.200いいえ
5 人以下を秘匿(ルール公表)0.00>500.135*0.250いいえ

秘匿は数値を歪めないので平均誤差は 0 のままです。代償は誤差ではなく、公表できるセルが減ること自体にあります。また「ルール公表」の行のほうが候補が少ない点に注目してください。秘匿ルールが公表されていると、あるセルが伏せられている事実そのものが「そのセルは k 人以下」という情報になり、伏せたセルの数だけ上限の制約が増えるためです。秘匿は情報を消しきれていません。

読み取れることが 3 つあります。小さな加工はほとんど効きません(丸め幅 2 や σ=0.5 のノイズでは、完全な復元がそのまま成立します)。効く強さまで上げると、公開値そのものが使い物にならなくなります(この表が平均誤差と正解率を並べているのは、その二者択一を同時に見るためです)。そして数値を歪める対策より、セルを伏せる対策のほうがよく効きます— ただし伏せたこと自体が情報になるので、消えてはいません。

次にどの統計を手に入れるかで、必要な数が変わる

どの行も同じ公開統計の集合から、同じ 16 個を出発点にして、1 回に 1 個ずつ足していきます(最大 140 回)。回数を使い切った場合は、確定した数ではなく下限として書いています。

統計の選び方一意になるまでに必要な数最終的な候補DB数正解率
ランダムな順に足す12611.000
低い次数から順に足す>156>300.500
情報利得が最大の統計を選ぶ5311.000

同じ集計値の集合でも、賢く選べば必要な数は数分の 1 になります。「公開した集計値は数百個だけだから安全」という理屈が成り立たないのはこのためです。

このデモが保証していないこと

自分の主張を壊しにいって見つかった限界です。都合の悪い結果も含めて載せます。

生データ

このページの数値はすべて以下のファイルから来ています。なお、元の個票データは意図的に公開していません

ファイル内容
exact.jsonケース A: 全実行結果(1 つずつ外した検証を含む)
rounding.jsonケース B: 丸めの比較
noise.jsonケース C: ランダムノイズの比較
differential_privacy.jsonケース D: 差分プライバシーの比較とプライバシーモデル
suppression.jsonケース E: 小セル秘匿の比較
school.json学校シナリオ: 全校集計と学級別集計の比較
deduction.json小規模校: 確定してしまう値の一覧と絞り込みの筋道
strategies.json統計の選び方の比較
progression.csv候補の縮小過程(1 ステップ 1 行)
strategies.csv選び方ごとの各ラウンド(1 ラウンド 1 行)
reconstructed.csv再構築された個票
statistics.json攻撃者に渡した公開統計そのもの
schema.json攻撃者に渡したスキーマ

再現性

このページの各実行に使った seed・データ設定・クエリ集合・ソルバ設定です。同じ設定なら同じ結果になります。

{
  "exact": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 100,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "rounding": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 50,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "noise": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 50,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "differential_privacy": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 50,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "suppression": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 50,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "school": {
    "config": {
      "n_students": 400,
      "dataset_seed": 20260401,
      "attributes": [
        "学年",
        "クラス",
        "性別",
        "出身小学校",
        "通学方法",
        "部活動",
        "委員会",
        "就学援助"
      ],
      "sensitive": "就学援助",
      "noise": "なし(正確な集計値)",
      "class": "3年B組"
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 100,
      "time_limit": 25.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "deduction": {
    "config": {
      "n_students": 40,
      "dataset_seed": 20,
      "attributes": [
        "学年",
        "性別",
        "地区",
        "通学方法",
        "きょうだい",
        "就学援助"
      ],
      "sensitive": "就学援助",
      "noise": "なし(正確な集計値)",
      "n_record_types": 576,
      "aid_recipients": 6,
      "seed_was_chosen": true
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 100,
      "time_limit": 10.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  },
  "strategies": {
    "config": {
      "schema_preset": "demo6",
      "n_records": 20,
      "dataset_seed": 20240501,
      "correlation": 0.6,
      "levels": [
        1,
        2,
        3
      ],
      "include_zeros": true,
      "max_statistics": null,
      "statistics_seed": 7,
      "noise": {
        "kind": "none",
        "base": 5,
        "sigma": 1.0,
        "epsilon": 1.0,
        "clamp_negative": false
      },
      "noise_label": "加工なし(正確な集計値)",
      "suppress_at_or_below": null,
      "disclose_suppression_rule": false
    },
    "solver_config": {
      "solver": "ortools-cpsat",
      "mode": "auto",
      "max_solutions": 30,
      "time_limit": 60.0,
      "random_seed": 0,
      "representation": "record-type frequency"
    }
  }
}

まとめ

攻撃者が使ったのは、すべて「〜な人が何人」という集計値です。どれ 1 つとして、単体では個人を指していません。それでも十分な数を重ねて公開すれば、個人単位の回答はそのまま復元されます。

集計値であることと、個人が守られていることは同じではありません。公開する統計を評価するときは、1 つ 1 つが匿名かどうかではなく、公開済みのものと組み合わさったときに何が決まってしまうかを見る必要があります。