vkatg/streaming-phi-deidentification-benchmark
Streaming PHI De-Identification Benchmark Most PHI de-identification benchmarks evaluate a single document in isolation. That is not how clinical data actually moves. A patient's name appears in a clinical note, then in an ASR transcript ten minutes later, then in imaging metadata an hour after that. Each event looks low-risk on its own. The cumulative exposure across modalities is what creates re-identification risk. This dataset captures that. Every record is fully synthetic.… See the full description on the dataset page: https://huggingface.co/datasets/vkatg/streaming-phi-deidentification-benchmark.
Streaming PHI De-Identification Benchmark
Most PHI de-identification benchmarks evaluate a single document in isolation. That is not how clinical data actually moves. A patient's name appears in a clinical note, then in an ASR transcript ten minutes later, then in imaging metadata an hour after that. Each event looks low-risk on its own. The cumulative exposure across modalities is what creates re-identification risk.
This dataset captures that. Every record is fully synthetic. It models how re-identification risk builds across a longitudinal multimodal stream and evaluates whether a masking policy responds to that accumulation or ignores it.
The short answer from the results: static policies cannot do both things at once. A policy that redacts everything achieves perfect privacy but destroys utility. A policy that pseudonymizes everything preserves utility but fails at high risk. The adaptive controller is the only one that clears both bars simultaneously.
Quick Start
import json
import pandas as pd
with open("audit_log.jsonl") as f:
events = [json.loads(line) for line in f]
with open("audit_log_signed_adaptive.jsonl") as f:
adaptive = [json.loads(line) for line in f]
df = pd.read_csv("policy_metrics.csv")
print(df)Via the Hugging Face datasets library:
from datasets import load_dataset
ds = load_dataset("vkatg/streaming-phi-deidentification-benchmark")
signed = load_dataset("vkatg/streaming-phi-deidentification-benchmark", "signed")
cm = load_dataset("vkatg/streaming-phi-deidentification-benchmark", "crossmodal")Why This Dataset Exists
Every open PHI benchmark we found had the same structure: a document comes in, you mask it, you score it, done. That works for NER-style de-identification. It does not work for evaluating a system where the threat model is cumulative exposure.
The specific gaps this dataset fills:
No memory across events. i2b2 and PhysioNet evaluate each record independently. A system that correctly masks event 1 and event 17 in isolation can still leak identity when you join them. This dataset tracks exposure state across the full stream and scores masking decisions against that accumulated state.
Single modality. Existing benchmarks are clinical text only. Real healthcare pipelines include ASR transcripts, imaging metadata, physiological waveforms, and audio. Cross-modal PHI linkage is a distinct threat that text-only evaluation misses entirely.
No open access. i2b2 and PhysioNet require data use agreements, which is appropriate given they contain real patient data. But it creates friction for anyone building or evaluating a de-identification system who just needs a benchmark they can run locally. This dataset is MIT-licensed, fully synthetic, and requires no agreement to use.
No adversarial coverage. None of the standard benchmarks include an attacker model. This dataset includes a formal sub-threshold probing scenario where an attacker spaces PHI submissions to stay below individual risk thresholds while accumulating cross-modal links. The adaptive controller detects and escalates on these; static policies do not.
Risk Timeline
Risk accumulates as PHI exposure is recorded across events. Patient A (research consent, ceiling: pseudo) and Patient B (standard consent, ceiling: redact) alternate. Dots are colored by the policy applied at each event. The controller escalates as risk crosses 0.40, 0.60, and 0.80.
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" style="font-family: system-ui, sans-serif; background: white;"> <text x="370" y="20" text-anchor="middle" font-size="13" font-weight="600" fill="#374151">Adaptive Risk Score — 34 Live Events</text> <line x1="65" y1="255" x2="670" y2="255" stroke="#e5e7eb" stroke-width="1"/> <line x1="65" y1="35" x2="65" y2="255" stroke="#e5e7eb" stroke-width="1"/> <line x1="65" y1="220.8" x2="670" y2="220.8" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="225" text-anchor="end" font-size="10" fill="#6b7280">0.4</text> <line x1="65" y1="186.7" x2="670" y2="186.7" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="191" text-anchor="end" font-size="10" fill="#6b7280">0.5</text> <line x1="65" y1="152.5" x2="670" y2="152.5" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="157" text-anchor="end" font-size="10" fill="#6b7280">0.6</text> <line x1="65" y1="118.3" x2="670" y2="118.3" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="123" text-anchor="end" font-size="10" fill="#6b7280">0.7</text> <line x1="65" y1="84.2" x2="670" y2="84.2" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="89" text-anchor="end" font-size="10" fill="#6b7280">0.8</text> <line x1="65" y1="50.0" x2="670" y2="50.0" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="54" text-anchor="end" font-size="10" fill="#6b7280">0.9</text> <text x="59" y="39" text-anchor="end" font-size="10" fill="#6b7280">1.0</text> <text x="59" y="259" text-anchor="end" font-size="10" fill="#6b7280">0.3</text> <line x1="65" y1="220.8" x2="670" y2="220.8" stroke="#fbbf24" stroke-width="1" stroke-dasharray="4 3" opacity="0.6"/> <text x="674" y="225" font-size="9" fill="#fbbf24">0.40</text> <line x1="65" y1="152.5" x2="670" y2="152.5" stroke="#f97316" stroke-width="1" stroke-dasharray="4 3" opacity="0.6"/> <text x="674" y="157" font-size="9" fill="#f97316">0.60</text> <line x1="65" y1="84.2" x2="670" y2="84.2" stroke="#ef4444" stroke-width="1" stroke-dasharray="4 3" opacity="0.6"/> <text x="674" y="89" font-size="9" fill="#ef4444">0.80</text> <polyline points="65.0,179.8 101.7,148.2 138.3,117.8 175.0,93.2 211.7,73.2 248.3,57.1 285.0,89.4 321.7,80.5 358.3,71.9 395.0,64.9 431.7,58.5 468.3,54.6 505.0,50.4 541.7,47.9 578.3,45.5 615.0,43.2 651.7,41.7" fill="none" stroke="#6366f1" stroke-width="2"/> <polyline points="83.3,197.9 120.0,158.1 156.7,125.8 193.3,103.1 230.0,81.3 266.7,65.9 303.3,53.0 340.0,86.3 376.7,77.8 413.3,70.8 450.0,64.0 486.7,59.2 523.3,54.6 560.0,50.9 596.7,48.3 633.3,46.1 670.0,44.0" fill="none" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5 3"/> <circle cx="65.0" cy="179.8" r="4" fill="#f59e0b" stroke="white" stroke-width="1.2"/> <circle cx="83.3" cy="197.9" r="4" fill="#f59e0b" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="101.7" cy="148.2" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="120.0" cy="158.1" r="4" fill="#6366f1" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="138.3" cy="117.8" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="156.7" cy="125.8" r="4" fill="#6366f1" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="175.0" cy="93.2" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="193.3" cy="103.1" r="4" fill="#6366f1" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="211.7" cy="73.2" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="230.0" cy="81.3" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="248.3" cy="57.1" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="266.7" cy="65.9" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="285.0" cy="89.4" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="303.3" cy="53.0" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="321.7" cy="80.5" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="340.0" cy="86.3" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="358.3" cy="71.9" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="376.7" cy="77.8" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="395.0" cy="64.9" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="413.3" cy="70.8" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="431.7" cy="58.5" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="450.0" cy="64.0" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="468.3" cy="54.6" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="486.7" cy="59.2" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="505.0" cy="50.4" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="523.3" cy="54.6" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="541.7" cy="47.9" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="560.0" cy="50.9" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="578.3" cy="45.5" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="596.7" cy="48.3" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="615.0" cy="43.2" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="633.3" cy="46.1" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <circle cx="651.7" cy="41.7" r="4" fill="#6366f1" stroke="white" stroke-width="1.2"/> <circle cx="670.0" cy="44.0" r="4" fill="#374151" stroke="white" stroke-width="1.2" opacity="0.85"/> <text x="65.0" y="270" text-anchor="middle" font-size="10" fill="#6b7280">0</text> <text x="156.7" y="270" text-anchor="middle" font-size="10" fill="#6b7280">5</text> <text x="248.3" y="270" text-anchor="middle" font-size="10" fill="#6b7280">10</text> <text x="340.0" y="270" text-anchor="middle" font-size="10" fill="#6b7280">15</text> <text x="431.7" y="270" text-anchor="middle" font-size="10" fill="#6b7280">20</text> <text x="523.3" y="270" text-anchor="middle" font-size="10" fill="#6b7280">25</text> <text x="615.0" y="270" text-anchor="middle" font-size="10" fill="#6b7280">30</text> <text x="670.0" y="270" text-anchor="middle" font-size="10" fill="#6b7280">33</text> <text x="367" y="290" text-anchor="middle" font-size="10" fill="#6b7280">Event Index</text> <text x="14" y="145" text-anchor="middle" font-size="10" fill="#6b7280" transform="rotate(-90 14 145)">Risk Score</text> <line x1="500" y1="240" x2="518" y2="240" stroke="#6366f1" stroke-width="2"/> <text x="522" y="244" font-size="10" fill="#374151">Patient A</text> <line x1="580" y1="240" x2="598" y2="240" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5 3"/> <text x="602" y="244" font-size="10" fill="#374151">Patient B</text> <circle cx="500" cy="228" r="4" fill="#f59e0b"/> <text x="507" y="232" font-size="10" fill="#374151">synthetic</text> <circle cx="570" cy="228" r="4" fill="#6366f1"/> <text x="577" y="232" font-size="10" fill="#374151">pseudo</text> <circle cx="630" cy="228" r="4" fill="#374151"/> <text x="637" y="232" font-size="10" fill="#374151">redact</text> </svg>
Re-identification Risk Reduction
Delta-AUROC measures the reduction in a logistic-regression re-identifier's ability to distinguish patients from masked vs. original text, computed on a rolling 32-event window. Negative values mean the masking is working. The multi-run mean is -0.9167 ± 0.0000 (95% CI, n=10). First non-zero signal appears at event 7, once the buffer has enough samples with 2 distinct patient labels.
<svg viewBox="0 0 740 295" xmlns="http://www.w3.org/2000/svg" style="font-family: system-ui, sans-serif; background: white;"> <text x="370" y="20" text-anchor="middle" font-size="13" font-weight="600" fill="#374151">Delta-AUROC — Re-identification Risk Reduction</text> <path d="M 65.0,45.5 L 65.0,45.5 83.3,45.5 101.7,45.5 120.0,45.5 138.3,45.5 156.7,45.5 175.0,45.5 193.3,71.7 211.7,71.7 230.0,71.7 248.3,97.9 266.7,255.0 285.0,80.4 303.3,150.2 321.7,115.3 340.0,220.1 358.3,150.2 376.7,150.2 395.0,71.7 413.3,150.2 431.7,150.2 450.0,228.8 468.3,66.4 486.7,150.2 505.0,66.4 523.3,234.0 541.7,150.2 560.0,150.2 578.3,80.4 596.7,220.1 615.0,150.2 633.3,237.5 651.7,237.5 670.0,237.5 L 670.0,45.5 Z" fill="#6366f1" opacity="0.10"/> <line x1="65" y1="45.5" x2="670" y2="45.5" stroke="#9ca3af" stroke-width="1.5"/> <text x="59" y="49" text-anchor="end" font-size="10" fill="#6b7280">0.00</text> <line x1="65" y1="255" x2="670" y2="255" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="259" text-anchor="end" font-size="10" fill="#6b7280">-1.00</text> <line x1="65" y1="202.9" x2="670" y2="202.9" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="207" text-anchor="end" font-size="10" fill="#6b7280">-0.75</text> <line x1="65" y1="150.2" x2="670" y2="150.2" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="154" text-anchor="end" font-size="10" fill="#6b7280">-0.50</text> <line x1="65" y1="97.9" x2="670" y2="97.9" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="102" text-anchor="end" font-size="10" fill="#6b7280">-0.25</text> <line x1="193.3" y1="35" x2="193.3" y2="71.7" stroke="#fbbf24" stroke-width="1" stroke-dasharray="3 2" opacity="0.7"/> <text x="196" y="43" font-size="9" fill="#b45309">first drop (evt 7)</text> <polyline points="65.0,45.5 83.3,45.5 101.7,45.5 120.0,45.5 138.3,45.5 156.7,45.5 175.0,45.5 193.3,71.7 211.7,71.7 230.0,71.7 248.3,97.9 266.7,255.0 285.0,80.4 303.3,150.2 321.7,115.3 340.0,220.1 358.3,150.2 376.7,150.2 395.0,71.7 413.3,150.2 431.7,150.2 450.0,228.8 468.3,66.4 486.7,150.2 505.0,66.4 523.3,234.0 541.7,150.2 560.0,150.2 578.3,80.4 596.7,220.1 615.0,150.2 633.3,237.5 651.7,237.5 670.0,237.5" fill="none" stroke="#6366f1" stroke-width="2"/> <circle cx="670.0" cy="237.5" r="4" fill="#6366f1" stroke="white" stroke-width="1.5"/> <text x="664" y="230" text-anchor="end" font-size="9" fill="#6366f1">-0.9167</text> <text x="65.0" y="271" text-anchor="middle" font-size="10" fill="#6b7280">0</text> <text x="156.7" y="271" text-anchor="middle" font-size="10" fill="#6b7280">5</text> <text x="248.3" y="271" text-anchor="middle" font-size="10" fill="#6b7280">10</text> <text x="340.0" y="271" text-anchor="middle" font-size="10" fill="#6b7280">15</text> <text x="431.7" y="271" text-anchor="middle" font-size="10" fill="#6b7280">20</text> <text x="523.3" y="271" text-anchor="middle" font-size="10" fill="#6b7280">25</text> <text x="615.0" y="271" text-anchor="middle" font-size="10" fill="#6b7280">30</text> <text x="670.0" y="271" text-anchor="middle" font-size="10" fill="#6b7280">33</text> <text x="367" y="288" text-anchor="middle" font-size="10" fill="#6b7280">Event Index</text> <text x="14" y="145" text-anchor="middle" font-size="10" fill="#6b7280" transform="rotate(-90 14 145)">Delta-AUROC</text> </svg>
Privacy vs Utility — Bursty Workload
The bursty workload is the hardest case: the system has to protect high-risk returning patients while preserving utility for newly admitted low-risk ones at the same time. The target region (top-right, shaded) requires Privacy@HighRisk above 0.85 and Utility@LowRisk above 0.50. No static policy reaches it. Adaptive does.
<svg viewBox="0 0 500 420" xmlns="http://www.w3.org/2000/svg" style="font-family: system-ui, sans-serif; background: white;"> <text x="250" y="22" text-anchor="middle" font-size="13" font-weight="600" fill="#374151">Privacy vs Utility — Bursty Workload</text> <!-- axes: x=60-440 (utility 0-1), y=40-360 (privacy 0-1 inverted) --> <line x1="60" y1="40" x2="60" y2="360" stroke="#9ca3af" stroke-width="1.5"/> <line x1="60" y1="360" x2="440" y2="360" stroke="#9ca3af" stroke-width="1.5"/> <!-- grid --> <line x1="60" y1="280" x2="440" y2="280" stroke="#e5e7eb" stroke-width="1"/> <line x1="60" y1="200" x2="440" y2="200" stroke="#e5e7eb" stroke-width="1"/> <line x1="60" y1="120" x2="440" y2="120" stroke="#e5e7eb" stroke-width="1"/> <line x1="60" y1="40" x2="440" y2="40" stroke="#e5e7eb" stroke-width="1"/> <line x1="155" y1="40" x2="155" y2="360" stroke="#e5e7eb" stroke-width="1"/> <line x1="250" y1="40" x2="250" y2="360" stroke="#e5e7eb" stroke-width="1"/> <line x1="345" y1="40" x2="345" y2="360" stroke="#e5e7eb" stroke-width="1"/> <line x1="440" y1="40" x2="440" y2="360" stroke="#e5e7eb" stroke-width="1"/> <!-- axis labels --> <text x="54" y="364" text-anchor="end" font-size="10" fill="#6b7280">0.0</text> <text x="54" y="284" text-anchor="end" font-size="10" fill="#6b7280">0.25</text> <text x="54" y="204" text-anchor="end" font-size="10" fill="#6b7280">0.50</text> <text x="54" y="124" text-anchor="end" font-size="10" fill="#6b7280">0.75</text> <text x="54" y="44" text-anchor="end" font-size="10" fill="#6b7280">1.00</text> <text x="60" y="376" text-anchor="middle" font-size="10" fill="#6b7280">0.0</text> <text x="155" y="376" text-anchor="middle" font-size="10" fill="#6b7280">0.25</text> <text x="250" y="376" text-anchor="middle" font-size="10" fill="#6b7280">0.50</text> <text x="345" y="376" text-anchor="middle" font-size="10" fill="#6b7280">0.75</text> <text x="440" y="376" text-anchor="middle" font-size="10" fill="#6b7280">1.00</text> <!-- floor lines --> <!-- privacy floor 0.85: y = 360 - 0.85320 = 88 --> <line x1="60" y1="88" x2="440" y2="88" stroke="#f97316" stroke-width="1" stroke-dasharray="5 3" opacity="0.6"/> <text x="442" y="92" font-size="9" fill="#f97316">privacy 0.85</text> <!-- utility floor 0.50: x = 60 + 0.5380 = 250 --> <line x1="250" y1="40" x2="250" y2="360" stroke="#10b981" stroke-width="1" stroke-dasharray="5 3" opacity="0.5"/> <!-- target region shading --> <rect x="250" y="40" width="190" height="48" fill="#10b981" opacity="0.08"/> <text x="345" y="70" text-anchor="middle" font-size="9" fill="#10b981">target region</text> <!-- axis titles --> <text x="250" y="395" text-anchor="middle" font-size="11" fill="#374151">Utility @ Low Risk</text> <text x="14" y="200" text-anchor="middle" font-size="11" fill="#374151" transform="rotate(-90 14 200)">Privacy @ High Risk</text> <!-- points: x = 60 + utility380, y = 360 - privacy320 --> <!-- Raw: utility=1.0, privacy=0.0 => x=440, y=360 --> <circle cx="440" cy="360" r="7" fill="none" stroke="#ef4444" stroke-width="2.5"/> <text x="430" y="352" text-anchor="end" font-size="10" fill="#ef4444">Raw</text> <!-- Weak: utility=0.8466, privacy=0.0035 => x=382.7, y=358.9 --> <rect x="377.2" y="353.4" width="11" height="11" fill="#f97316"/> <text x="376" y="350" text-anchor="end" font-size="10" fill="#f97316">Weak</text> <!-- Synthetic: utility=0.6755, privacy=0.5642 => x=316.7, y=179.5 --> <polygon points="316.7,168 325.7,183 307.7,183" fill="#f59e0b"/> <text x="328" y="179" font-size="10" fill="#f59e0b">Synthetic</text> <!-- Pseudo: utility=0.4399, privacy=0.8547 => x=227.2, y=86.5 --> <polygon points="227.2,74 238.7,93 215.7,93" fill="#22c55e"/> <text x="200" y="78" text-anchor="end" font-size="10" fill="#22c55e">Pseudo</text> <!-- Redact: utility=0.0, privacy=1.0 => x=60, y=40 --> <polygon points="60,52 54,40 66,40" fill="#3b82f6"/> <text x="72" y="50" font-size="10" fill="#3b82f6">Redact</text> <!-- Adaptive: utility=0.8466, privacy=0.9907 => x=382.7, y=43.0 --> <circle cx="382.7" cy="43.0" r="9" fill="#7c3aed" stroke="white" stroke-width="2"/> <text x="394" y="40" font-size="11" fill="#7c3aed" font-weight="600">Adaptive</text> </svg>
Full Pareto frontier across all three workloads is in EXPERIMENT_REPORT.md.
Baseline Policy Comparison
Three workloads: monotonic (risk accumulates continuously), bursty (new low-risk patients enter every 6 events), mixed (70% routine / 30% high-complexity). Full numbers are in baseline_comparison.csv.
Bursty workload (primary claim)
Monotonic workload
Mixed workload
Threshold Sensitivity
remask_thresh controls when the controller triggers pseudonym re-tokenization. Lower values trigger remask more aggressively. The chart shows how Privacy@HighRisk and Utility@LowRisk shift on the bursty workload across a grid of threshold values. The default (0.68, marked) sits at an inflection point: privacy is near ceiling and utility has not yet dropped. Full data for all three workloads is in data/threshold_sensitivity.csv.
<svg viewBox="0 0 700 295" xmlns="http://www.w3.org/2000/svg" style="font-family: system-ui, sans-serif; background: white;"> <text x="350" y="20" text-anchor="middle" font-size="13" font-weight="600" fill="#374151">remaskthresh Sensitivity — Bursty Workload</text> <line x1="65" y1="255" x2="630" y2="255" stroke="#9ca3af" stroke-width="1.5"/> <line x1="65" y1="35" x2="65" y2="255" stroke="#9ca3af" stroke-width="1.5"/> <line x1="65" y1="222.5" x2="630" y2="222.5" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="227" text-anchor="end" font-size="10" fill="#6b7280">0.70</text> <line x1="65" y1="195.0" x2="630" y2="195.0" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="199" text-anchor="end" font-size="10" fill="#6b7280">0.75</text> <line x1="65" y1="167.5" x2="630" y2="167.5" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="172" text-anchor="end" font-size="10" fill="#6b7280">0.80</text> <line x1="65" y1="140.0" x2="630" y2="140.0" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="144" text-anchor="end" font-size="10" fill="#6b7280">0.85</text> <line x1="65" y1="112.5" x2="630" y2="112.5" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="117" text-anchor="end" font-size="10" fill="#6b7280">0.90</text> <line x1="65" y1="85.0" x2="630" y2="85.0" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="89" text-anchor="end" font-size="10" fill="#6b7280">0.95</text> <line x1="65" y1="57.5" x2="630" y2="57.5" stroke="#e5e7eb" stroke-width="1"/> <text x="59" y="62" text-anchor="end" font-size="10" fill="#6b7280">1.00</text> <line x1="404.0" y1="35" x2="404.0" y2="255" stroke="#6366f1" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.6"/> <text x="407" y="48" font-size="9" fill="#6366f1">default 0.68</text> <polyline points="65.0,141.8 159.2,119.8 253.3,97.8 347.5,75.8 404.0,62.6 479.3,57.5 535.8,57.5 630.0,57.5" fill="none" stroke="#6366f1" stroke-width="2"/> <circle cx="65.0" cy="141.8" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="159.2" cy="119.8" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="253.3" cy="97.8" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="347.5" cy="75.8" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="404.0" cy="62.6" r="5" fill="#6366f1" stroke="white" stroke-width="1.5"/> <circle cx="479.3" cy="57.5" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="535.8" cy="57.5" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <circle cx="630.0" cy="57.5" r="4" fill="#6366f1" stroke="white" stroke-width="1"/> <polyline points="65.0,201.3 159.2,184.8 253.3,168.3 347.5,151.8 404.0,141.9 479.3,128.7 535.8,118.8 630.0,102.3" fill="none" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5 3"/> <circle cx="65.0" cy="201.3" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="159.2" cy="184.8" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="253.3" cy="168.3" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="347.5" cy="151.8" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="404.0" cy="141.9" r="5" fill="#f59e0b" stroke="white" stroke-width="1.5"/> <circle cx="479.3" cy="128.7" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="535.8" cy="118.8" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <circle cx="630.0" cy="102.3" r="4" fill="#f59e0b" stroke="white" stroke-width="1"/> <text x="65.0" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.50</text> <text x="159.2" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.55</text> <text x="253.3" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.60</text> <text x="347.5" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.65</text> <text x="404.0" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.68</text> <text x="479.3" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.72</text> <text x="535.8" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.75</text> <text x="630.0" y="270" text-anchor="middle" font-size="9" fill="#6b7280">0.80</text> <text x="347" y="287" text-anchor="middle" font-size="10" fill="#6b7280">remaskthresh</text> <text x="14" y="145" text-anchor="middle" font-size="10" fill="#6b7280" transform="rotate(-90 14 145)">Score</text> <line x1="430" y1="72" x2="448" y2="72" stroke="#6366f1" stroke-width="2"/> <text x="452" y="76" font-size="10" fill="#374151">Privacy@HighRisk</text> <line x1="430" y1="86" x2="448" y2="86" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5 3"/> <text x="452" y="90" font-size="10" fill="#374151">Utility@LowRisk</text> </svg>
Comparison with Existing PHI Datasets
Dataset Structure
Three configs:
Fields
Sample Record
{
"event_id": "evt_8",
"patient_key": "A",
"modality": "text",
"chosen_policy": "pseudo",
"reason": "medium-high risk: pseudonymization",
"risk": 0.8783,
"localized_remask_trigger": false,
"latency_ms": 17.01,
"leaks_after": 0,
"consent_status": "capped",
"extra": {
"delta_auroc": -0.125,
"crdt_risk": 0.512
},
"decision_blob": {
"rl_policy": "redact",
"rl_source": "rl_model",
"rl_reward": 0.63
}
}Cross-Modal Benchmark Scenarios
Each scenario runs all five policy variants. 260 total rows.
Risk Model
R = 0.8 * (1 - exp(-k * units)) + 0.2 * recency + link_bonuskunits = 0.05. Link bonus: +0.20 for 2 or more cross-modal links, +0.30 for 3 or more. Validated against a closed-form combinatorial reconstruction probability (Pearson r = 0.881, n=34). Cross-modal PHI correlation between image and audio modalities is r = 0.081, confirming largely independent signals and validating the CROSSMODALSIMTHRESHOLD = 0.30 design constant.
Consent Layer
All consent-cap events occur on patient A (research consent, ceiling: pseudo): the controller decided redact at high risk, downgraded to pseudo by the consent layer. Patient B (standard consent, ceiling: redact) was never capped. 14 caps total across 34 live events. Full event-by-event log is in consent_cap_log.jsonl.
RL Agent
PPO with an LSTM-backed policy network (128-dim hidden, 2 layers, 14-dimensional state), pretrained for 200 stratified episodes. Overall reward mean: 0.2806 (min -0.3337, max 0.7093, n=234). Warmup mean: 0.4994 (n=22). Model-driven mean: 0.2579 (n=212). All 34 live-loop events were model-driven. Reward statistics are in rl_reward_stats.json.
CRDT Federation
Federated graph merge demo: two edge devices, deviceA (text + imageproxy) and deviceB (audioproxy + text). Post-merge: 3 nodes, merged risk = 0.1806, convergence guaranteed. Full output in dcpg_crdt_demo.json.
System Architecture
Files
Limitations
This dataset is fully synthetic. It models the structural properties of longitudinal healthcare streams but does not contain real clinical language, real diagnoses, or real patient records. Risk scores, leakage values, and policy decisions are generated by the simulation, not extracted from real deployments. Any system trained or evaluated here should be validated against real clinical data before production use. English-language proxies only.
Reproduce
git clone https://github.com/azithteja91/phi-exposure-guard.git
cd phi-exposure-guard
pip install -e .
python -m amphi_rl_dpgraph.run_demoGenerate the supplementary data files:
python scripts/generate_crossmodal_train.py
python scripts/generate_leakage_breakdown.py
python scripts/generate_risk_trace.py
python scripts/generate_threshold_sensitivity.pyGitHub: azithteja91/phi-exposure-guard
HF Space: vkatg/amphi-rl-dpgraph
Colab: 
Citation
Cite via the CITATION.cff file in the GitHub repository.
License
MIT
