7. Canonical pharmacometrics event semantics¶
Field: pharmacometrics and clinical pharmacology
Analysis: deterministic event canonicalization, ordering, expansion, and audit
Records: dose, observation, covariate, infusion start, and infusion stop
Download the complete, pre-executed notebook
7.1. Domain and problem¶
Pharmacometric data are event streams, not merely rectangular numeric tables. A dose changes a model state, an observation reads a model state, a covariate record updates context, a reset clears state, and an infusion stop changes the input rate. These distinctions become especially important when multiple records share a time or when a compact source row implies additional events.
This tutorial converts ordinary Python mappings into PyMixEF’s immutable
pymixef.pharmacometrics.events.EventTable. It then makes ordering,
ADDL/II dose expansion, finite-infusion expansion, source-row provenance,
and amount-status semantics inspectable.
For a finite infusion with amount \(D\) and duration \(\tau\), canonicalization derives the constant rate and terminal event from
For additional doses, the generated time of dose \(k\) is \(t_k=t_0+k\,\mathrm{II}\) for \(k=1,\ldots,\mathrm{ADDL}\). These equations are simple; preserving their source-row provenance and same-time priority is the important part of the event contract.
7.2. What you will learn¶
By the end of the tutorial, you will be able to:
canonicalize dose, observation, covariate, and infusion records;
inspect deterministic subject/time/event ordering;
understand which state a same-time observation sees;
materialize additional doses and generated infusion-stop events;
trace every generated row to its source row through the audit log;
distinguish a recorded zero dose, an explicitly unknown dose, and an amount that is structurally not applicable; and
use validation errors as an explicit part of an event-data workflow.
7.3. Dataset and event snapshot¶
Item |
Value |
|---|---|
Subjects |
|
Source rows |
7 |
Expanded rows |
10 |
Generated rows |
3 |
P01 regimen |
100-unit bolus at 0 h plus 2 additional doses every 12 h |
P02 regimen |
60-unit infusion over 2 h |
Derived P02 rate |
30 units/hour |
P01 observation times |
0 h and 24 h |
P02 observation times |
0 h and 3 h |
P01 covariate update |
Weight 72 at 12 h |
The example deliberately places observations at the same time as a dose or infusion start so that ordering is visible rather than implicit.
7.4. Runnable core analysis¶
The following excerpt reproduces the central event workflow.
from pymixef.pharmacometrics import canonicalize_events
source_records = [
{
"ID": "P01",
"TIME": 0,
"EVID": "dose",
"AMT": 100,
"CMT": "central",
"ADDL": 2,
"II": 12,
"ROW_ID": "dose-p01",
},
{
"ID": "P01",
"TIME": 0,
"EVID": 0,
"DV": 4.8,
"CMT": "central",
"ROW_ID": "obs-p01-0",
},
{
"ID": "P01",
"TIME": 12,
"EVID": "covariate",
"COVARIATES": {"weight": 72},
"ROW_ID": "wt-p01",
},
{
"ID": "P01",
"TIME": 24,
"EVID": 0,
"DV": 1.1,
"CMT": "central",
"ROW_ID": "obs-p01-24",
},
{
"ID": "P02",
"TIME": 0,
"EVID": "dose",
"AMT": 60,
"DUR": 2,
"CMT": "central",
"ROW_ID": "inf-p02",
},
{
"ID": "P02",
"TIME": 0,
"EVID": 0,
"DV": 0,
"CMT": "central",
"ROW_ID": "obs-p02-0",
},
{
"ID": "P02",
"TIME": 3,
"EVID": 0,
"DV": 2.2,
"CMT": "central",
"ROW_ID": "obs-p02-3",
},
]
events = canonicalize_events(source_records)
same_time_p01 = [
event.kind
for event in events
if event.subject_id == "P01" and event.time == 0
]
assert same_time_p01 == ["dose", "observation"]
expanded = events.expand_additional().expand_infusions()
generated = [event for event in expanded if event.generated]
assert len(expanded) == 10
assert len(generated) == 3
7.5. Step-by-step analysis¶
7.5.1. 1. Canonicalize heterogeneous source rows¶
canonicalize_events(...) accepts the source mappings, validates them, assigns
event kinds, derives the finite-infusion rate, retains source identifiers, and
returns a deterministically sorted EventTable.
The canonical rows are:
Subject |
Time (h) |
Kind |
Amount |
Rate |
Row ID |
|---|---|---|---|---|---|
P01 |
0 |
dose |
100 |
— |
|
P01 |
0 |
observation |
— |
— |
|
P01 |
12 |
covariate |
— |
— |
|
P01 |
24 |
observation |
— |
— |
|
P02 |
0 |
infusion start |
60 |
30 |
|
P02 |
0 |
observation |
— |
— |
|
P02 |
3 |
observation |
— |
— |
|
The source row for P02 supplies amount and duration, so PyMixEF derives a
constant rate of 60 / 2 = 30.
7.5.2. 2. Inspect the canonical timeline¶
Canonical event timeline by subject. Event type is encoded by color and marker while time remains on the horizontal axis.¶
Interpretation. The timeline exposes both schedules and the same-time dose/observation pairs at zero. Small vertical offsets separate markers only for readability; they neither modify the time nor override canonical priority.
7.5.3. 3. Apply the same-time rule¶
The complete PyMixEF event priority is:
reset;
covariate update;
infusion stop;
bolus dose or infusion start;
observation; and
other.
Source position is the final stable tie-breaker. Therefore, a same-time observation in this convention sees the post-dose state. This is a scientific modeling choice that should be reconciled with the protocol, data derivation, and intended software comparison.
7.5.4. 4. Expand compact dosing instructions¶
Expansion is functional: it returns a new event table and does not mutate the source table.
expanded = events.expand_additional().expand_infusions()
assert [
event.kind
for event in expanded
if event.subject_id == "P01" and event.time == 24
] == ["dose", "observation"]
The expanded schedule adds:
dose-p01:addl:1at 12 h, sourced fromdose-p01;dose-p01:addl:2at 24 h, sourced fromdose-p01; andinf-p02:infusion-stopat 2 h, sourced frominf-p02.
Canonical source rows and deterministic expansions. Circles indicate source rows and stars indicate generated rows.¶
Interpretation. P01 gains scheduled doses at 12 and 24 hours, while P02
gains the infusion stop at 2 hours. Every generated event has a deterministic
row identifier and retains the originating source_row_id.
7.5.5. 5. Read provenance from the audit log¶
The audit describes transformations directly, so a reviewer does not need to infer them from row-count differences.
from collections import Counter
audit_counts = Counter(entry.code for entry in expanded.audit)
assert audit_counts["EVENT-ADDL-EXPANDED-001"] == 2
assert audit_counts["EVENT-INFUSION-EXPANDED-001"] == 1
generated_audit = [
entry.to_dict()
for entry in expanded.audit
if entry.code
in {
"EVENT-ADDL-EXPANDED-001",
"EVENT-INFUSION-EXPANDED-001",
}
]
The saved audit counts are:
Audit code |
Count |
Meaning in this example |
|---|---|---|
|
7 |
One entry for every source row |
|
2 |
Two generated P01 doses |
|
1 |
P02 rate derived from amount/duration |
|
1 |
One generated P02 infusion stop |
Canonicalization and expansion audit actions. Machine-readable action counts summarize how the event table was constructed.¶
Interpretation. The seven canonicalization records match the seven source rows. The two additional-dose and one infusion-stop entries match the three generated rows, while the rate-derived entry records how the infusion input was resolved.
7.5.6. 6. Preserve missingness semantics¶
A missing dose amount is not automatically zero. PyMixEF distinguishes three states:
from pymixef.pharmacometrics import EventValidationError
unknown = canonicalize_events(
[{"ID": "P03", "TIME": 0, "EVID": 1, "AMT_STATUS": "unknown"}]
)
zero = canonicalize_events(
[{"ID": "P03", "TIME": 0, "EVID": 1, "AMT": 0}]
)
observation = canonicalize_events(
[{"ID": "P03", "TIME": 1, "EVID": 0, "DV": 0.5}]
)
assert (unknown[0].amount, unknown[0].amount_status) == (None, "unknown")
assert (zero[0].amount, zero[0].amount_status) == (0.0, "recorded")
assert (observation[0].amount, observation[0].amount_status) == (
None,
"not-applicable",
)
try:
canonicalize_events([{"ID": "P03", "TIME": 0, "EVID": 1}])
except EventValidationError as error:
print(error)
An ambiguous dose without AMT is refused unless
AMT_STATUS="unknown" is explicit. The saved message is:
dose records without AMT require AMT_STATUS='unknown'; use AMT=0 for a
recorded zero dose [row=0]
7.6. Key saved results¶
Canonical subjects:
("P01", "P02").Original event-table length:
7.Expanded event-table length:
10.Generated-event count:
3.P02 derived infusion rate:
30.0.P01 additional-dose times:
12.0and24.0.P02 generated infusion-stop time:
2.0.P01 ordering at 24 h:
["dose", "observation"].All expansion and same-time assertions passed.
Unknown dose: amount
None, statusunknown.Recorded zero dose: amount
0.0, statusrecorded.Observation: amount
None, statusnot-applicable.
7.7. API map¶
Task |
Public API |
Result used here |
|---|---|---|
Canonicalize records |
||
Catch invalid event semantics |
Explicit refusal |
|
List subjects |
Stable subject tuple |
|
Iterate/index events |
|
|
Expand |
|
New event table |
Expand finite infusions |
|
Generated stop events |
Select one subject |
Subject-level event table |
|
Inspect transformations |
||
Serialize an audit row |
Plain Python mapping |
|
Return ordinary records |
List of mappings |
Important CanonicalEvent fields
used here are subject_id, time, kind,
amount, rate, row_id, source_row_id, generated, and amount_status.
7.8. Exercises¶
Add a reset-and-dose record and inspect its position relative to a same-time covariate update.
Supply inconsistent
AMT,RATE, andDURvalues and study the refusal message.Use
expanded.for_subject("P01")to create a subject-only table.Round-trip
expanded.to_records()throughcanonicalize_events(...)and compare subject/time/event ordering.Add two observations at the same subject and time, then verify that source position remains the stable tie-breaker.
Interpretation boundaries
Use canonicalization as an auditable contract between source data and numerical modeling. Before simulation, confirm that the documented same-time convention, units, compartment mapping, and protocol derivations match the intended analysis. An explicitly unknown amount can be retained for review but must be resolved before it can drive a numerical state update. The current simulator also refuses steady-state and modeled or negative infusion-rate semantics rather than approximating them silently.