本文へ
Archival Packager

Archival Packager 技術資料

個人情報(PII)候補検出の精度と限界

最終更新: 2026年9月12日

このアプリは、受け入れた資料の中から個人情報の候補を探して一覧(pii-report.csv)に出します。 その検出がどれくらい当たるのかを実際に測った結果と、測っても直らない限界をここにまとめます。

最初に結論。検出が 0 件でも、個人情報が無いことの証明にはなりません。 この機能は「人が確認すべき箇所の候補出し」です。公開可否の判断そのものには使えません。


1. 何を検出しているか

検出する種別は次の 5 つだけです。

種別 判定のしかた
マイナンバー 12 桁の数字で、検査用数字(12 桁目)が規定どおり
クレジットカード番号 13〜19 桁の数字で、Luhn の検査を通り、かつ実在ブランドの先頭桁(3/4/5/6、または 2221〜2720)
メールアドレス 名前@ドメイン.トップレベル の形
電話番号 0 で始まりハイフンか括弧で区切られた、合計 10 桁または 11 桁の数字
郵便番号 〒 付きなら 7 桁(ハイフン任意)、〒 無しなら 3 桁‐4 桁

次のものは検出対象に入っていません。 氏名、住所、生年月日、銀行口座、被保険者番号、旅券番号、 運転免許証番号、家族構成、病歴。 ストアの説明文(「個人情報の候補の検出(マイナンバー、クレジットカード番号など)」)を読むと もっと広く見ているように受け取れますが、実際に見ているのは上の 5 つです。

検出した値は必ずマスクして出します(末尾 4 桁だけ、郵便番号は全桁伏せ、メールはローカル部の先頭 1 文字だけ)。 レポート自体が漏洩源にならないようにするためです。

2. どうやって測ったか

tests/fixtures/pii/cases.jsonl に、正解ラベルを付けた 119 行を用意しました。 1 行が 1 件の文面で、「この行から何が検出されるべきか」を書いてあります。

内訳:

区分 件数 中身
陽性(検出されるべき) 59 5 種別の記載。うち 6 件は「取れないと分かっている表記」(§5)
陰性(検出されてはいけない) 44 整理番号・請求記号・年度+連番・ISBN・頁範囲・和暦の日付など
敵対的な陰性 5 検査用数字や Luhn を偶然通ってしまう別種の番号(§4)
未対応種別 11 住所・氏名・生年月日・口座など、そもそも見ていないもの

表記のゆれも入れてあります。全角数字、全角@、ハイフンあり/なし、スペース区切り、タブ区切り、 括弧書きの市外局番、縦書き資料の OCR でハイフンが長音符(ー)や別種のダッシュ(‐ ‑ – −)に化けたもの。

測り方は 1 行ごとの多重集合の突き合わせです(同じ行に同じ種別が 2 件あるケースも数え分けます)。 テストは tests/test_pii_accuracy.py。uv run pytest -q tests/test_pii_accuracy.py で再現できます。

3. 実測値(2026年9月12日)

種別 正解 見逃し 誤検出 適合率 再現率 F値
マイナンバー 13 1 2 0.8667 0.9286 0.8966
クレジットカード番号 12 2 3 0.8000 0.8571 0.8276
メールアドレス 12 0 0 1.0000 1.0000 1.0000
電話番号 18 3 0 1.0000 0.8571 0.9231
郵便番号 9 0 0 1.0000 1.0000 1.0000
合計 64 6 5 0.9275 0.9143 0.9209

適合率 = 検出したもののうち本当に個人情報だった割合。 再現率 = 本来検出すべきもののうち実際に検出できた割合。

参考として、同じ 119 行を今回の修正前の実装で測ると 適合率 0.7313 / 再現率 0.7000 / F値 0.7153 でした(誤検出 18 件、見逃し 21 件)。 主な原因は、全角数字と OCR のダッシュを一切拾えなかったこと、 カード番号の一部を電話番号として二重に報告していたこと、 ISBN をカード番号として報告していたことです。

今回直したこと

  1. 全角・互換文字の正規化(NFKC)。全角数字・全角@・全角ハイフンで書かれた申請書を取りこぼしていました。
  2. OCR で化けたダッシュ(‐ ‑ ‒ – — ― − ー)をハイフンとして扱うようにしました。
  3. カード番号に先頭桁の条件を追加。Luhn は 10 回に 1 回は偶然通るため、 ISBN-13(978/979 始まり)のおよそ 1 割がカード番号として報告されていました (ISBN-13 の検査用数字と Luhn は別の計算なので、全部が通るわけではありません)。
  4. 電話番号の桁数を 10 桁か 11 桁に限定し、代わりに 3 桁終わり(フリーダイヤル 0120-123-456)と 括弧書き((03)1234-5678、03(1234)5678、03 - 1234 - 5678)に対応しました。 12 桁の文書番号 0123-4567-8901 を電話番号と誤認しなくなります。
  5. 長い数字列の一部を切り出さないようにしました。24 桁の整理番号の先頭 16 桁が 偶然 Luhn を通ってカード番号として報告される、という切り出し方をしていました。
  6. 郵便番号の前後の語を見るようにしました。「第 003-0045 号」「頁 113-0033」は 郵便番号と同じ形で、目録には郵便番号よりこちらの方がよく出てきます。
  7. 〒 付きならハイフン無しの 7 桁も拾うようにしました(〒 があれば別物の可能性が低いため)。

誤検出と見逃しのどちらに倒したか

見逃しを減らす方向に倒しています。 この機能は「人が確認する候補を出す」ものなので、 出てきたものを人が捨てるのは容易ですが、出てこなかったものには誰も気づけないからです。

ただし再現率を上げるために誤検出を増やす変更はしていません。 上の 1〜7 のうち、3・5・6 は誤検出だけを減らすもの、1・2・4・7 は表記のゆれを吸収して 見逃しだけを減らすものです。両者を取り引きした箇所はありません。

一方で、次の 2 つは「誤検出を減らせるが見逃しが増える」ため採用しませんでした。

4. 誤検出の中身(5 件)

すべて「検証を偶然通ってしまう別種の番号」です。これは原理的に選り分けられません。

文面 何と誤認するか なぜ避けられないか
整理番号 111111111118 マイナンバー 任意の 12 桁は約 10 回に 1 回、検査用数字を満たす
資料 ID 202600000006 マイナンバー 同上。年度+連番でも起こる
JAN コード 4901234567894 カード番号 13 桁の数値コードは約 10 回に 1 回 Luhn を満たす。JAN の先頭 49 はカードの先頭桁と重なる
法人番号 5000000000005 カード番号 同上
伝票番号 4900000000007 カード番号 同上

12 桁・13 桁の連番を大量に含む台帳を受け入れると、その 1 割前後が候補として出ます。 候補一覧が長いこと自体は異常ではありません。

確率の根拠(2026-09-12 に 30 万件の乱数で実測):

条件 実測 理屈
任意の 12 桁が検査用数字を満たす 9.89% 検査用数字は 11 を法として求めるが、10・11 は 0 に写すので取りうる値は 0〜9 の 10 通り。末尾 1 桁が偶然一致する確率は 1/11 ではなく 1/10
任意の 13 桁が Luhn を満たす 10.11% 同じく 1/10
有効な ISBN-13 が Luhn も満たす 10.05% ISBN-13 の検査用数字は重み 1,3 の mod 10 で、Luhn(交互 2 倍の mod 10)とは別の計算。したがって ISBN だからといって通るわけではなく、偶然一致する分だけ通る

「ISBN はカード番号として報告される」ではありません。 報告されるのは ISBN のうち およそ 1 割です。ここを「ISBN は通る」と書くと、検査の性質を誤って伝えることになります。

5. 検出できないもの

仕組み上できないもの

表記のせいで取れないもの(評価データに含めて固定済み)

そもそも見ていないもの

氏名、住所、生年月日、本籍、銀行口座、被保険者番号、旅券番号、運転免許証番号。 日本語の住所や氏名を正規表現で拾うと誤検出が実用に耐えない量になるため、入れていません。 資料の大半の個人情報はこの区分に入ることに注意してください。

6. この数字をどう読むべきか

7. 数字を更新するとき

評価データを増やしたり実装を直したときは、次の順で行ってください。

  1. tests/fixtures/pii/cases.jsonl に行を足す(正解ラベル付き、合成データのみ)
  2. uv run pytest -q tests/test_pii_accuracy.py で測る
  3. uv run python tests/test_pii_accuracy.py を直接実行すると種別ごとの表(TP / FP / FN 付き)が出ます
  4. tests/test_pii_accuracy.py のしきい値と、この文書の §3 を同時に直す

しきい値は実測値より少し低く置いてあります。上げるためではなく、下がったときに気づくためのものです。