Flag structurally unusual – and so easily identifiable – subjects
Source:R/compare.R
flag_identifiable_subjects.RdA post-generation screen for subjects that stand out from the cohort and are
therefore easy to single out and re-identify: the per-subject counterpart to
compare_pmx_distributions(), which compares whole distributions. Each
subject is scored, one axis at a time, on a robust median/MAD statistic across
four structural features:
Arguments
- data
A PMX dataset – typically the synthetic output, or the source.
- roles
Explicit roles from
pmx_roles().- threshold
Absolute modified-z cutoff above which a subject is an outlier on an axis. Default 3.5, the Iglewicz–Hoaglin value.
- min_relative_gap
How far a subject must sit from the nearest other subject in its comparison group before that outlier counts, as a fraction of the group's median on that axis. Default 1, chosen against ordinary between-subject variability: a subject 40 minutes from a 216-hour follow-up is not a finding, and one followed to 1730 hours where everybody else has finished by 672 is. Lower it to widen the net – 0.5 adds the merely striking, 0 leaves the z alone.
Value
A pmx_identifiability data frame, most-unusual first, one row per
subject: subject_id, the four axis values (follow_up_time, n_doses,
max_dose, max_dv), outlier_axes (a comma-separated list of the axes on
which it is unusual, empty if none), and flagged.
Details
follow-up time – the last observation time (catches the lone long-followed subject);
number of doses – an unusual dosing-history length;
dose magnitude – a rare dose level (needs an
amtrole); andDV value – an extreme peak measurement.
A subject is flagged on an axis when both hold: it is a robust outlier
there, and its gap to the nearest other subject in the comparison group is at
least min_relative_gap of that group's median. The second condition is what
makes the answer readable. A modified z says a subject is unusual for its
cohort; it cannot say the difference is big enough to single anybody out, and
on protocol-driven data that is the question that decides. Every subject in
xgxr::mad completed the study, so follow-up times sit within two hours of
each other and the median absolute deviation of that is six minutes: a
subject forty minutes from its arm's median scores 4.7 and is identifiable to
nobody. Requiring the gap as well takes that study from 6 flagged of 60 to 0,
pheno_sd from 25 of 59 to 0 and mavoglurant from 41 of 120 to 0, while
leaving a subject given twice its arm's dose, or followed three times as long
as anyone else, flagged. The gap is measured to the nearest other subject
rather than to the median, because two subjects sharing an extreme value
single out neither – the same reasoning as min_pattern_share = 2 on visit
sets.
The default of 1 is set against ordinary between-subject variability
rather than against any cohort: a 30-50% coefficient of variation is
unremarkable on a pharmacokinetic parameter, so a subject separated by less
than that is inside the noise of the study and could not be picked out of it.
At 1 the separation must exceed the group's whole median value, which is the
glaring case and nothing smaller – nlmixr2data::wbcSim reports the two
patients followed to 1730 and 4580 hours in a cohort that otherwise ends by
672, and not the one at 1130.
On that setting no synthetic dataset in the public-data evaluation at https://iamstein.github.io/synpmx/articles/avatar-public-data-examples.html reports anybody, which is the screen working rather than idling: an avatar is a blend of several donors, so producing a subject that extreme takes a defect, and this is the row that would say so.
This matters because
synpmx_avatar() copies each avatar's event skeleton from a single anchor, so
a structurally unique source subject yields a structurally unique – and
identifiable – avatar even though its measurements are blended. Run it on the
synthetic data before the data leaves the source's access controls and drop or
regenerate the flagged subjects; it can also be run on the source itself to
see which real subjects are hardest to hide. It is a heuristic screen, not a
privacy guarantee, and is marked "restricted_not_releasable".
Scores are computed within each declared stratum (pmx_roles()
strata), because "does this patient stand out?" needs a comparison group
and the whole cohort is the wrong one as soon as a study assigns anything.
The clearest case is a patient given a higher arm's dose: cohort-wide that
dose is thirty other patients' dose, so nothing is reported, and it is only
unusual next to the arm the patient was actually allocated to. Strata holding
fewer than five subjects are scored against the whole cohort instead, since a
scale estimated from four patients describes the four rather than the one
being screened. With no strata declared, every subject is scored against the
cohort.
Examples
data <- pmx_simulated_fixture(30)
roles <- pmx_roles(
id = "ID", time = "TIME", dv = "DV", amt = "AMT", evid = "EVID",
cmt = "CMT", dvid = "DVID", covariates = "WT"
)
synthetic <- suppressWarnings(synpmx_avatar(data, roles, seed = 1))
#> synpmx_avatar(): dropped 9 undeclared column(s): NTIME, TAD, OCC, RATE, MDV, CENS, LIMIT, AGE, SEX.
#> Declare a column in `keep` to carry it through verbatim.
flag_identifiable_subjects(synthetic, roles)
#> PMX outlier / identifiability check: 0 of 30 subjects flagged
#> Flag = a robust outlier in follow-up time, dose count, dose magnitude, or DV value,
#> that is also at least 100% of the group median away from the nearest
#> other subject.
#>
#> Twelve most unusual:
#> subject_id follow_up_time n_doses max_dose max_dv outlier_axes flagged
#> 31 20 2 87 77 FALSE
#> 32 20 2 92 133 FALSE
#> 33 20 2 103 91.6 FALSE
#> 34 20 2 107 101 FALSE
#> 35 20 2 93.6 73 FALSE
#> 36 20 2 88.6 79.8 FALSE
#> 37 20 2 91.9 72.8 FALSE
#> 38 20 2 103 76.8 FALSE
#> 39 20 2 124 67.7 FALSE
#> 40 20 2 114 51.4 FALSE
#> 41 20 2 83.3 89.8 FALSE
#> 42 20 2 111 84.6 FALSE
#> ... 18 more row(s) in the returned table.
#>
#> Source-derived; not releasable unless separately public or privately budgeted.