25  Regulatory and CDISC Context

PKNCA does not build a complete submission dataset for you, but it comes close: its tidy, long-format result table maps directly onto the CDISC PP (Pharmacokinetic Parameters) domain, and as of version 0.13.0 as.data.frame(out_format = "cdisc") translates parameter codes into CDISC standard codes and adds the SDTM time point reference, method, and group identifier columns (see the CDISC output format section below). This page documents that mapping and describes how to prepare outputs for regulatory submissions. For PKNCA’s regulator-facing overview, see PKNCA – an R package for noncompartmental analysis.


25.1 CDISC PP domain alignment

The CDISC PP domain stores one parameter value per row, with the parameter code in PPTESTCD and the original result in PPORRES. With any units table, PKNCA adds the original-unit column PPORRESU; when preferred units or custom conversions are requested, it also fills its standardized-result columns, PPSTRES and PPSTRESU (the corresponding CDISC SDTM variables are PPSTRESC (character), PPSTRESN (numeric), and PPSTRESU). PKNCA’s as.data.frame() output follows exactly this long-format convention.

df <- as.data.frame(o_nca)
names(df)
[1] "Subject"  "start"    "end"      "PPTESTCD" "PPORRES"  "PPANMETH" "exclude" 

The columns map to CDISC PP variables as follows:

PKNCA column CDISC PP variable Notes
PPTESTCD PPTESTCD PKNCA parameter name (auclast); translated to the CDISC code (AUCLST) by out_format = "cdisc"
PPORRES PPORRES Original numeric result (character in CDISC; numeric here)
PPANMETH PPANMETH Analysis method (≥ 0.13.0): imputation and the AUC and half-life methods used
start — Interval start, relative to the first dose; out_format = "cdisc" adds PPSTINT relative to the dose that starts the interval
end — Interval end, relative to the first dose; out_format = "cdisc" adds PPENINT
exclude PPSTAT / PPREASND (or a supplemental qualifier) Non-NA string gives the exclusion reason
Grouping columns (Subject, etc.) USUBJID, PPCAT Pass through unchanged

The PPORRESU column is added whenever the data have units: either unit arguments (concu, timeu, doseu, …) given to PKNCAconc() and PKNCAdose(), from which PKNCAdata() builds the units table automatically (≥ 0.13.0), or a table from pknca_units_table() passed to PKNCAdata(). PPSTRES and PPSTRESU are added only when the units table requests preferred units (concu_pref, timeu_pref, …) or custom conversions. Without units, PPORRES carries the raw numeric value and unit derivation must be done externally. For the full units workflow, see the Units chapter and Unit Assignment and Conversion with PKNCA.

head(df, 10)
# A tibble: 10 × 7
   Subject start   end PPTESTCD       PPORRES PPANMETH                   exclude
   <ord>   <dbl> <dbl> <chr>            <dbl> <chr>                      <chr>  
 1 1           0   Inf auclast       147.     Imputation: start_predose… <NA>   
 2 1           0   Inf cmax           10.5    Imputation: start_predose… <NA>   
 3 1           0   Inf tmax            1.12   Imputation: start_predose… <NA>   
 4 1           0   Inf tlast          24.4    Imputation: start_predose… <NA>   
 5 1           0   Inf clast.obs       3.28   Imputation: start_predose… <NA>   
 6 1           0   Inf tlag            0      Imputation: start_predose… <NA>   
 7 1           0   Inf count_conc     11      Imputation: start_predose… <NA>   
 8 1           0   Inf lambda.z        0.0485 Imputation: start_predose… <NA>   
 9 1           0   Inf r.squared       1.000  Imputation: start_predose… <NA>   
10 1           0   Inf adj.r.squared   1.000  Imputation: start_predose… <NA>   

25.2 Grouping columns as CDISC identifiers

Whatever grouping structure you define in PKNCAconc() — subject ID, analyte, treatment, period — those column names carry through unchanged into as.data.frame(o_nca). They become the basis for USUBJID, PPCAT (analyte/treatment), and other PP domain identifiers.

# Grouping columns appear alongside PPTESTCD and PPORRES
df |>
  select(Subject, start, end, PPTESTCD, PPORRES) |>
  head(6)
# A tibble: 6 × 5
  Subject start   end PPTESTCD  PPORRES
  <ord>   <dbl> <dbl> <chr>       <dbl>
1 1           0   Inf auclast    147.  
2 1           0   Inf cmax        10.5 
3 1           0   Inf tmax         1.12
4 1           0   Inf tlast       24.4 
5 1           0   Inf clast.obs    3.28
6 1           0   Inf tlag         0   

For multi-analyte or multi-period studies, add those variables to the formula in PKNCAconc():

# Example: analyte + period grouping
d_conc_multi <- PKNCAconc(
  my_data,
  conc ~ Time | USUBJID / ANALYTE / PERIOD
)

All grouping variables will appear as columns in the long-format result, ready to be mapped to their CDISC equivalents.


25.3 Extracting results for submission

as.data.frame(o_nca) returns the individual (subject-level) long-format table. This is the primary output for PP domain construction. The PPTESTCD and PPORRES column names are already the CDISC variable names, so no renaming is needed; the PPTESTCD values are PKNCA’s parameter names until they are translated with out_format = "cdisc" (next section):

pp_domain_draft <- as.data.frame(o_nca)

# PPSTRESN is typically the same as PPORRES for numeric results
pp_cdisc <- pp_domain_draft |>
  mutate(PPSTRESN = PPORRES)

head(pp_cdisc)
# A tibble: 6 × 8
  Subject start   end PPTESTCD  PPORRES PPANMETH                exclude PPSTRESN
  <ord>   <dbl> <dbl> <chr>       <dbl> <chr>                   <chr>      <dbl>
1 1           0   Inf auclast    147.   Imputation: start_pred… <NA>      147.  
2 1           0   Inf cmax        10.5  Imputation: start_pred… <NA>       10.5 
3 1           0   Inf tmax         1.12 Imputation: start_pred… <NA>        1.12
4 1           0   Inf tlast       24.4  Imputation: start_pred… <NA>       24.4 
5 1           0   Inf clast.obs    3.28 Imputation: start_pred… <NA>        3.28
6 1           0   Inf tlag         0    Imputation: start_pred… <NA>        0   

The result is in tidy long format: one row per subject per parameter per interval, which is the structure expected by the CDISC PP domain.


25.4 CDISC output format (≥ 0.13.0)

The long-format extraction above keeps PKNCA’s own parameter codes in PPTESTCD — auclast, cmax, lambda.z, and so on. For submission datasets, as.data.frame(out_format = "cdisc") translates them and adds the SDTM PP columns that PKNCA can fill:

  • PPTESTCD holds the CDISC submission code (auclast becomes AUCLST, clast.obs becomes CLST, lambda.z becomes LAMZ) and a new PPTEST column holds the standard parameter name.
  • A new pknca_parameter column, placed just before PPTESTCD, keeps the original PKNCA parameter name.
  • PPSTINT, PPENINT, PPTPTREF, and (for date-time data) PPRFTDTC give every row its time point reference.
  • PPGRPID gives the record group identifier.
df_cdisc <- as.data.frame(o_nca, out_format = "cdisc")
names(df_cdisc)
 [1] "Subject"         "start"           "end"             "pknca_parameter"
 [5] "PPTESTCD"        "PPTEST"          "PPORRES"         "PPANMETH"       
 [9] "exclude"         "PPSTINT"         "PPENINT"         "PPTPTREF"       
[13] "PPGRPID"        
df_cdisc |>
  select(Subject, pknca_parameter, PPTESTCD, PPTEST, PPORRES) |>
  head(8)
  Subject pknca_parameter PPTESTCD                        PPTEST    PPORRES
1       1         auclast   AUCLST      AUC to Last Nonzero Conc 147.234749
2       1            cmax     CMAX                      Max Conc  10.500000
3       1            tmax     TMAX      Time of CMAX Observation   1.120000
4       1           tlast     TLST     Time of Last Nonzero Conc  24.370000
5       1       clast.obs     CLST             Last Nonzero Conc   3.280000
6       1            tlag     TLAG Time Until First Nonzero Conc   0.000000
7       1      count_conc  CNTCONC           Concentration count  11.000000
8       1        lambda.z     LAMZ                      Lambda z   0.048457

25.4.1 Parameter codes and the pknca_parameter column

The translation is many-to-one. CDISC has one “AUC from T1 to T2” code regardless of how the region after Tlast is handled, so aucint.last and aucint.inf.obs both become AUCINT. The pknca_parameter column is then the only column that identifies which PKNCA calculation produced a row, and PPANMETH records the method. The column name is lowercase so that it cannot be mistaken for an SDTM PP variable; drop it before submission.

o_nca_aucint <- pk.nca(PKNCAdata(
  o_conc, o_dose,
  intervals = data.frame(start = 0, end = 48, aucint.last = TRUE, aucint.inf.obs = TRUE)
))
as.data.frame(o_nca_aucint, out_format = "cdisc", filter_requested = TRUE) |>
  filter(Subject == "1") |>
  select(pknca_parameter, PPTESTCD, PPORRES, PPANMETH)
  pknca_parameter PPTESTCD  PPORRES
1     aucint.last   AUCINT 147.2347
2  aucint.inf.obs   AUCINT 193.3842
                                                                   PPANMETH
1   AUC: lin up/log down. Interpolation: dose-aware. Extrapolation: AUClast
2 AUC: lin up/log down. Interpolation: dose-aware. Extrapolation: half-life

Route-dependent parameters resolve to the code for the route recorded in the dose data: cl.obs is CLFO (“Total CL Obs by F”) after an extravascular dose and CLO (“Total CL Obs”) after an intravascular one. The same applies to the volume and MRT parameters.

pknca_cdisc_codes() (≥ 0.13.0) lists every registered code, one row per route or dense/sparse variant, generated from the parameter registry at call time (so it includes parameters registered by other packages):

d_codes <- pknca_cdisc_codes()
nrow(d_codes)
[1] 247
d_codes |>
  filter(parameter %in% c("auclast", "aucint.last", "aucint.inf.obs", "cl.obs", "count_conc"))
       parameter   tier       variant pptestcd_cdisc             pptest_cdisc
1        auclast common        single         AUCLST AUC to Last Nonzero Conc
2    aucint.last common        single         AUCINT        AUC from T1 to T2
3     count_conc common        single        CNTCONC      Concentration count
4 aucint.inf.obs common        single         AUCINT        AUC from T1 to T2
5         cl.obs common extravascular           CLFO        Total CL Obs by F
6         cl.obs common intravascular            CLO             Total CL Obs
  in_ct
1  TRUE
2  TRUE
3 FALSE
4  TRUE
5  TRUE
6  TRUE

The in_ct column says whether each code is a term of the CDISC PKPARMCD codelist. It is checked against the cdiscdata package (a suggested dependency, ≥ 0.2.0); when cdiscdata is not installed, in_ct is NA for every row, with a message. Every "common" tier parameter has a real PKPARMCD code except count_conc, sparse_auc_se, and cl.int.inf.obs, which have no CDISC PP code; a parameter with no CDISC equivalent keeps a sponsor-defined code.

25.4.2 The method column (PPANMETH)

Every pk.nca() result has a PPANMETH column (≥ 0.13.0) describing the analysis methods used for that parameter: the imputation, the AUC integration method, and, for aucint and aumcint parameters, whether the calculation was dose-aware and how the region after Tlast was handled (in the example above, Extrapolation: AUClast for aucint.last, which stops at Tlast, and Extrapolation: half-life for aucint.inf.obs). A parameter with no method of its own, such as cmax, shows only the imputation, or nothing when no imputation was used. When a calculation function sets no method at run time, PPANMETH falls back to the parameter’s registered formula_note.

25.4.3 Time point references

Every row of the CDISC output, not only interval-based parameters, gets PPSTINT and PPENINT (the interval start and end as ISO 8601 durations, such as "PT0H" and "PT24H") relative to the reference named in PPTPTREF. The reference is the last dose at or before the interval start ("LAST DOSE PRIOR TO INTERVAL"), or the first concentration for a subject without a dose ("FIRST OBSERVATION"). PPENINT is NA for an interval to infinity, as in this single-dose analysis:

df_cdisc |>
  select(Subject, start, end, PPTESTCD, PPSTINT, PPENINT, PPTPTREF) |>
  head(3)
  Subject start end PPTESTCD PPSTINT PPENINT                    PPTPTREF
1       1     0 Inf   AUCLST    PT0H    <NA> LAST DOSE PRIOR TO INTERVAL
2       1     0 Inf     CMAX    PT0H    <NA> LAST DOSE PRIOR TO INTERVAL
3       1     0 Inf     TMAX    PT0H    <NA> LAST DOSE PRIOR TO INTERVAL

When the concentration and dose times are date-times, PPRFTDTC gives the reference’s date-time, so it no longer has to be derived outside PKNCA. The Date-Time Data and Time References chapter shows these columns for a multiple-dose study with date-time data.

25.4.4 Record group identifier (PPGRPID)

PPGRPID identifies the rows of one interval: "<prefix1><value1>.<prefix2><value2>.I<nn>", built from the grouping columns named in grpid_cols and the number of the row’s interval within each combination of the subject, the analyte, and those columns. Without grpid_cols it is just the interval number ("I01" above). grpid_cols is a named character vector of PKNCAconc() grouping columns (other than the subject and analyte), each with the text written before its value; grpid_numeric names the columns whose values are whole numbers, such as the period, to be written as that number. Both can be given to PKNCAdata(), as the default for every output of one analysis, or to as.data.frame().

For a two-period crossover (built here by repeating the Theoph data as a second period), with two intervals per period:

d_conc_xover <- bind_rows(
  as.data.frame(Theoph) |> mutate(Period = 1),
  as.data.frame(Theoph) |> mutate(Period = 2)
)
d_dose_xover <- d_conc_xover |>
  group_by(Period, Subject) |>
  summarise(Dose = Dose[1] * Wt[1], Time = 0, .groups = "drop")

o_data_xover <- PKNCAdata(
  PKNCAconc(d_conc_xover, conc ~ Time | Period + Subject),
  PKNCAdose(d_dose_xover, Dose ~ Time | Period + Subject),
  intervals = data.frame(start = 0, end = c(24, Inf), auclast = TRUE),
  grpid_cols = c(Period = "P"),
  grpid_numeric = "Period"
)
as.data.frame(pk.nca(o_data_xover), out_format = "cdisc") |>
  filter(Subject == "1") |>
  select(Period, Subject, start, end, PPTESTCD, PPSTINT, PPENINT, PPGRPID)
  Period Subject start end PPTESTCD PPSTINT PPENINT PPGRPID
1      1       1     0  24   AUCLST    PT0H   PT24H  P1.I01
2      1       1     0 Inf   AUCLST    PT0H    <NA>  P1.I02
3      2       1     0  24   AUCLST    PT0H   PT24H  P2.I01
4      2       1     0 Inf   AUCLST    PT0H    <NA>  P2.I02

Within each subject and period, the intervals are numbered from 1 by start and then end, and every parameter of one interval shares the number. The number is written with at least two digits (more when needed) so that the text sorts in time order. grpid_cols = character() in as.data.frame() gives the interval-only identifier regardless of the PKNCAdata() default.

25.4.5 Codes for custom parameters

Custom parameters registered with add.interval.col() declare their CDISC codes through its pptestcd_cdisc and pptest_cdisc arguments. Each is a character string; a list with one element named route holding a named list of codes per route (for route-dependent parameters such as CL, VZ, MRT, and VSS); or a list of exactly the elements dense and sparse. A parameter without a declared code keeps its PKNCA name in the CDISC output. See the Writing Custom Parameter Functions chapter.


25.5 The exclude column

The exclude column records whether a result was flagged for exclusion and why. When exclude is NA, the result is valid. When it is a non-NA character string, it contains a human-readable exclusion reason.

# How many rows are flagged for exclusion?
table(is.na(df$exclude))

TRUE 
 240 
# Show any excluded rows
df |> filter(!is.na(exclude)) |> head()
# A tibble: 0 × 7
# ℹ 7 variables: Subject <ord>, start <dbl>, end <dbl>, PPTESTCD <chr>,
#   PPORRES <dbl>, PPANMETH <chr>, exclude <chr>

Exclusions are set programmatically via the exclude_nca_* family of functions (e.g. exclude_nca_max.aucinf.pext(), exclude_nca_min.hl.r.squared()), or manually via exclude(). The rule functions supply their own exclusion reason text (e.g. exclude_nca_min.hl.r.squared(0.9) records r.squared < 0.9), so no reason= argument is needed. For regulatory submissions, include all rows in the PP domain — both included and excluded. Exclusion is typically represented via PPSTAT/PPREASND (status and reason not done) or a supplemental qualifier; PKNCA’s exclude column provides the source text for whichever convention your submission uses. Exclusion workflows are covered in the Post-Processing chapter and vignette, and data-quality rules such as Tmax coverage and missing samples in the Data Quality Checks chapter.

# Apply standard exclusion rules and inspect excluded rows
o_nca_excl <- o_nca |>
  exclude(FUN = exclude_nca_min.hl.r.squared(0.9)) |>
  exclude(FUN = exclude_nca_max.aucinf.pext(20))

df_excl <- as.data.frame(o_nca_excl)
df_excl |> filter(!is.na(exclude)) |> select(Subject, PPTESTCD, PPORRES, exclude) |> head()
# A tibble: 3 × 4
  Subject PPTESTCD    PPORRES exclude     
  <ord>   <chr>         <dbl> <chr>       
1 1       aucinf.obs   215.   aucpext > 20
2 1       aucpext.obs   31.5  aucpext > 20
3 1       cl.obs         1.49 aucpext > 20

25.6 Summary table for reports

summary(o_nca) returns a wide-format table suitable for clinical study reports. Each row is a group/interval and each parameter gets a column, with cells formatted as “point [spread]” (geometric mean [CV%] by default):

summary(o_nca)
 start end  N     auclast        cmax               tmax                 tlag
     0 Inf 12 98.7 [22.5] 8.65 [17.0] 1.14 [0.630, 3.55] 0.000 [0.000, 0.000]
        count_conc   half.life aucinf.obs aucpext.obs      cl.obs
 11.0 [11.0, 11.0] 8.18 [2.12] 115 [28.4] 13.8 [6.34] 2.74 [27.9]

Caption: auclast, cmax, aucinf.obs, cl.obs: geometric mean and geometric coefficient of variation; tmax, tlag, count_conc: median and range; half.life, aucpext.obs: arithmetic mean and standard deviation; N: number of subjects

The summary uses the PKNCA.set.summary() configuration for each parameter type. You can customize which statistics are reported and their formatting by calling PKNCA.set.summary() before calling summary().


25.7 Reproducibility: pinning options and environment

For regulatory analyses, the PKNCA option set used during the run must be documented. PKNCA.options() with no arguments returns the full current option set:

# Capture the full option set at the start of an analysis script
analysis_options <- PKNCA.options()

# Confirm a key option
analysis_options$auc.method      # trapezoidal method
[1] "lin up/log down"
analysis_options$conc.blq        # BLQ handling
$first
[1] "keep"

$middle
[1] "drop"

$last
[1] "keep"

Set any non-default options at the top of the analysis script, before any data is processed:

# Example: force the plain linear trapezoidal rule everywhere
PKNCA.options(auc.method = "linear")

For full reproducibility, record the session environment alongside the option snapshot:

# Save at the end of the analysis script
sessionInfo()
R version 4.6.1 (2026-06-24)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.5 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: UTC
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] dplyr_1.2.1       PKNCA_0.12.1.9002

loaded via a namespace (and not attached):
 [1] jsonlite_2.0.0    compiler_4.6.1    tidyselect_1.2.1  Rcpp_1.1.2       
 [5] tidyr_1.3.2       yaml_2.3.12       fastmap_1.2.0     lattice_0.22-9   
 [9] R6_2.6.1          generics_0.1.4    knitr_1.52        htmlwidgets_1.6.4
[13] backports_1.5.1   conflicted_1.2.0  checkmate_2.3.4   tibble_3.3.1     
[17] units_1.0-1       pillar_1.11.1     rlang_1.3.0       utf8_1.2.6       
[21] cachem_1.1.0      cdiscdata_0.2.0   xfun_0.62         otel_0.2.0       
[25] memoise_2.0.1     cli_3.6.6         withr_3.0.3       magrittr_2.0.5   
[29] digest_0.6.39     grid_4.6.1        lifecycle_1.0.5   nlme_3.1-169     
[33] vctrs_0.7.3       evaluate_1.0.5    glue_1.8.1        rmarkdown_2.32   
[37] purrr_1.2.2       tools_4.6.1       pkgconfig_2.0.3   htmltools_0.5.9  

For validated environments, use renv to lock package versions:

# Lock the current environment
renv::snapshot()

# Restore a locked environment on a validation system
renv::restore()

Together, PKNCA.options(), sessionInfo(), and renv provide the three layers of reproducibility documentation expected for regulatory NCA submissions.

25.7.1 Provenance of results

PKNCA objects carry a provenance record (a hash of the object, the creation time, and the R and PKNCA versions), and checkProvenance() confirms that an object is unchanged since PKNCA created it:

checkProvenance(o_nca)
[1] TRUE

A PKNCAresults object changed after the run by filter(), mutate(), group_by(), ungroup(), the joins, exclude(), or normalize() no longer vouches for itself (≥ 0.13.0): its provenance becomes a marker such as "filtered from <hash>" and checkProvenance() returns FALSE. A call that leaves the object identical keeps its provenance.

checkProvenance(o_nca_excl)
[1] FALSE

For a submission, keep the unmodified PKNCAresults object with the analysis outputs, so that the reported numbers can be traced to an intact run, and record the exclusions applied to it in the analysis script.