CoolFace
Datasetpublic

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.

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes135downloads
Dataset Card

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

python
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:

python
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)

PolicyPrivacy@HighRiskUtility@LowRiskConsent ViolsLatency (ms)
Always-Raw0.00001.000000.5
Always-Weak0.00350.846601.0
Always-Synthetic0.56420.675502.0
Always-Pseudo0.85470.439901.5
Always-Redact1.00000.0000171.0
Adaptive0.99070.8466101.1

Monotonic workload

PolicyPrivacy@HighRiskUtility@LowRiskConsent Viols
Always-Pseudo0.85510.00000
Always-Redact1.00000.000017
Adaptive0.98630.000014

Mixed workload

PolicyPrivacy@HighRiskUtility@LowRiskConsent Viols
Always-Pseudo0.85440.45770
Always-Redact1.00000.000017
Adaptive0.98480.85266

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

DatasetModalityAccessReal DataStreamingAdversarialSize
i2b2 2014Text onlyDUA requiredYesNoNo1,304 records
PhysioNet deidText onlyDUA requiredYesNoNo2,434 notes
This datasetText, ASR, Image, Waveform, AudioOpen (MIT)NoYesYes1K-10K events

Dataset Structure

Three configs:

ConfigFileDescription
defaultaudit_log.jsonlFull run log, adaptive policy
signedaudit_log_signed_adaptive.jsonlCryptographically signed adaptive-only events
crossmodaldata/crossmodal_train.jsonlCross-modal link benchmark scenarios (A-E)

Fields

FieldDescription
event_idEvent identifier within the stream
patient_keySynthetic subject identifier
modalitytext, asr, image_proxy, waveform_proxy, audio_proxy
chosen_policyPolicy actually applied
reasonSelection reason from the controller
riskCumulative re-identification risk at decision time
localized_remask_triggerWhether risk crossing remask_thresh triggered pseudonym versioning
latency_msDecision latency in milliseconds
leaks_afterResidual PHI leakage post-masking
policy_versionPolicy version token
consent_statusok or capped
extra.delta_aurocRolling delta-AUROC at this event
extra.crdt_riskCRDT-backed graph risk at this event
decision_blobFull decision record: risk components, DCPG state, cross-modal matches

Sample Record

json
{
  "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

ScenarioWhat it tests
ABaseline: single-modality accumulation, no cross-modal link
BCross-modal link fires mid-sequence (text then ASR)
CThree-modal convergence (text, ASR, image_proxy)
DRemask trigger: risk crosses 0.68 mid-run, pseudonym versioning fires
EAdversarial sub-threshold probing with cross-modal spike every 5th event

Each scenario runs all five policy variants. 260 total rows.

Risk Model

R = 0.8 * (1 - exp(-k * units)) + 0.2 * recency + link_bonus

kunits = 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

ModuleDescription
context_state.pyPer-subject PHI exposure state, persisted via SQLite
controller.pyRisk scoring and adaptive policy selection
dcpg.pyDynamic Contextual Privacy Graph: cross-modal identity linkage
dcpg_crdt.pyCRDT-based graph merging for distributed/federated deployments
dcpg_federation.pyStreaming delta sync and HMAC-authenticated inter-device federation
cmo_registry.pyComposable Masking Operator registry and DAG execution
cmo_media.pyToken-level synthetic replacement ops (names, dates, MRNs, facilities)
flow_controller.pyDAG-based policy flow controller with audit provenance
masking.pyHigh-level masking dispatcher: routes events to the right CMO chain
masking_ops.pyLow-level masking primitives per policy tier
rl_agent.pyPPO reinforcement learning agent for adaptive policy control
phi_detector.pyPHI span detection and leakage measurement
consent.pyConsent token resolution and policy ceiling enforcement
downstream_feedback.pyRolling utility monitor for downstream task signal
metrics.pyLeakage scoring, utility proxy, and delta-AUROC computation
eval.pyEvaluation harness: latency summarization and per-policy scoring
audit_signing.pyECDSA signing, Merkle chain, and FHIR export of audit records
baseline_experiment.pyStatic policy baselines, Pareto frontier, and workload sweep
db.pySQLite connection management and WAL configuration
schemas.pyShared dataclasses: PHISpan, DataEvent, DecisionRecord, AuditRecord
run_demo.pyEnd-to-end demo: pretraining, live loop, evaluation, report generation

Files

FileDescription
audit_log.jsonlFull run log, adaptive policy
audit_log_signed_adaptive.jsonlSigned adaptive-only audit trail
data/crossmodal_train.jsonlCross-modal scenario benchmark (260 rows)
data/leakage_breakdown.jsonlPer-event leakage by PHI entity type (mrn, date, name, facility)
data/risk_trace.jsonlPer-event risk component history (unitsfactor, recency, linkbonus)
data/threshold_sensitivity.csvPrivacy/utility across remask_thresh grid (0.50-0.80), all workloads
policy_metrics.csvPer-policy metrics from the live run
latency_summary.csvLatency distribution summary
baseline_comparison.csvFull baseline results across all workloads
statistical_robustness.jsonMulti-run (n=10) robustness statistics
controller_config.jsonController configuration for this run
rl_reward_stats.jsonPPO reward statistics
consent_cap_log.jsonlPer-event consent cap decisions
delta_auroc_log.jsonlPer-event delta-AUROC and CRDT risk
dcpg_crdt_demo.jsonCRDT federation merge output
EXPERIMENT_REPORT.mdFull experiment report

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

bash
git clone https://github.com/azithteja91/phi-exposure-guard.git
cd phi-exposure-guard
pip install -e .
python -m amphi_rl_dpgraph.run_demo

Generate the supplementary data files:

bash
python scripts/generate_crossmodal_train.py
python scripts/generate_leakage_breakdown.py
python scripts/generate_risk_trace.py
python scripts/generate_threshold_sensitivity.py

GitHub: azithteja91/phi-exposure-guard

HF Space: vkatg/amphi-rl-dpgraph

Colab: ![Open In Colab](https://colab.research.google.com/github/azithteja91/phi-exposure-guard/blob/main/notebooks/demo_colab.ipynb)

Citation

Cite via the CITATION.cff file in the GitHub repository.

License

MIT