From Results to Report: Building Clean NCA Summary Tables

Transform PKNCAresults into clean, report-ready tables. Filter, reshape, summarize, and document NCA outputs clearly and reproducibly.
Tip

Big idea: NCA output is structured for computation, not communication. Your job is to reshape it into clear, auditable summary tables.

Learning Objectives

By the end of this lesson, you will be able to:

  • Extract subject-level parameters from a PKNCAresults object.
  • Build clean subject-level exposure tables.
  • Compute descriptive statistics (mean, SD, CV%, geometric mean).
  • Reshape tables for flexible reporting.
  • Document units and calculation assumptions explicitly.

Key Ideas

  • Raw NCA output is long format and parameter-driven.
  • Reporting tables are typically wide format and subject-centered.
  • Calculation and reporting logic must remain separate.
  • Units must be explicit (e.g., L/h/kg vs L/h).
  • Extrapolated AUC should be interpreted cautiously.

Setup: Generate NCA Results

library(tidyverse)
library(PKNCA)

data(Theoph)

theoph_conc <- as_tibble(Theoph) %>%
  transmute(ID = Subject, TIME = Time, CONC = conc)

theoph_dose <- as_tibble(Theoph) %>%
  distinct(Subject, Dose) %>%
  transmute(ID = Subject, TIME = 0, DOSE = Dose)

conc_obj <- PKNCAconc(CONC ~ TIME | ID, data = theoph_conc)
dose_obj <- PKNCAdose(DOSE ~ TIME | ID, data = theoph_dose)

intervals_df <- data.frame(
  start = 0,
  end = Inf,
  auclast = TRUE,
  aucinf.obs = TRUE,
  cmax = TRUE,
  half.life = TRUE,
  cl.obs = TRUE
)

nca_data <- PKNCAdata(conc_obj, dose_obj, intervals = intervals_df)
results_obj <- pk.nca(nca_data)

raw_df <- as.data.frame(results_obj)

Step 1: Inspect Available Parameters

Before reshaping, always inspect what was computed:

sort(unique(raw_df$PPTESTCD))
 [1] "adj.r.squared"       "aucinf.obs"          "auclast"            
 [4] "cl.obs"              "clast.obs"           "clast.pred"         
 [7] "cmax"                "half.life"           "lambda.z"           
[10] "lambda.z.n.points"   "lambda.z.time.first" "lambda.z.time.last" 
[13] "r.squared"           "span.ratio"          "tlast"              
[16] "tmax"               

Confirm expected parameters are present before building tables.


Step 2: Build a Subject-Level Table

We pivot to wide format for reporting clarity.

subject_tbl <- raw_df %>%
  filter(PPTESTCD %in% c("cmax", "auclast", "aucinf.obs", "half.life", "cl.obs")) %>%
  select(ID, PPTESTCD, PPORRES) %>%
  pivot_wider(names_from = PPTESTCD, values_from = PPORRES)

subject_tbl %>% head()
# A tibble: 6 × 6
  ID    auclast  cmax half.life aucinf.obs cl.obs
  <ord>   <dbl> <dbl>     <dbl>      <dbl>  <dbl>
1 6        71.7  6.44      7.89       82.2 0.0487
2 7        88.0  7.09      7.85      101.  0.0490
3 8        86.8  7.56      8.51      102.  0.0443
4 11       77.9  8         7.26       86.9 0.0566
5 3        95.9  8.2       6.77      106.  0.0427
6 2        88.7  8.33      6.66       97.4 0.0452

Now each row = one subject, each column = one parameter.


Important: Label Units Explicitly

Note

In this example:

  • Dose is in mg/kg
  • Concentration is in mg/L
  • Time is in hours

Therefore:

  • AUC → mg·h/L
  • Clearance (cl.obs) → L/h/kg

Always include units in reporting tables.


Step 3: Compute Descriptive Statistics

Mean, SD, CV%

summary_tbl <- subject_tbl %>%
  summarise(
    n = n(),
    mean_cmax = mean(cmax, na.rm = TRUE),
    sd_cmax   = sd(cmax, na.rm = TRUE),
    cv_cmax   = 100 * sd_cmax / mean_cmax,
    mean_auc  = mean(aucinf.obs, na.rm = TRUE),
    sd_auc    = sd(aucinf.obs, na.rm = TRUE),
    cv_auc    = 100 * sd_auc / mean_auc
  )

summary_tbl
# A tibble: 1 × 7
      n mean_cmax sd_cmax cv_cmax mean_auc sd_auc cv_auc
  <int>     <dbl>   <dbl>   <dbl>    <dbl>  <dbl>  <dbl>
1    12      8.76    1.47    16.8     119.   38.2   32.0

Geometric Mean (Common for AUC & Cmax)

geo_tbl <- subject_tbl %>%
  summarise(
    geo_mean_auc  = exp(mean(log(aucinf.obs), na.rm = TRUE)),
    geo_mean_cmax = exp(mean(log(cmax), na.rm = TRUE))
  )

geo_tbl
# A tibble: 1 × 2
  geo_mean_auc geo_mean_cmax
         <dbl>         <dbl>
1         115.          8.65

Geometric means are commonly used in bioequivalence contexts.


Step 4: Long Format for Flexible Reporting

Long format allows grouped summaries and easier plotting.

report_tbl <- subject_tbl %>%
  pivot_longer(
    cols = -ID,
    names_to = "parameter",
    values_to = "value"
  )

report_tbl %>% head(12)
# A tibble: 12 × 3
   ID    parameter     value
   <ord> <chr>         <dbl>
 1 6     auclast     71.7   
 2 6     cmax         6.44  
 3 6     half.life    7.89  
 4 6     aucinf.obs  82.2   
 5 6     cl.obs       0.0487
 6 7     auclast     88.0   
 7 7     cmax         7.09  
 8 7     half.life    7.85  
 9 7     aucinf.obs 101.    
10 7     cl.obs       0.0490
11 8     auclast     86.8   
12 8     cmax         7.56  

Grouped summary:

report_summary <- report_tbl %>%
  group_by(parameter) %>%
  summarise(
    n = n(),
    mean = mean(value, na.rm = TRUE),
    sd   = sd(value, na.rm = TRUE),
    cv   = 100 * sd / mean
  )

report_summary
# A tibble: 5 × 5
  parameter      n     mean       sd    cv
  <chr>      <int>    <dbl>    <dbl> <dbl>
1 aucinf.obs    12 119.     38.2      32.0
2 auclast       12 101.     23.5      23.3
3 cl.obs        12   0.0411  0.00981  23.9
4 cmax          12   8.76    1.47     16.8
5 half.life     12   8.18    2.12     25.9

Worked Example (Conceptual)

Suppose regulatory guidance requires:

  • Mean ± SD for \(C_{max}\)
  • Geometric mean for \(AUC_{0-\infty}\)
  • CV% for exposure metrics
  • Explicit software documentation

A clean reporting statement might read:

Mean Cmax was X ± Y mg/L.
Geometric mean AUCinf was Z mg·h/L (CV% = Q%).
Calculations performed using PKNCA (version X.X.X) with documented trapezoidal interpolation.

Notice:

  • Units are explicit
  • Parameter definitions are clear
  • Software and method are documented

Strategies

  • Always build a clean subject-level table first.
  • Compute summaries programmatically.
  • Separate calculation scripts from report templates.
  • Document interpolation method and software version.
  • Store intermediate tables for audit traceability.

Common Mistakes

Warning
  • Editing summary statistics manually in Word or Excel.
  • Forgetting to verify parameter names before pivoting.
  • Comparing L/h/kg to L/h without unit awareness.
  • Reporting AUCinf when extrapolation is excessive.

Practice Problems

Executable

  1. Add tmax to the subject-level table.
  2. Compute median and IQR for half-life.
  3. Create a table that rounds all exposure metrics to 2 decimals.

Conceptual

  1. Why must calculation logic remain separate from reporting logic?
  2. Why should units always appear in tables?

1. Add tmax:

subject_tbl2 <- raw_df %>%
  filter(PPTESTCD %in% c("cmax", "tmax", "auclast", "aucinf.obs", "half.life")) %>%
  select(ID, PPTESTCD, PPORRES) %>%
  pivot_wider(names_from = PPTESTCD, values_from = PPORRES)
subject_tbl2
# A tibble: 12 × 6
   ID    auclast  cmax  tmax half.life aucinf.obs
   <ord>   <dbl> <dbl> <dbl>     <dbl>      <dbl>
 1 6        71.7  6.44  1.15      7.89       82.2
 2 7        88.0  7.09  3.48      7.85      101. 
 3 8        86.8  7.56  2.02      8.51      102. 
 4 11       77.9  8     0.98      7.26       86.9
 5 3        95.9  8.2   1.02      6.77      106. 
 6 2        88.7  8.33  1.92      6.66       97.4
 7 4       103.   8.6   1.07      6.98      114. 
 8 9        83.9  9.03  0.63      8.41       97.5
 9 12      115.   9.75  3.52      6.29      126. 
10 10      136.  10.2   3.55      9.25      168. 
11 1       147.  10.5   1.12     14.3       215. 
12 5       118.  11.4   1         8.00      136. 

2. Median & IQR:

subject_tbl %>%
  summarise(
    median_t12 = median(half.life, na.rm = TRUE),
    iqr_t12    = IQR(half.life, na.rm = TRUE)
  )
# A tibble: 1 × 2
  median_t12 iqr_t12
       <dbl>   <dbl>
1       7.87    1.50

3. Round values:

subject_tbl %>%
  mutate(across(-ID, ~ round(., 2)))
# A tibble: 12 × 6
   ID    auclast  cmax half.life aucinf.obs cl.obs
   <ord>   <dbl> <dbl>     <dbl>      <dbl>  <dbl>
 1 6        71.7  6.44      7.89       82.2   0.05
 2 7        88.0  7.09      7.85      101.    0.05
 3 8        86.8  7.56      8.51      102.    0.04
 4 11       77.9  8         7.26       86.9   0.06
 5 3        95.9  8.2       6.77      106.    0.04
 6 2        88.7  8.33      6.66       97.4   0.05
 7 4       103.   8.6       6.98      114.    0.04
 8 9        83.9  9.03      8.41       97.5   0.03
 9 12      115.   9.75      6.29      126.    0.04
10 10      136.  10.2       9.25      168.    0.03
11 1       147.  10.5      14.3       215.    0.02
12 5       118.  11.4       8         136.    0.04

4. Separation rationale:
Separating computation from reporting ensures reproducibility, transparency, and reduces transcription error risk.

5. Units rationale:
Units prevent misinterpretation and ensure parameters are comparable across analyses.


Summary

To move from analysis to report:

  • Extract subject-level exposure metrics.
  • Reshape into clean tables.
  • Compute descriptive statistics explicitly.
  • Label units clearly.
  • Document interpolation rules and software version.

Clear reporting is not cosmetic — it is part of analytical integrity.


  • Always build subject-level tables first.
  • Compute summaries programmatically.
  • Label units explicitly (e.g., mg·h/L, L/h/kg).
  • Document interpolation and software version.
  • Never manually edit summary statistics.