Score how many patients share each patient's event skeleton
Source:R/compare.R
skeleton_uniqueness.RdA source-side screen for patients that are alone, the complement to
flag_identifiable_subjects(), which finds patients that are extreme. A
patient can be perfectly ordinary on every distribution and still hold the
only copy of their visit schedule, and synpmx_avatar() copies the anchor's
event skeleton verbatim, so such a patient hands an identifying schedule to
every avatar anchored on it.
Arguments
- data
A PMX dataset – normally the source.
- roles
Explicit roles from
pmx_roles().- coarsen_time
Score the coarsened visit grid
synpmx_avatar()would build (TRUE) or the recorded times as given (FALSE, the default).
Value
A pmx_skeleton_uniqueness data frame, most-exposed first, one row
per patient: subject_id, n_obs, n_doses, n_share_dosing,
n_share_schedule, n_share_rarest_time, n_share_obs_count (each
counting the patients sharing that property, including this one),
n_visits and nearest_set_diff (how many visit slots – one endpoint at
one time – separate this patient from the closest other one),
unique_schedule (TRUE when n_share_schedule == 1), and why_unique.
Attributes n_unique_schedule, n_unique_dose_signature,
n_unique_obs_count, n_unshared_time, min_class, and coarsened
summarize the cohort; summary_table, sharing_table and by_endpoint
hold the tables print() shows.Reading the count
n_share_schedule == 1 is exact-set equality, and on a real study that is a
harsh test: with forty visit slots and twenty patients, two patients who
differ by one missed sample score as "unique" exactly like two with nothing
in common. nearest_set_diff is what separates those cases, and the printed
output states it alongside the count. A cohort can read 15 of 21 unique while
every one of those 15 is a single missing sample away from somebody else.
The by_endpoint table says which endpoint is responsible, since a
schedule is only as shared as its least shared part: a study measuring a
biomarker at every visit and PK at some of them is unique on the pooled
schedule the moment one PK sample is missing.
None of this is something generation can lower — it is a property of the
source. What generation controls is whether an avatar ends up carrying one
of these schedules, which pmx_masking_report() reports as "avatars keeping
their anchor's own visit set".
Details
Three questions are asked of every patient, and the answer to each is a count of how many patients share that property, the patient included. A count of 1 means "nobody else":
n_share_schedule– who else was observed at exactly this list of times? This is the fingerprint, becausesynpmx_avatar()copies the anchor's event skeleton verbatim. Under nominal visit times the count is large, since the schedule is protocol-driven; under actual recorded times it is near-universally 1. Coarsening collapses the second case into the first.n_share_obs_count– who else has this many observations? Coarsening cannot change a count, so this is what survives it: missed visits, early discontinuation, and follow-up that has not reached the later visits.n_share_dosing– who else has this dose structure and these dose amounts? This is the fullpmx_roles()event signature and it does not include observation times; it is the key donor compatibility uses. Weight-based dosing or per-patient titration makes it unique regardless of schedule, and coarsening does not change that either.
n_share_rarest_time splits the schedule count by cause, which matters
because the two causes have opposite remedies. A patient whose schedule is
unique and whose rarest single time was shared with nobody
(n_share_rarest_time == 1) was sampled at a one-off moment: a time grid is
meant to absorb that, and declaring nominal_time is the fix. A patient
whose schedule is unique while every individual time is shared
(n_share_rarest_time >= 2) has visits missing rather than moved, and no
grid at any resolution touches it. why_unique states which.
Before or after coarsening
By default this scores the times exactly as they appear in data.
synpmx_avatar() snaps the source onto a shared visit grid first
(coarsen_time = TRUE, its default) and the numbers it records in
pmx_settings are therefore post-coarsening. Pass coarsen_time = TRUE
here to score the same grid the generator would build, and run it both ways
to see how much of the exposure coarsening actually removed. The printed
header always says which of the two you are looking at.
Run it on the source, before generating. It is a heuristic screen, not a
privacy guarantee, and is marked "restricted_not_releasable".
See also
plot_pmx_schedule() for the same information as a picture,
flag_identifiable_subjects(), synpmx_avatar().
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"
)
skeleton_uniqueness(data, roles)
#> PMX schedule-uniqueness screen
#> Scored on the recorded times AS GIVEN, before any coarsening.
#> `synpmx_avatar()` coarsens first by default, so run this again with
#> `coarsen_time = TRUE` to see what the grid removes.
#>
#> Every patient shares their observation schedule with somebody. Nothing to
#> do.
#>
#> This is a property of the SOURCE, and nothing in generation can lower it.
#> What generation controls is whether an avatar ends up with one of these
#> schedules -- that is `pmx_masking_report()`'s "avatars keeping their
#> anchor's own visit set", which should be near 0% however high the count
#> above is.
#>
#> Patients whose ... n % of cohort
#> Observation schedule nobody else has 0 0
#> ... a one-off observation time 0 0
#> ... the set of visits attended 0 0
#> Observation count nobody else has 0 0
#> Dosing nobody else has 30 100
#>
#> How crowded is each schedule (1 = nobody else has it):
#> Patients sharing that schedule Patients % of cohort
#> 30 30 100
#>
#> Which endpoint is doing it:
#> endpoint patients distinct visit sets patients alone on theirs
#> cp 30 1 0
#> pd 30 1 0
#>
#> One row per patient is in the returned data frame; `plot_pmx_schedule()`
#> draws the same cohort. Source-derived; not releasable unless separately
#> public or privately budgeted.
skeleton_uniqueness(data, roles, coarsen_time = TRUE)
#> PMX schedule-uniqueness screen
#> Scored AFTER coarsening, on the shared visit grid `synpmx_avatar()` builds.
#> These are the numbers a run reports.
#>
#> Every patient shares their observation schedule with somebody. Nothing to
#> do.
#>
#> This is a property of the SOURCE, and nothing in generation can lower it.
#> What generation controls is whether an avatar ends up with one of these
#> schedules -- that is `pmx_masking_report()`'s "avatars keeping their
#> anchor's own visit set", which should be near 0% however high the count
#> above is.
#>
#> Patients whose ... n % of cohort
#> Observation schedule nobody else has 0 0
#> ... a one-off observation time 0 0
#> ... the set of visits attended 0 0
#> Observation count nobody else has 0 0
#> Dosing nobody else has 30 100
#>
#> How crowded is each schedule (1 = nobody else has it):
#> Patients sharing that schedule Patients % of cohort
#> 30 30 100
#>
#> Which endpoint is doing it:
#> endpoint patients distinct visit sets patients alone on theirs
#> cp 30 1 0
#> pd 30 1 0
#>
#> One row per patient is in the returned data frame; `plot_pmx_schedule()`
#> draws the same cohort. Source-derived; not releasable unless separately
#> public or privately budgeted.