Column roles are explicit: synpmx does not infer critical PMX
semantics from column names. The declaration is also the complete manifest of
what survives into synthetic data. synpmx_avatar() drops every column not
named by some role, so a column you forget is dropped rather than silently
copied out of a real subject. Name a column in keep to carry it through.
Usage
pmx_roles(
id,
time,
dv,
amt = NULL,
evid,
cmt = NULL,
dvid = NULL,
mdv = NULL,
rate = NULL,
nominal_time = NULL,
tad = NULL,
occasion = NULL,
cens = NULL,
limit = NULL,
addl = NULL,
ii = NULL,
adm = NULL,
routes = NULL,
dose_endpoints = NULL,
covariates = NULL,
strata = NULL,
dose_covariate = NULL,
assigned_dose = NULL,
endpoint_types = NULL,
keep = NULL,
exclude = NULL
)Arguments
- id, time, dv, evid
Required single column names for subject ID, actual time, dependent variable, and event ID.
- amt, cmt, mdv, rate
Optional single column names for amount, compartment, missing-DV indicator, and infusion rate.
- dvid
Endpoint-key column(s). Usually one column. A dataset that labels the same endpoint two ways — a numeric
YTYPEbeside a characterNAME— may declare both,dvid = c("YTYPE", "NAME"). The first is the grouping key; validation checks the rest are a consistent 1:1 mapping with it and errors if they disagree, andsynpmx_avatar()carries all of them through.dvidandcmtanswer different questions.dvidis which endpoint a measurement is;cmtis which compartment a dose enters, and it is read only on event rows. Nothing infers one from the other, so a source with more than one endpoint needsdvid— without it every measurement is treated as one endpoint, sharing a single value transform and a single censoring boundary.synpmx_avatar()refuses rather than let that happen silently.A NONMEM
CMTusually does both jobs, so one column may be named as both roles:pmx_roles(..., cmt = "CMT", dvid = "CMT").cmtandadmmay likewise be one column, for the same reason — a NONMEM dataset has no administration column, and the compartment a dose enters is what says how it was given and which drug it is. Those two are the only permitted overlaps; every other collision is an error.- nominal_time, occasion
Optional time metadata columns.
- tad
Time-after-dose column. It is an output, not an input.
synpmx_avatar()recomputes it from the generated times and the generated dose rows and overwrites whatever the source held, so declaring the role says which column to overwrite and to carry through — the source's values are never used to generate anything.validate_pmx()does read them, and reports (as a non-fatal warning) where the declared column disagrees with time since the most recent dose row. That disagreement is worth knowing: a study may measure TAD from the end of an infusion, from a nominal dose time, or from an assigned dosing occasion rather than the most recent dose.nlmixr2data::nimoDatadisagrees on 45% of its observation rows. Where it does, the synthetic column follows the derivation rather than the source's convention.Two limits. Samples taken before any dose are reported as 0, because
validate_pmx()refuses a negative TAD — not because a baseline sample is genuinely zero hours after a dose it precedes. And whereaddloriiis declared, the derivation cannot see the doses those imply, so the agreement check is skipped and says so.- cens, limit
Optional Monolix-style censoring indicator and other interval-boundary columns.
- addl, ii
Optional additional-dose and interdose-interval columns, in the NONMEM sense: one record plus "and
ADDLmore like it, everyII". Declared together or not at all. Every generator expands them on the way in, so each dose the patient received is a row to the dose skeleton, the derived time after dose and the population fit, and folds the output back on the way out, so the synthetic study keeps its source's encoding. Seepmx_expand_doses().- adm, routes
Optional administration-id column and what its values mean, as
adm = "ADM", routes = c("1" = "iv", "2" = "extravascular"). Declared together or not at all: which id is which route is a convention of the dataset and cannot be read off the numbers, so reading it wrong would put doses in the wrong compartment without failing. Two routes are recognised.ivcovers a bolus and an infusion, told apart per record byraterather than declared twice;extravascularcovers subcutaneous, oral and intramuscular, which are one first-order absorption with an absorption rate constant and a bioavailability. A study dosing both ways is fitted with one model that routes each dose record to its own compartment, so a patient may receive both.admmay name the same column ascmt: a NONMEM dataset has no administration column, and the compartment a dose enters is the administration id. Seedose_endpointsfor the other thing an administration id says — which drug the dose is.- dose_endpoints
Which endpoint each administration id doses, as
dose_endpoints = c("1" = "drug A PK", "2" = "drug B PK"). Needsadm, and names endpoints by theirdvidvalue.Declare it for a study that gives two different drugs and measures a concentration of each. Undeclared, every dose record drives every concentration endpoint's fit and its generated profile, which is right for a parent and its metabolite – one dose, two analytes – and wrong for a combination, where each drug's model would be fitted against the other drug's doses as well as its own.
The two cases cannot be told apart from the data: a metabolite has no dose records of its own, so "this endpoint's doses are the ones marked
2" and "this endpoint has no doses" are the same table. Hence a declaration, on the same argumentroutesmakes.Every administration id present on a dose record must be named, and every endpoint named must be one the fit puts a structural model on — both are checked against the study when the fit runs. A mapping that leaves a concentration with no dose record at all is refused, and that refusal is the signal that the endpoint is a metabolite and this is not the argument for it.
Declared, each drug gets its own dose schedule per arm — its own amounts, its own interval, its own ladder of reductions — each endpoint's fit and generated profile read only that drug's doses, each observation's time after dose is measured from that drug's last dose, and the generated study writes both drugs' dose records back with their own
admvalue and compartment.In a NONMEM dataset the compartment does this job, so declare the column twice:
pmx_roles(cmt = "CMT", adm = "CMT", routes = ..., dose_endpoints = c("1" = "drug A PK", "3" = "drug B PK")).Undeclared,
model_report()says so on any study with more than one concentration, because that is the case worth a line.- covariates
Baseline covariate column names, or
NULL.- strata
Treatment arm, dose group, cohort — any assigned, subject-level stratum, as opposed to a measured characteristic, which is a
covariate. Must be constant within subject; subjects with no recorded value are grouped as their own stratum, with a warning rather than an error. Several columns may be named, and their combination defines the stratum.synpmx_avatar()carries these verbatim from the subject that supplied the event skeleton and uses the stratum to group two things that are protocol properties rather than patient properties: the dose-to-covariate relationship, and the pool of attendance patterns an avatar may draw from. It is not a blending barrier — only route of administration is (seesynpmx_avatar()), so donors are still borrowed across strata to reach the donor floor.The differential-privacy engines model the same columns jointly with the regimen as a released category domain.
- dose_covariate
The covariate the dose is a fixed multiple of, for weight-based or body-surface-area dosing: name the covariate, not the amount. Declaring it tells
synpmx_avatar()to recompute each avatar'samtfrom the avatar's own blended covariate at the anchor's own milligrams-per-unit, instead of copying the anchor's milligrams.This matters twice over. An avatar's covariates are blended while its
amtwould otherwise be copied verbatim, so under proportional dosing the avatar's own mg/kg comes out wrong — every generated patient violates the protocol it claims to follow. And a copied amount is one real patient's real dose, which under proportional dosing discloses that patient's weight exactly.Left
NULL,synpmx_avatar()tries to infer the relationship, and that inference deliberately fails closed: it requires the dose-to-covariate ratio to collapse onto a handful of protocol levels, so a study that rounds doses to vial sizes, or escalates within a patient by ratios that are not quite equal, is refused and the amounts are left alone. Declaring the covariate skips inference entirely and holds each dose row's own ratio, so intra-patient escalation is preserved exactly — three doses at 1, 2 and 4 mg/kg stay at 1, 2 and 4 mg/kg. The run report says which path was taken and, when inference declined, why.- assigned_dose
Differential-privacy engines only. A nominal assigned-dose column reconstructed from the generated regimen.
synpmx_avatar()does not use this — carry the column withkeep.- endpoint_types
What kind of values an endpoint takes, as a named character vector keyed by the endpoint's
dvidvalue — for examplec("PD - Binary" = "binary", "PD - Ordinal" = "ordinal"). Use"DV"as the name when nodvidis declared. Each value is one of"continuous","binary","ordinal", or"integer";"count"is accepted for"integer".This exists because blending is a weighted mean, and a weighted mean of several patients' zeros and ones is a number between them. Without it a binary endpoint comes back continuous — measured on
xgxr::mad, a 0/1 endpoint came back as 600 distinct values from -0.13 to 1.08. Declared or inferred,synpmx_avatar()snaps a"binary"or"ordinal"endpoint onto the levels the source used and rounds an"integer"one to whole numbers.Left
NULL, the type is inferred per endpoint from whether every observed value is a whole number and how many distinct ones there are;pmx_endpoint_types()reports what that inference decided and why. Unlikedose_covariate, this inference is on by default, because the question it asks is answered outright by the data and the repair reproduces granularity the source already had. Declare the type where the data is misleading: an endpoint recorded as whole numbers that is genuinely continuous, or a scale with a level nobody in this cohort happened to hit.- keep
Columns to carry into synthetic data verbatim, copied from the same source subject that supplied the event skeleton, with no blending or synthesis. This is for assigned, subject-defining values you want kept faithful to a subject's dosing — a treatment arm, a dose group, a randomization sequence, or a redundant endpoint label such as a character
NAMEbeside a numericdvid. Because the value comes from the same anchor as the doses, it stays coherent with them. Contrastcovariates, which are blended into new values across neighbours. A kept value is one real subject's real value, so use it only where the source data's own access controls and confidentiality obligations still apply.- exclude
Differential-privacy engines only. Columns removed before private fitting, such as direct identifiers.
synpmx_avatar()does not use this — it drops every undeclared column by default, so not naming a column is how you drop it.
Examples
roles <- pmx_roles(
id = "ID", time = "TIME", dv = "DV", amt = "AMT",
evid = "EVID", cmt = "CMT", tad = "TAD", covariates = "WT"
)
# Two columns for one endpoint, and a treatment arm carried through verbatim.
roles <- pmx_roles(
id = "ID", time = "TIME", dv = "DV", amt = "AMT", evid = "EVID",
dvid = c("YTYPE", "NAME"), covariates = "WT", keep = "ARM"
)
# A study dosed both ways, saying which administration id is which route.
roles <- pmx_roles(
id = "ID", time = "TIME", dv = "DV", amt = "AMT", evid = "EVID",
cmt = "CMT", adm = "ADM", routes = c("1" = "iv", "2" = "extravascular")
)