导出理赔数据,却认不出是谁
目标
把合成的实损型索赔数据导出用于分析。去除直接标识符,泛化日期,归并小地区和罕见的值,使 k 提高到 5 以上,然后亲自尝试重新识别,并留下导出记录。
为什么重要
即使删掉姓名和会员编号,只要出生日期、性别、邮政编码还在,大多数人都能找到自己的那一行。在统计出准标识符组合指向多少人(k)之前,什么都没有得到证明。 提高 k 的办法只有把值模糊化或者去掉行,而顺序颠倒的话,被舍弃的行会像滚雪球一样增加。所以先做泛化,抑制放到最后。 作为检查清单的范本,使用的是美国 HIPAA 的 45 CFR 164.514 Safe Harbor 的 18 类,但这项规定并不直接适用于韩国的保险公司。韩国的标准是《个人信息保护法》,其中健康信息作为敏感信息,要求更严格地处理。
步骤
- 创建并运行 /root/deid/gen_claims.py,生成 /root/deid/claims.csv(700 笔以上,会员 300 名以上)和 /root/deid/zip_pop.csv。要包含:90 岁以上投保人的索赔 5 笔以上、出现次数不足 5 次的诊断代码 3 种以上、人口在 20000 人以下的地区 2 个以上。
- 用 /root/deid/strip.py 去除直接标识符,生成 /root/deid/stripped.csv。rec_id 按原始顺序从 R-000001 开始编号。
- 用 /root/deid/kcount.py 统计准标识符组合的 k,并写入 /root/deid/k_before.json(qi、rows、groups、k_min、unique_rows、lt5_rows)。
- 用 /root/deid/generalize.py 泛化日期,生成 /root/deid/generalized.csv。出生日期只保留年份,以基准年份 2026 计算的年龄超过 89 的,连年份也删除,并把 age_over_89 设为 Y。索赔日期只保留年份,入院日期去掉。
- 用 /root/deid/suppress.py 生成 /root/deid/suppressed.csv。邮政编码只保留前三位,但人口在 20000 人以下的地区改为 000,出现次数不足 5 次的诊断代码改为 RARE,并去掉城市名称。
- 用 /root/deid/kanon.py 抑制组合不足 5 笔的行,生成 /root/deid/export.csv 和 /root/deid/k_after.json。剩余的行必须在 70% 以上。
- 用 /root/deid/reidentify.py 尝试重新识别,生成 /root/deid/attack.json。对象是导出文件中 rec_id 最小的那一行。
- 留下 /root/deid/export_record.json 和 /root/deid/checklist.md。哈希和笔数要从实际文件中测得,检查清单中要同时写出依据和适用范围。
参考
- claims.csv 的表头:claim_id,member_id,name,birth_date,sex,zip5,city,claim_date,admit_date,dx_code,amount,hospital
- stripped.csv 的表头:rec_id,birth_date,sex,zip5,city,claim_date,admit_date,dx_code,amount
- generalized.csv 的表头:rec_id,birth_year,age_over_89,sex,zip5,city,claim_year,dx_code,amount
- suppressed.csv 和 export.csv 的表头:rec_id,birth_year,age_over_89,sex,zip3,claim_year,dx_code,amount
- 准标识符在第 3 步是 birth_date、sex、zip5,从第 6 步起是 birth_year、age_over_89、sex、zip3、claim_year。
- 年龄按 2026 减去出生年份计算(本实验的假设)。诊断代码是形如 DX-A10 的合成代码,人名、地区、医院名称也全部是合成的。
- 评分器在每一步都会根据上一步的文件重新计算这一步的结果并逐行核对,k 也会重新统计。
- 常见错误:先做抑制,导致一半以上的数据被舍弃;保留入院日期;在检查清单中原样抄录原始日期。
创建合成索赔快照和地区人口表
创建并运行 /root/deid/gen_claims.py,生成 /root/deid/claims.csv 和 /root/deid/zip_pop.csv。
让同一个人多次提出索赔,并把出生日期散布到具体的日。这样才会出现组合中只有自己一个人的行。还要放入 90 岁以上的投保人、人口较少的地区,以及只偶尔出现的诊断代码,后面的步骤才有可处理的对象。
去除直接标识符
用 /root/deid/strip.py 生成 /root/deid/stripped.csv。去掉姓名、会员编号、索赔编号、医院名称,并加上 rec_id。
要读取的文件是第 1 步生成的 /root/deid/claims.csv。不要舍弃任何一行。rec_id 必须按原始顺序编号,日后才能指向同一行。去掉医院名称,是因为小医院会缩小居住地的范围。
统计一个组合指向多少人
用 /root/deid/kcount.py 统计 birth_date、sex、zip5 组合的 k,并写入 /root/deid/k_before.json。
要读取的文件是 /root/deid/stripped.csv。统计每个组合的行数,其最小值就是 k。同时统计只有自己一行的(k 为 1)有多少,以及不足五人的行有多少。这些数字是后续各步骤的起点。
把日期缩减为年份并合并高龄
用 /root/deid/generalize.py 生成 /root/deid/generalized.csv。超过 90 岁的,连出生年份也删除。
要读取的文件是 /root/deid/stripped.csv。不保留年份以外的日期要素,这是 Safe Harbor 的规则。年龄按 2026 减去出生年份来计算。极高的年龄仅凭一个年份就能缩小到某个人,所以合并为一个类别。
归并小地区和罕见的诊断代码
用 /root/deid/suppress.py 生成 /root/deid/suppressed.csv。人口在 20000 人以下的地区为 000,出现次数不足 5 次的诊断代码为 RARE。
要读取的文件是 /root/deid/generalized.csv 和 /root/deid/zip_pop.csv。只有当该地区人数足够多时,才保留邮政编码前三位。请读取人口表来判断。诊断代码的出现次数,要在这次导出的数据范围内统计。
把 k 提高到 5 并测量损失量
用 /root/deid/kanon.py 抑制组合不足 5 笔的行,生成 /root/deid/export.csv 和 /root/deid/k_after.json。
要读取的文件是 /root/deid/suppressed.csv。抑制是最后的手段。前面的泛化足够的话,舍弃的行就很少。如果剩余比例低于 70%,不应当增加抑制,而是应当回头检查前面的步骤。
亲自尝试重新识别
用 /root/deid/reidentify.py 生成 /root/deid/attack.json。对象是导出文件中 rec_id 最小的那一行。
要读取的文件是 /root/deid/export.csv 和 /root/deid/stripped.csv。攻击者所知道的只有泛化后的值(出生年份、性别、地区、索赔年份)。把同一个人在只去掉了直接标识符的版本中能缩小到几行,与此作比较,用数字就能看出泛化实际阻止了什么。
留下导出记录和检查清单
留下 /root/deid/export_record.json 和 /root/deid/checklist.md。哈希和笔数从实际文件中测得。
哈希和笔数从 /root/deid/claims.csv 和 /root/deid/export.csv 中测得。记录中要包含原始文件和导出文件的哈希、所应用的规则、目的与批准人、保存期限。检查清单中除了依据条文,还要写明该标准是否直接适用于我们。如果把原始日期抄进清单,清单就成了又一份泄露的文件。