ADaM Docstrings

ADaM.JoinColumnsKeywordError — Type
JoinColumnsKeywordError(keyword::Symbol, err::Exception)

Custom error type for join_columns keyword argument failures.

ADaM.bloq_summary — Method
bloq_summary(df; style = :table_one, as_df = false, kwargs...) -> SummaryTables.Table

Build a Below-Limit-of-Quantification (BLQ) summary from a CDISC ADPPK-shaped DataFrame and render it as a SummaryTables.Table. The style kwarg picks the layout; pass as_df = true to skip rendering and get the underlying DataFrame instead.

Shared pre-processing (evid == 0 filter, BLQFN ∈ {0, 1, missing} validation, time rounding to time_digits) is applied for both modes, so the underlying counts are identical across renderers — only the layout differs.

stylerendered layout
:table_oneSummaryTables.table_one pivot of :BLQC over [groupby..., :<time>C]
:simple_tableSummaryTables.simple_table of the per-(group × time) summary, with Pooled/All rows bolded
:summarytableSummaryTables.summarytable with groupby as rows, nominal time across the top, and an N / NBLQ / BLQ [%] panel per cell

Required Columns

Both modes require the columns referenced by blqfl (default :BLQFN), time (default :NPRLT), and evid (default :EVID), plus every column listed in groupby.

Keyword Arguments

  • style::Symbol = :table_one — selects the layout.
  • as_df::Bool = false — when true, return the underlying DataFrame (display-ready per-(group × time) summary for :simple_table, filtered observations enriched with :BLQC and :<time>C categoricals for :table_one, or filtered observations enriched with :<time>C and the raw numeric blqfl column preserved for :summarytable) instead of the rendered Table.
  • groupby = [] — stratification column(s); accepts Symbol, AbstractString, or a Vector of either.
  • blqfl::Union{Symbol,AbstractString} = :BLQFN — numeric BLQ flag column (0/1; missing counts as non-BLQ). Callers with a character flag column (e.g. BLQFL = "Y"/"N") should derive BLQFN = BLQFL .== "Y" first.
  • time::Union{Symbol,AbstractString} = :NPRLT — nominal time column (default: time relative to previous dose).
  • evid::Union{Symbol,AbstractString} = :EVID — rows with evid != 0 are dropped.
  • time_digits::Int = 2 — decimals to round nominal time to before grouping.

The following kwarg is honored by both :simple_table and :summarytable:

  • pct_digits::Int = 1 — decimals to round the BLQ percentage to.

The following kwargs are honored only when style = :simple_table:

  • show_total::Bool = true — append per-group Pooled row.
  • show_overall::Bool = true — when grouped, append a cross-group All row.
  • placeholder_missing::Union{Missing,AbstractString} = missing — value used in :NBLQ and Symbol("BLQ [%]") body cells when the BLQ count is zero. Pass a string (e.g. "-" or "NC") to render a visible placeholder instead of missing.
  • total_label::AbstractString = "Pooled" — label for per-group total rows.
  • overall_label::AbstractString = "All" — label for the cross-group total.

The following kwarg is honored only when style = :table_one:

  • blq_labels::Tuple{AbstractString,AbstractString} = ("BLQ", "Above LOQ") — labels used for the :BLQC column (BLQ label first).

style = :summarytable requires at least one column in groupby (the summarytable layout puts those columns on the row axis).

Example

Default — :table_one rendered Table:

julia> adppk = DataFrame(USUBJID = ["S1", "S1", "S1", "S2", "S2", "S2"], STUDYID = ["A", "A", "A", "B", "B", "B"], NFRLT = [0.0, 1.0, 2.0, 0.0, 1.0, 2.0], EVID = [0, 0, 0, 0, 0, 0], BLQFN = [0, 0, 1, 0, 1, 1], DV = [1.0, 2.0, 0.0, 3.0, 0.0, 0.0])
6×6 DataFrame
 Row │ USUBJID  STUDYID  NFRLT    EVID   BLQFN  DV
     │ String   String   Float64  Int64  Int64  Float64
─────┼──────────────────────────────────────────────────
   1 │ S1       A            0.0      0      0      1.0
   2 │ S1       A            1.0      0      0      2.0
   3 │ S1       A            2.0      0      1      0.0
   4 │ S2       B            0.0      0      0      3.0
   5 │ S2       B            1.0      0      1      0.0
   6 │ S2       B            2.0      0      1      0.0

julia> bloq_summary(adppk; time = :NFRLT, groupby = :STUDYID) isa Table
true

julia> bloq_summary(adppk; style = :simple_table, time = :NFRLT, groupby = :STUDYID) isa Table
true

Pass as_df = true to get the underlying DataFrame:

julia> adppk = DataFrame(USUBJID = ["S1", "S1", "S1", "S2", "S2", "S2"], STUDYID = ["A", "A", "A", "B", "B", "B"], NFRLT = [0.0, 1.0, 2.0, 0.0, 1.0, 2.0], EVID = [0, 0, 0, 0, 0, 0], BLQFN = [0, 0, 1, 0, 1, 1], DV = [1.0, 2.0, 0.0, 3.0, 0.0, 0.0])
6×6 DataFrame
 Row │ USUBJID  STUDYID  NFRLT    EVID   BLQFN  DV
     │ String   String   Float64  Int64  Int64  Float64
─────┼──────────────────────────────────────────────────
   1 │ S1       A            0.0      0      0      1.0
   2 │ S1       A            1.0      0      0      2.0
   3 │ S1       A            2.0      0      1      0.0
   4 │ S2       B            0.0      0      0      3.0
   5 │ S2       B            1.0      0      1      0.0
   6 │ S2       B            2.0      0      1      0.0

julia> bloq_summary(adppk; style = :simple_table, time = :NFRLT, as_df = true)
4×4 DataFrame
 Row │ NFRLTC  N      NBLQ     BLQ [%]
     │ String  Int64  String?  String?
─────┼─────────────────────────────────
   1 │ 0.0         2  missing  missing
   2 │ 1.0         2  1        50.0
   3 │ 2.0         2  2        100.0
   4 │ Pooled      6  3        50.0
ADaM.body_mass_index — Method
body_mass_index(weight::Number, height::Number; kwargs...)
body_mass_index(weight::Quantity, height::Quantity)
body_mass_index(df::DataFrame; kwargs...)

Calculates BMI from height and weight which can be provided as Quantitys, Scalars or Vectors via DataFrame. BMI Wikipedia

The weight_unit and height_unit can be explicitly passed as unit values or can be passed as a Vector. Follows the units from DynamicQuantities.jl

Arguments

  • weight: Weight value.
  • height: Height value.
  • weight_unit: Weight unit (default: "kg").
  • height_unit: Height unit (default: "cm").

Default Columns

The following default column names are used for DataFrame input:

  • weight = :WTBL
  • height = :HTBL
  • weight_unit = :WTBLU
  • height_unit = :HTBLU
  • col = :BMIBL

Output

BMI value in kg/m² as a scalar or DataFrame column with unit column.

Examples

julia> bmi = body_mass_index(60, 160)
23.437499999999996 m⁻² kg

julia> value, unit = ustrip(bmi), dimension(bmi)
(23.437499999999996, m⁻² kg)

julia> body_mass_index(60, 1.6, height_unit = :m)
23.437499999999996 m⁻² kg

julia> body_mass_index(60000, 160, weight_unit = :g)
23.437499999999996 m⁻² kg

julia> body_mass_index(60u"kg", 160u"cm")
23.437499999999996 m⁻² kg

julia> df = DataFrame(HTBL = [150, 160, 170, 180], WTBL = [50, 60, 70, 80], HTBLU = "cm", WTBLU = "kg")
4×4 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU
     │ Int64  Int64  String  String
─────┼──────────────────────────────
   1 │   150     50  cm      kg
   2 │   160     60  cm      kg
   3 │   170     70  cm      kg
   4 │   180     80  cm      kg

julia> body_mass_index(df)
4×6 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU   BMIBL    BMIBLU    
     │ Int64  Int64  String  String  Float64  Symbolic… 
─────┼──────────────────────────────────────────────────
   1 │   150     50  cm      kg      22.2222  m⁻² kg
   2 │   160     60  cm      kg      23.4375  m⁻² kg
   3 │   170     70  cm      kg      24.2215  m⁻² kg
   4 │   180     80  cm      kg      24.6914  m⁻² kg

julia> df = DataFrame(HT = [150, 160, 170, 180], WT = [50, 60, 70, 80], HTU = "cm", WTU = "kg")
4×4 DataFrame
 Row │ HT     WT     HTU     WTU    
     │ Int64  Int64  String  String 
─────┼──────────────────────────────
   1 │   150     50  cm      kg
   2 │   160     60  cm      kg
   3 │   170     70  cm      kg
   4 │   180     80  cm      kg

julia> body_mass_index(df, height = :HT, weight = :WT, height_unit = :HTU, weight_unit = :WTU, col = :BMI)
4×6 DataFrame
 Row │ HT     WT     HTU     WTU     BMI      BMIU      
     │ Int64  Int64  String  String  Float64  Symbolic… 
─────┼──────────────────────────────────────────────────
   1 │   150     50  cm      kg      22.2222  m⁻² kg
   2 │   160     60  cm      kg      23.4375  m⁻² kg
   3 │   170     70  cm      kg      24.2215  m⁻² kg
   4 │   180     80  cm      kg      24.6914  m⁻² kg
ADaM.body_surface_area — Method
body_surface_area(height::Number, weight::Number; kwargs...)
body_surface_area(height::Quantity, weight::Quantity; kwargs...)
body_surface_area(df::DataFrame; kwargs...)

Calculates BSA from height and weight which can be provided as Quantitys, Scalars or Vectors via DataFrame. BSA Wikipedia

BSA can be calculated using the following formulas:

  • mosteller (default)
  • dubois-dubois
  • haycock
  • gehan-george
  • boyd
  • fujimoto
  • takahira

The weight_unit and height_unit can be explicitly passed as unit values or can be passed as a Vector. Follows the units from DynamicQuantities.jl

Arguments

  • height: Height value.
  • weight: Weight value.
  • height_unit: Height unit (default: "cm").
  • weight_unit: Weight unit (default: "kg").
  • formula: BSA calculation formula (default: :mosteller).

Default Columns

The following default column names are used for DataFrame input:

  • height = :HTBL
  • weight = :WTBL
  • height_unit = :HTBLU
  • weight_unit = :WTBLU
  • col = :BSABL

Output

BSA value in m² as a scalar or DataFrame column with unit column.

Examples

julia> bsa = body_surface_area(160, 60) # default height(cm), weight(kg), formula(mosteller)
1.632993161855452 m²

julia> value, unit = ustrip(bsa), dimension(bsa)
(1.632993161855452, m²)

julia> bsa = body_surface_area(160, 60, formula="dubois-dubois")
1.6220414635466536 m²

julia> bsa = body_surface_area(160, 60, formula=:takahira)
1.6349324596500971 m²

julia> body_surface_area( 1.6, 60, height_unit = :m)
1.632993161855452 m²

julia> body_surface_area(160, 60000, weight_unit = :g)
1.632993161855452 m²

julia> body_surface_area(160u"cm", 60u"kg")
1.632993161855452 m²

julia> df = DataFrame(HTBL = [150, 160, 170, 180], WTBL = [50, 60, 70, 80], HTBLU = "cm", WTBLU = "kg")
4×4 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU
     │ Int64  Int64  String  String
─────┼──────────────────────────────
   1 │   150     50  cm      kg
   2 │   160     60  cm      kg
   3 │   170     70  cm      kg
   4 │   180     80  cm      kg

julia> body_surface_area(df)
4×6 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU   BSABL    BSABLU    
     │ Int64  Int64  String  String  Float64  Symbolic… 
─────┼──────────────────────────────────────────────────
   1 │   150     50  cm      kg      1.44338  m²
   2 │   160     60  cm      kg      1.63299  m²
   3 │   170     70  cm      kg      1.81812  m²
   4 │   180     80  cm      kg      2.0      m²

julia> df = DataFrame(HT = [150, 160, 170, 180], WT = [50, 60, 70, 80], HTU = "cm", WTU = "kg")
4×4 DataFrame
 Row │ HT     WT     HTU     WTU    
     │ Int64  Int64  String  String 
─────┼──────────────────────────────
   1 │   150     50  cm      kg
   2 │   160     60  cm      kg
   3 │   170     70  cm      kg
   4 │   180     80  cm      kg

julia> body_surface_area(df, height = :HT, weight = :WT, height_unit = :HTU, weight_unit = :WTU, col = :BSA)
4×6 DataFrame
 Row │ HT     WT     HTU     WTU     BSA      BSAU      
     │ Int64  Int64  String  String  Float64  Symbolic… 
─────┼──────────────────────────────────────────────────
   1 │   150     50  cm      kg      1.44338  m²
   2 │   160     60  cm      kg      1.63299  m²
   3 │   170     70  cm      kg      1.81812  m²
   4 │   180     80  cm      kg      2.0      m²
ADaM.categorical_covariate_count_df — Method
categorical_covariate_count_df(df; covariate_cols, subject, groupby, variable, value, count_col, drop_missing)
  • Prepares a DataFrame for categorical-covariate barplots from a CDISC ADPPK-shaped input.
  • Dedupes to one row per subject, narrows to the requested covariates, optionally renames them, coerces non-String categorical columns (Symbol, Bool, Integer, CategoricalValue) to String for stacking, and aggregates to per- (covariate, category, [stratum]) subject counts.
  • Returns a long-format DataFrame with one row per (covariate, category[, stratum]) combination and subject counts in the count_col column. When groupby is set, the returned frame contains the full cross-product of (covariate value × stratum); cells with zero observed subjects are emitted as rows with a count of 0 so a dodged barplot renders a zero-height bar rather than dropping the category from one stratum. See the "Plotting" section below — because value is a plain AbstractString column (not a per-covariate CategoricalArray), a naive mapping(:value, :n; layout = :variable) call shares a single global category scale across every panel, so each panel gets tick labels for the other panels' categories too. Split by variable first.
  • Continuous (Float) covariates are rejected.

Required Columns

  • The subject column (default :USUBJID).
  • Every column referenced in covariate_cols.
  • The groupby column, when specified.

Keyword Arguments

  • covariate_cols: Categorical covariates to count. Accepts a single element (:SEX, "SEX", :SEX => "Sex") or a vector of them. Each element is a bare Symbol / AbstractString (column name) or a Symbol => label / AbstractString => label pair (column name with display label).
  • subject: Unique subject identifier column (default :USUBJID).
  • groupby: One or more columns to stratify counts by. Accepts a Symbol, an AbstractString, or an AbstractVector of either (e.g. :STUDYID or [:STUDYID, :TRTA]). Default nothing.
  • variable: Output column name for the covariate label (default :variable).
  • value: Output column name for the category level (default :value).
  • count_col: Output column name for the subject count (default :n).
  • drop_missing: Drop missing category values post-stack (default true). When false, missing becomes its own category row per covariate and sorts last within its panel.

Example

julia> adppk = DataFrame(USUBJID = ["S1","S1","S2","S2","S3","S4"], SEX = ["M","M","F","F","M","F"], RACE = ["White","White","Black","Black","White","White"])
6×3 DataFrame
 Row │ USUBJID  SEX     RACE
     │ String   String  String
─────┼─────────────────────────
   1 │ S1       M       White
   2 │ S1       M       White
   3 │ S2       F       Black
   4 │ S2       F       Black
   5 │ S3       M       White
   6 │ S4       F       White

julia> categorical_covariate_count_df(adppk; covariate_cols = [:SEX, :RACE])
4×3 DataFrame
 Row │ variable  value   n
     │ Cat…      String  Int64
─────┼─────────────────────────
   1 │ SEX       F           2
   2 │ SEX       M           2
   3 │ RACE      Black       1
   4 │ RACE      White       3

Plotting

value comes back as a plain AbstractString column (String when a covariate is coerced from a non-string type, SubString{String} if the input was a SubString column left unpromoted), so a plotting layer that autobuilds a categorical scale from it will share one global scale across every covariate panel (each panel gets tick labels for every other panel's categories, and the bars get squeezed against one edge). This isn't fixed by linkxaxes = :none — unlinking axes doesn't unshare the scale. Split by variable and give each panel its own axis:

using DataFrames, CairoMakie
out = categorical_covariate_count_df(adppk; covariate_cols = [:SEX, :RACE])
fig = Figure()
for (i, sub) in enumerate(DataFrames.groupby(out, :variable))
    ax = Axis(fig[1, i]; title = string(first(sub.variable)))
    barplot!(ax, 1:nrow(sub), sub.n)
    ax.xticks = (1:nrow(sub), sub.value)
end
ADaM.compress_dose_events — Method
compress_dose_events(df::DataFrame; group, order, sampling_rows)

This function replaces a sequence of dosing rows (EVID == 1) into compressed format based on EVID column, creating ADDL (Additional Doses) and II (Inter-dose Interval) columns

group and order variables (Vectors or Scalars) can be passed to customise the compression.

Compression can be done so as to retain one inter-sampling row sampling_rows = :single or two sampling rows sampling_rows = :double. Only the information of the 1st row of the sequence is retained.

Required columns for expansion: EVID

Example

julia> df = DataFrame([(1, 1, 40), (2, 1, 90), (1, 0, 10), (2, 0, 60), (1, 1, 20), (2, 1, 70), (1, 1, 30), (2, 1, 80), (1, 0, 50), (2, 0, 100)], [:ID, :EVID, :AFRLT]) # unordered and ungrouped dataset
10×3 DataFrame
 Row │ ID     EVID   AFRLT
     │ Int64  Int64  Int64
─────┼─────────────────────
   1 │     1      1     40
   2 │     2      1     90
   3 │     1      0     10
   4 │     2      0     60
   5 │     1      1     20
   6 │     2      1     70
   7 │     1      1     30
   8 │     2      1     80
   9 │     1      0     50
  10 │     2      0    100

julia> compress_dose_events(df) # compress without groupby
6×4 DataFrame
 Row │ ID     EVID   AFRLT  ADDL  
     │ Int64  Int64  Int64  Int64 
─────┼────────────────────────────
   1 │     1      1     40      1
   2 │     1      0     10      0
   3 │     2      0     60      0
   4 │     1      1     20      3
   5 │     1      0     50      0
   6 │     2      0    100      0

julia> compress_dose_events(df, group = ["ID"])
8×4 DataFrame
 Row │ ID     EVID   AFRLT  ADDL  
     │ Int64  Int64  Int64  Int64 
─────┼────────────────────────────
   1 │     1      1     40      0
   2 │     1      0     10      0
   3 │     1      1     20      1
   4 │     1      0     50      0
   5 │     2      1     90      0
   6 │     2      0     60      0
   7 │     2      1     70      1
   8 │     2      0    100      0

julia> compress_dose_events(df, group = [:ID], order = [:AFRLT])
6×4 DataFrame
 Row │ ID     EVID   AFRLT  ADDL  
     │ Int64  Int64  Int64  Int64 
─────┼────────────────────────────
   1 │     1      0     10      0
   2 │     1      1     20      2
   3 │     1      0     50      0
   4 │     2      0     60      0
   5 │     2      1     70      2
   6 │     2      0    100      0

julia> compress_dose_events(df, group = "ID", order = "AFRLT", sampling_rows = :double)
8×4 DataFrame
 Row │ ID     EVID   AFRLT  ADDL  
     │ Int64  Int64  Int64  Int64 
─────┼────────────────────────────
   1 │     1      0     10      0
   2 │     1      1     20      1
   3 │     1      1     40      0
   4 │     1      0     50      0
   5 │     2      0     60      0
   6 │     2      1     70      1
   7 │     2      1     90      0
   8 │     2      0    100      0
ADaM.conc_summary — Method
conc_summary(df; style = :simple, as_df = false, kwargs...) -> Union{SummaryTables.Table, DataFrame}

Compute PK-standard descriptive statistics on concentrations, stratified by groupby × nominal sampling time, and render as a SummaryTables.Table. Tabular counterpart to NCAUtilities.summary_observations_vs_time.

:GeoCV and :CV are returned as percentages (e.g. 27.5 not 0.275). Pass as_df = true to skip rendering and get the underlying long-form DataFrame instead.

Required Columns

The input DataFrame must contain the columns referenced by time, conc, and evid (defaults :NFRLT, :DV, :EVID), plus every column listed in groupby (default [:TRTA]).

Statistics

Each stat keyword maps to an output column name:

  • :n → N — number of non-missing observations.
  • :geomean → GeoMean — geometric mean, exp(mean(log(x))). Requires all positive values.
  • :geocv → GeoCV — geometric coefficient of variation as percent, 100 * sqrt(exp(var(log(x))) - 1).
  • :gsd → GSD — geometric standard deviation, exp(sqrt(var(log(x)))).
  • :mean → Mean — arithmetic mean.
  • :sd → SD — sample standard deviation (Bessel-corrected, n - 1 denominator).
  • :cv → CV — coefficient of variation as percent, 100 * SD / Mean.
  • :median → Median — sample median.
  • :q1 → Q1 — 25th percentile (first quartile).
  • :q3 → Q3 — 75th percentile (third quartile).
  • :min → Min — minimum value.
  • :max → Max — maximum value.

Edge cases (all stat columns are missing when undefined):

  • n == 0 — only N is reported; every other stat is missing.
  • n < 2 — SD, CV, GeoCV, GSD, Q1, Q3 are missing (no spread).
  • Any non-positive value in the cell — GeoMean, GeoCV, GSD are missing (log undefined).

Keyword Arguments

  • style::Symbol = :simple — rendering layout:
    • :simple — flat table (one row per group × time), rendered with SummaryTables.simple_table. Backwards-compatible default.
    • :table_one — group hierarchy across the top, stats stacked vertically on the left, rendered with SummaryTables.table_one.
    • :summarytable — group hierarchy on the left, time across the top, full stat panel per cell, rendered with SummaryTables.summarytable.
  • as_df::Bool = false — when true, return the underlying long-form DataFrame (one row per group × time, one column per requested stat) regardless of style.
  • groupby = [:TRTA] — stratification column(s); accepts Symbol, AbstractString, or a Vector of either. Nested outside time in the group hierarchy. Default stratifies by actual treatment. Pass an empty vector to aggregate across all subjects; :summarytable requires at least one column here since it uses groupby as the row axis. For a custom sort order, pre-cast the relevant column to a CategoricalArray with ordered = true.
  • time::Union{Symbol,AbstractString} = :NFRLT — nominal time column.
  • conc::Union{Symbol,AbstractString} = :DV — concentration column.
  • evid::Union{Symbol,AbstractString} = :EVID — rows with EVID != 0 are dropped.
  • stats = collect(keys(PK_STAT_NAMES)) — which statistics to include, in output order. Defaults to the full panel: [:n, :geomean, :geocv, :gsd, :mean, :sd, :cv, :median, :q1, :q3, :min, :max].
  • time_digits::Int = 2 — decimals to round nominal time to before grouping.
  • sig_digits::Int = 3 — significant figures for numeric stat values; :N is always Int and never rounded.
  • time_as_string::Bool = false — display-only. When true, the time column in the rendered Table is converted to a String formatted to exactly time_digits decimal places. Use this for rendering with SummaryTables.simple_table, which otherwise strips trailing zeros and makes consecutive times like 239.77 / 240.0 / 240.5 look identical ("240"). Sorting happens on the numeric values before conversion. Ignored when as_df = true.
  • missing_placeholder = nothing — display-only. When set (e.g. "NC" or "-"), every missing cell in the rendered Table is replaced with this string via SummaryTables.ReplaceMissing. Ignored when as_df = true; the returned DataFrame keeps missing for downstream numeric processing.
  • table_kwargs::NamedTuple = NamedTuple() — extra kwargs forwarded to the SummaryTables renderer for style (e.g. footnotes, show_n). Merged after internal defaults so entries here win — e.g. (show_n = true,) with style = :table_one re-enables the N column. Ignored when as_df = true. Invalid kwargs raise MethodError, though simple_table/summarytable silently accept any CellTable field (e.g. round_digits). Overriding structural kwargs (groupby, rows, cols, summary, postprocess) will silently change the computation.

Example

Minimal ADPPK-shaped fixture with two subjects per treatment and two nominal sampling times:

julia> adppk = DataFrame(USUBJID = repeat(["S1", "S2", "S3", "S4"], inner = 2), TRTA = repeat(["Active", "Active", "Placebo", "Placebo"], inner = 2), NFRLT = repeat([1.0, 2.0], outer = 4), EVID = zeros(Int, 8), DV = [10.0, 5.0, 12.0, 6.0, 0.5, 0.2, 0.6, 0.3])
8×5 DataFrame
 Row │ USUBJID  TRTA     NFRLT    EVID   DV
     │ String   String   Float64  Int64  Float64
─────┼───────────────────────────────────────────
   1 │ S1       Active       1.0      0     10.0
   2 │ S1       Active       2.0      0      5.0
   3 │ S2       Active       1.0      0     12.0
   4 │ S2       Active       2.0      0      6.0
   5 │ S3       Placebo      1.0      0      0.5
   6 │ S3       Placebo      2.0      0      0.2
   7 │ S4       Placebo      1.0      0      0.6
   8 │ S4       Placebo      2.0      0      0.3

julia> conc_summary(adppk; stats = [:n, :geomean, :mean, :sd, :median, :min, :max], as_df = true)
4×9 DataFrame
 Row │ TRTA     NFRLT    N      GeoMean  Mean     SD       Median   Min      Max
     │ String   Float64  Int64  Float64  Float64  Float64  Float64  Float64  Float64
─────┼───────────────────────────────────────────────────────────────────────────────
   1 │ Active       1.0      2   11.0      11.0    1.41      11.0      10.0     12.0
   2 │ Active       2.0      2    5.48      5.5    0.707      5.5       5.0      6.0
   3 │ Placebo      1.0      2    0.548     0.55   0.0707     0.55      0.5      0.6
   4 │ Placebo      2.0      2    0.245     0.25   0.0707     0.25      0.2      0.3

Default — rendered Table with full stat panel:

julia> adppk = DataFrame(USUBJID = repeat(["S1", "S2", "S3", "S4"], inner = 2), TRTA = repeat(["Active", "Active", "Placebo", "Placebo"], inner = 2), NFRLT = repeat([1.0, 2.0], outer = 4), EVID = zeros(Int, 8), DV = [10.0, 5.0, 12.0, 6.0, 0.5, 0.2, 0.6, 0.3])
8×5 DataFrame
 Row │ USUBJID  TRTA     NFRLT    EVID   DV
     │ String   String   Float64  Int64  Float64
─────┼───────────────────────────────────────────
   1 │ S1       Active       1.0      0     10.0
   2 │ S1       Active       2.0      0      5.0
   3 │ S2       Active       1.0      0     12.0
   4 │ S2       Active       2.0      0      6.0
   5 │ S3       Placebo      1.0      0      0.5
   6 │ S3       Placebo      2.0      0      0.2
   7 │ S4       Placebo      1.0      0      0.6
   8 │ S4       Placebo      2.0      0      0.3

julia> conc_summary(adppk; stats = [:n, :geomean, :geocv, :gsd, :mean, :sd, :median, :q1, :q3, :min, :max], time_as_string = true, missing_placeholder = "NC") isa Table
true
ADaM.continuous_covariate_distribution_df — Method
continuous_covariate_distribution_df(df; covariate_cols, subject, groupby, variable, value, drop_missing)
  • Prepares a DataFrame for continuous-covariate distribution plots (histograms, density plots, ridge plots, etc.) from a CDISC ADPPK-shaped input.
  • Dedupes to one row per subject, narrows to the requested covariates, optionally renames them, and stacks them to a long-format DataFrame (one row per (covariate, subject) observation). See "Plotting" below — panels need their own axis and bin edges.
  • The output variable column is an ordered CategoricalArray whose levels follow the input order, so plot panels appear in the order specified.
  • Missing values are dropped post-stack (per-covariate) by default — each panel shows all available data for that covariate rather than only subjects with every covariate present.

Required Columns

  • The subject column (default :USUBJID).
  • Every column referenced in covariate_cols.
  • The groupby column, when specified.

Keyword Arguments

  • covariate_cols: Continuous covariates to plot. Accepts a single element (:AGE, "AGE", :AGE => "Age [yr]") or a vector of them. Each element is a bare Symbol / AbstractString (column name) or a Symbol => label / AbstractString => label pair (column name with display label). CategoricalArray covariates whose level type is <: Real (e.g. CategoricalArray{Int} from .xpt round-trips) are accepted.
  • subject: Unique subject identifier column (default :USUBJID).
  • groupby: One or more categorical columns preserved alongside the stacked covariates. Accepts a Symbol, an AbstractString, or an AbstractVector of either (e.g. :STUDYID or [:STUDYID, :TRTA]). Not renamed. Default nothing.
  • variable: Output column name for the covariate label (default :variable).
  • value: Output column name for the covariate value (default :value).
  • drop_missing: Drop missing values from the stacked output (default true).

Example

julia> adppk = DataFrame(USUBJID = ["S1","S1","S2","S2","S3"], AGE = [30.0,30.0,45.0,45.0,60.0], WTBL = [70.0,70.0,80.0,80.0,65.0], BMIBL = [22.0,22.0,26.5,26.5,21.5])
5×4 DataFrame
 Row │ USUBJID  AGE      WTBL     BMIBL
     │ String   Float64  Float64  Float64
─────┼────────────────────────────────────
   1 │ S1          30.0     70.0     22.0
   2 │ S1          30.0     70.0     22.0
   3 │ S2          45.0     80.0     26.5
   4 │ S2          45.0     80.0     26.5
   5 │ S3          60.0     65.0     21.5

julia> continuous_covariate_distribution_df(adppk; covariate_cols = [:AGE, :WTBL, :BMIBL])
9×2 DataFrame
 Row │ variable  value
     │ Cat…      Float64
─────┼───────────────────
   1 │ AGE          30.0
   2 │ AGE          45.0
   3 │ AGE          60.0
   4 │ WTBL         70.0
   5 │ WTBL         80.0
   6 │ WTBL         65.0
   7 │ BMIBL        22.0
   8 │ BMIBL        26.5
   9 │ BMIBL        21.5

Plotting

All covariates share one value column, so a faceted call gives every panel the pooled range — and bins is applied to that pooled range, so a narrow covariate collapses to a handful of bars. linkxaxes = :none does not help: the range comes from the shared continuous scale, not from axis linking. Split by variable and give each panel its own axis:

fig = Figure(size = (900, 300))
for (i, sub) in enumerate(DataFrames.groupby(out, :variable))
    ax = Axis(fig[1, i]; title = string(first(sub.variable)), xlabel = "value")
    hist!(ax, sub.value; bins = 20)
end
ADaM.convert_to_missing — Method
convert_to_missing(df::DataFrame, NaStr::Vector)
  • Converts values to missing. The values that need to be converted to missing can be passed as a Vector.
  • Example : [nothing, "", NaN, ".", "-"]

Example

julia> df = DataFrame(Col1 = [1, "."], Col2 = ["", 2], Col3 = [3, nothing], Col4 = ["-", 4])
2×4 DataFrame
 Row │ Col1  Col2  Col3    Col4 
     │ Any   Any   Union…  Any  
─────┼──────────────────────────
   1 │ 1           3       -
   2 │ .     2             4

julia> convert_to_missing(df, ["", nothing, ".", "-"])
2×4 DataFrame
 Row │ Col1     Col2     Col3     Col4
     │ Int64?   Int64?   Int64?   Int64?
─────┼────────────────────────────────────
   1 │       1  missing        3  missing
   2 │ missing        2  missing        4
ADaM.covariate_pairplot_df — Method
covariate_pairplot_df(df; covariate_cols, subject, groupby, drop_missing)
  • Prepares a DataFrame for a covariate pairplot (scatterplot matrix) from a CDISC ADPPK-shaped input.
  • Deduplicates to one row per subject and narrows to the requested covariates, optionally renaming them for display via Symbol => String pairs.
  • Returns a wide-format DataFrame (one row per unique subject, one column per covariate, plus any groupby columns carried through). Ready to feed into a mapping(color=:col)-style API (AlgebraOfGraphics, Vega, etc.); when the downstream is PairPlots.jl (which has no color=:col mapping), split by groupby first — see the second example below.

Required Columns

  • The subject column (default :USUBJID).
  • Every column referenced in covariate_cols.
  • The groupby column, when specified.

Keyword Arguments

  • covariate_cols: Continuous covariates to include. Each element is a bare Symbol (e.g. :AGE) or a Symbol => String pair (e.g. :AGE => "Age [yr]"). At least 2 covariates are required. CategoricalArray covariates whose level type is <: Real (e.g. CategoricalArray{Int} from .xpt round-trips) are accepted; truly categorical covariates go via groupby.
  • subject: Unique subject identifier column (default :USUBJID).
  • groupby: One or more categorical columns preserved alongside the covariates for use as a series / color / stratification grouping. Accepts a Symbol, an AbstractString, or an AbstractVector of either (e.g. :STUDYID or [:STUDYID, :TRTA]). Not renamed. Default nothing.
  • drop_missing: Drop subjects with any missing covariate value (default true).

Example

julia> adppk = DataFrame(USUBJID = ["S1","S1","S2","S2","S3"], AGE = [30,30,45,45,60], WTBL = [70.0,70.0,80.0,80.0,65.0], BMIBL = [22.0,22.0,26.5,26.5,21.5])
5×4 DataFrame
 Row │ USUBJID  AGE    WTBL     BMIBL
     │ String   Int64  Float64  Float64
─────┼──────────────────────────────────
   1 │ S1          30     70.0     22.0
   2 │ S1          30     70.0     22.0
   3 │ S2          45     80.0     26.5
   4 │ S2          45     80.0     26.5
   5 │ S3          60     65.0     21.5

julia> covariate_pairplot_df(adppk; covariate_cols = [:AGE, :WTBL, :BMIBL])
3×3 DataFrame
 Row │ AGE    WTBL     BMIBL
     │ Int64  Float64  Float64
─────┼─────────────────────────
   1 │    30     70.0     22.0
   2 │    45     80.0     26.5
   3 │    60     65.0     21.5

Splitting by groupby for PairPlots.jl

When groupby is set, the returned DataFrame carries the categorical column(s) alongside the numeric covariates. Because PairPlots.pairplot has no color=:col mapping, split the frame yourself before passing it in — the groupby column is present precisely to make this splitting possible:

using DataFrames, PairPlots
out = covariate_pairplot_df(adppk; covariate_cols = [:AGE, :WTBL], groupby = :SEX)
pairplot((select(sub, Not(:SEX)) for sub in DataFrames.groupby(out, :SEX))...)

For mapping(color=:col)-style downstreams (AlgebraOfGraphics, Vega, etc.), pass the DataFrame directly — the groupby column is already the mapping target.

ADaM.creatinine_clearance — Method
creatinine_clearance(weight::Number, height::Number; kwargs...)
creatinine_clearance(weight::Quantity, height::Quantity)
creatinine_clearance(df::DataFrame; kwargs...)

Calculates creatinine clearance from age, weight,creatinine and sex using Cockcroft–Gault formula which can be provided as Quantitys, Scalars or Vectors via DataFrame. Cockcroft–Gault formula Wikipedia

The age_unit, weight_unit and creat_unit can be explicitly passed as unit values or can be passed as a Vector. Follows the units from DynamicQuantities.jl

Default values for keyword arguments:

  • age = :AGE
  • weight = :WTBL
  • creat = :CREATBL
  • sex = :SEX
  • age_unit = :AGEU
  • weight_unit = :WTBLU
  • creat_unit = :CREATBLU
  • col = :CRCLBL

Example

julia> creatinine_clearance(53, 85, 90, "M", creat_unit = "umol/L")
100.88434438681963 min⁻¹ mL

julia> creatinine_clearance(53, 85, 1, "M", creat_unit = "mg/dL")
102.70833333333333 min⁻¹ mL

julia> creatinine_clearance(53us"yr", 85us"kg", 90us"umol/L", "M")
100.88434438681963 min⁻¹ mL

julia> creatinine_clearance(53u"yr", 85u"kg", 1us"mg/dL", "M")
102.70833333333333 min⁻¹ mL

julia> df = DataFrame(AGE = [20, 30, 40, 53], WTBL = [50, 60, 70, 85], CREATBL = [60, 70, 80, 90], SEX = ["M", "M", "F", "F"], AGEU = :yr, WTBLU = :kg, CREATBLU = "umol/L")
4×7 DataFrame
 Row │ AGE    WTBL   CREATBL  SEX     AGEU    WTBLU   CREATBLU 
     │ Int64  Int64  Int64    String  Symbol  Symbol  String   
─────┼─────────────────────────────────────────────────────────
   1 │    20     50       60  M       yr      kg      umol/L
   2 │    30     60       70  M       yr      kg      umol/L
   3 │    40     70       80  F       yr      kg      umol/L
   4 │    53     85       90  F       yr      kg      umol/L

julia> creatinine_clearance(df)
4×9 DataFrame
 Row │ AGE    WTBL   CREATBL  SEX     AGEU    WTBLU   CREATBLU  CRCLBL    CRCLBLU   
     │ Int64  Int64  Int64    String  Symbol  Symbol  String    Float64   Symbolic… 
─────┼──────────────────────────────────────────────────────────────────────────────
   1 │    20     50       60  M       yr      kg      umol/L    122.78    min⁻¹ mL
   2 │    30     60       70  M       yr      kg      umol/L    115.764   min⁻¹ mL
   3 │    40     70       80  F       yr      kg      umol/L     91.3177  min⁻¹ mL
   4 │    53     85       90  F       yr      kg      umol/L     85.7517  min⁻¹ mL

julia> df = DataFrame(AGEYRS = [20, 30, 40, 53], WEIGHT = [50, 60, 70, 85], CREAT = [60, 70, 80, 90], GENDER = ["M", "M", "F", "F"], AGEUNI = :yr, WTUNI = :kg, CREATUNI = "umol/L")
4×7 DataFrame
 Row │ AGEYRS  WEIGHT  CREAT  GENDER  AGEUNI  WTUNI   CREATUNI 
     │ Int64   Int64   Int64  String  Symbol  Symbol  String   
─────┼─────────────────────────────────────────────────────────
   1 │     20      50     60  M       yr      kg      umol/L
   2 │     30      60     70  M       yr      kg      umol/L
   3 │     40      70     80  F       yr      kg      umol/L
   4 │     53      85     90  F       yr      kg      umol/L

julia> creatinine_clearance(df; age = :AGEYRS, weight = :WEIGHT, creat = :CREAT, sex = :GENDER, age_unit = :AGEUNI, weight_unit = :WTUNI, creat_unit = :CREATUNI, col = :CREATCL)
4×9 DataFrame
 Row │ AGEYRS  WEIGHT  CREAT  GENDER  AGEUNI  WTUNI   CREATUNI  CREATCL   CREATCLU  
     │ Int64   Int64   Int64  String  Symbol  Symbol  String    Float64   Symbolic… 
─────┼──────────────────────────────────────────────────────────────────────────────
   1 │     20      50     60  M       yr      kg      umol/L    122.78    min⁻¹ mL
   2 │     30      60     70  M       yr      kg      umol/L    115.764   min⁻¹ mL
   3 │     40      70     80  F       yr      kg      umol/L     91.3177  min⁻¹ mL
   4 │     53      85     90  F       yr      kg      umol/L     85.7517  min⁻¹ mL
ADaM.definition_table — Method
definition_table(table)

Creates a Table that gives a defintion overview of the columns of table, intended to give a quick intuition of the dataset. ategoric and Numeric columns(NUM, CD , N) are automatically mapped to each other in the Summary column. Custom comments can be passed for each column as additional information.

Keyword arguments

  • max_categories = 10: Limit the number of categories listed individually for categorical columns, the rest will be lumped together.
  • label_metadata_key = "label": Key to look up column label metadata with.
  • map_dict = Helps map unique values of 2 columns.
  • comment_dict = Helps display custom comments for columns.
ADaM.derive_body_covariates — Method
derive_body_covariates(df; kwargs...)

Derive standard body size (BMI, BSA) and renal function (CRCL, EGFR) covariates from a DataFrame.

This is a convenience function that internally calls:

  • body_mass_index
  • body_surface_area
  • creatinine_clearance
  • est_glomerular_filtration_rate

Positional Arguments

  • df: Input DataFrame containing subject data.

Keyword Arguments

  • bmi: Output column for Body Mass Index (default: :BMIBL)
  • bsa: Output column for Body Surface Area (default: :BSABL)
  • crcl: Output column for Creatinine Clearance (default: :CRCLBL)
  • egfr: Output column for estimated GFR (default: :EGFRBL)
  • weight: Input column for weight (default: :WTBL)
  • height: Input column for height (default: :HTBL)
  • weight_unit: Input column for weight units (default: :WTBLU)
  • height_unit: Input column for height units (default: :HTBLU)
  • age: Input column for age (default: :AGE)
  • age_unit: Input column for age units (default: :AGEU)
  • sex: Input column for sex/gender (default: :SEX)
  • creat: Input column for serum creatinine (default: :CREATBL)
  • creat_unit: Input column for creatinine units (default: :CREATBLU)
  • bsa_formula: Formula for BSA calculation (default: :mosteller)
  • egfr_formula: Formula for eGFR calculation (default: "ckd-epi-creat-2021")

Returns

A new DataFrame with additional columns for derived covariates, including BMI, BSA, CRCL, and EGFR column values and units.

Examples

julia> df = DataFrame(HTBL = [150, 160, 170, 180], WTBL = [50, 60, 70, 80], HTBLU = "cm", WTBLU = "kg")
4×4 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU
     │ Int64  Int64  String  String
─────┼──────────────────────────────
   1 │   150     50  cm      kg
   2 │   160     60  cm      kg
   3 │   170     70  cm      kg
   4 │   180     80  cm      kg

julia> derive_body_covariates(df)
4×8 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU   BMIBL    BMIBLU     BSABL    BSABLU    
     │ Int64  Int64  String  String  Float64  Symbolic…  Float64  Symbolic… 
─────┼──────────────────────────────────────────────────────────────────────
   1 │   150     50  cm      kg      22.2222  m⁻² kg     1.44338  m²
   2 │   160     60  cm      kg      23.4375  m⁻² kg     1.63299  m²
   3 │   170     70  cm      kg      24.2215  m⁻² kg     1.81812  m²
   4 │   180     80  cm      kg      24.6914  m⁻² kg     2.0      m²

julia> df = DataFrame(HTBL = [150, 160, 170, 180], WTBL = [50, 60, 70, 80], HTBLU = "cm", WTBLU = "kg", AGE = [20, 30, 40, 53], CREATBL = [60, 70, 80, 90], SEX = ["M", "M", "F", "F"], AGEU = :yr, CREATBLU = "umol/L")
4×9 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU   AGE    CREATBL  SEX     AGEU    CREATBLU 
     │ Int64  Int64  String  String  Int64  Int64    String  Symbol  String   
─────┼────────────────────────────────────────────────────────────────────────
   1 │   150     50  cm      kg         20       60  M       yr      umol/L
   2 │   160     60  cm      kg         30       70  M       yr      umol/L
   3 │   170     70  cm      kg         40       80  F       yr      umol/L
   4 │   180     80  cm      kg         53       90  F       yr      umol/L

julia> derive_body_covariates(df)
4×17 DataFrame
 Row │ HTBL   WTBL   HTBLU   WTBLU   AGE    CREATBL  SEX     AGEU    CREATBLU  BMIBL    BMIBLU     BSABL    BSABLU     CRCLBL    CRCLBLU    EGFRBL    EGFRBLU         
     │ Int64  Int64  String  String  Int64  Int64    String  Symbol  String    Float64  Symbolic…  Float64  Symbolic…  Float64   Symbolic…  Float64   String          
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │   150     50  cm      kg         20       60  M       yr      umol/L    22.2222  m⁻² kg     1.44338  m²         122.78    min⁻¹ mL   136.546   min⁻¹ mL/1.73m²
   2 │   160     60  cm      kg         30       70  M       yr      umol/L    23.4375  m⁻² kg     1.63299  m²         115.764   min⁻¹ mL   122.476   min⁻¹ mL/1.73m²
   3 │   170     70  cm      kg         40       80  F       yr      umol/L    24.2215  m⁻² kg     1.81812  m²          91.3177  min⁻¹ mL    82.3362  min⁻¹ mL/1.73m²
   4 │   180     80  cm      kg         53       90  F       yr      umol/L    24.6914  m⁻² kg     2.0      m²          80.7075  min⁻¹ mL    65.9318  min⁻¹ mL/1.73m²
ADaM.dose_interval_sequence — Method
function dose_interval_sequence(df::DataFrame; group, order)

This function creates a column DINTSEQ from a either dose(EX) or combined dataset (EX and PC).

The values in DINTSEQ column contains a sequences of doses/washout (EVID in [1, 4]) starting from missing (predose sample) or 1 (initial dose).

Group and order variables (Vectors or Scalars) can be passed to customise the sequence.

Required columns for dose count: EVID

Example

julia> df = DataFrame([(1, 1, 40), (2, 1, 90), (1, 0, 10), (2, 0, 60), (1, 1, 20), (2, 1, 70), (1, 1, 30), (2, 1, 80), (1, 0, 50), (2, 0, 100)], [:ID, :EVID, :AFRLT]) # unordered and ungrouped dataset
10×3 DataFrame
 Row │ ID     EVID   AFRLT
     │ Int64  Int64  Int64
─────┼─────────────────────
   1 │     1      1     40
   2 │     2      1     90
   3 │     1      0     10
   4 │     2      0     60
   5 │     1      1     20
   6 │     2      1     70
   7 │     1      1     30
   8 │     2      1     80
   9 │     1      0     50
  10 │     2      0    100

julia> dose_interval_sequence(df) # count without groupby
10×4 DataFrame
 Row │ ID     EVID   AFRLT  DINTSEQ 
     │ Int64  Int64  Int64  Int64   
─────┼──────────────────────────────
   1 │     1      1     40        1
   2 │     2      1     90        2
   3 │     1      0     10        2
   4 │     2      0     60        2
   5 │     1      1     20        3
   6 │     2      1     70        4
   7 │     1      1     30        5
   8 │     2      1     80        6
   9 │     1      0     50        6
  10 │     2      0    100        6

julia> dose_interval_sequence(df, group = ["ID"])
10×4 DataFrame
 Row │ ID     EVID   AFRLT  DINTSEQ 
     │ Int64  Int64  Int64  Int64   
─────┼──────────────────────────────
   1 │     1      1     40        1
   2 │     1      0     10        1
   3 │     1      1     20        2
   4 │     1      1     30        3
   5 │     1      0     50        3
   6 │     2      1     90        1
   7 │     2      0     60        1
   8 │     2      1     70        2
   9 │     2      1     80        3
  10 │     2      0    100        3

julia> dose_interval_sequence(df, group = :ID, order = :AFRLT)
10×4 DataFrame
 Row │ ID     EVID   AFRLT  DINTSEQ 
     │ Int64  Int64  Int64  Int64?  
─────┼──────────────────────────────
   1 │     1      0     10  missing 
   2 │     1      1     20        1
   3 │     1      1     30        2
   4 │     1      1     40        3
   5 │     1      0     50        3
   6 │     2      0     60  missing 
   7 │     2      1     70        1
   8 │     2      1     80        2
   9 │     2      1     90        3
  10 │     2      0    100        3
ADaM.est_glomerular_filtration_rate — Method
est_glomerular_filtration_rate(age::Number, sex::Union{Symbol,AbstractString}; kwargs...)
est_glomerular_filtration_rate(age::Quantity, sex::Union{Symbol,AbstractString}; kwargs...)
est_glomerular_filtration_rate(df::DataFrame; kwargs...)

Calculates eGFR from age, creatinine and sex which can be provided as Quantitys, Scalars or Vectors via DataFrame. Supports various formulas:

  • CKD-EPI Creatinine 2021 Formula - ckd-epi-creat-2021 (default)
  • CKD-EPI Cystatin C 2012 Formula - ckd-epi-cyst-2012
  • MDRD Formula - mdrd (requires race parameter)

The age_unit, creat_unit, and cyst_unit can be explicitly passed as unit values or can be passed as a Vector. Follows the units from DynamicQuantities.jl

Arguments

  • age: Age value.
  • sex: Sex/gender (valid values: "M", "MALE", "F", "FEMALE", case-insensitive).
  • creat: Serum creatinine value (default: 0).
  • cyst: Cystatin C value (default: 0).
  • race: Race (default: "U"). Required for MDRD formula.
  • age_unit: Age unit (default: "yr").
  • creat_unit: Creatinine unit (default: "mg/dL").
  • cyst_unit: Cystatin C unit (default: "mg/L").
  • formula: eGFR calculation formula (default: "ckd-epi-creat-2021").

Default Columns

The following default column names are used for DataFrame input:

  • age = :AGE
  • sex = :SEX
  • creat = :CREATBL
  • cyst = :CYSTBL
  • race = :RACE
  • age_unit = :AGEU
  • creat_unit = :CREATBLU
  • cyst_unit = :CYSTBLU
  • col = :EGFRBL

Output

eGFR value in mL/min/1.73m² as a scalar or DataFrame column with unit column.

Notes

  • Age must be between 1 and 140 years.

Examples

julia> est_glomerular_filtration_rate(53, "M", creat = 90, creat_unit = "umol/L")
88.08210123133497

julia> est_glomerular_filtration_rate(53, "M", creat = 1, creat_unit = "mg/dL")
89.99656911635697

julia> est_glomerular_filtration_rate(53us"yr", "M", creat =  90us"umol/L", formula="ckd-epi-creat-2021")
88.08210123133497

julia> est_glomerular_filtration_rate(53u"yr", "M", creat = 1us"mg/dL")
89.99656911635697

julia> est_glomerular_filtration_rate(53u"yr", "M", creat = 1us"mg/dL", race="BLACK OR AFRICAN AMERICAN", formula=:mdrd)
94.7354815425664

julia> est_glomerular_filtration_rate(53u"yr", "M", cyst = 0.6us"mg/L", formula="ckd-epi-cyst-2012")
124.1483657437901

julia> df = DataFrame(AGE = [20, 30, 40, 53], CREATBL = [60, 70, 80, 90], SEX = ["M", "M", "F", "F"], AGEU = :yr, CREATBLU = "umol/L")
4×5 DataFrame
 Row │ AGE    CREATBL  SEX     AGEU    CREATBLU 
     │ Int64  Int64    String  Symbol  String   
─────┼──────────────────────────────────────────
   1 │    20       60  M       yr      umol/L
   2 │    30       70  M       yr      umol/L
   3 │    40       80  F       yr      umol/L
   4 │    53       90  F       yr      umol/L

julia> est_glomerular_filtration_rate(df)
4×7 DataFrame
 Row │ AGE    CREATBL  SEX     AGEU    CREATBLU  EGFRBL    EGFRBLU         
     │ Int64  Int64    String  Symbol  String    Float64   String          
─────┼─────────────────────────────────────────────────────────────────────
   1 │    20       60  M       yr      umol/L    136.546   min⁻¹ mL/1.73m²
   2 │    30       70  M       yr      umol/L    122.476   min⁻¹ mL/1.73m²
   3 │    40       80  F       yr      umol/L     82.3362  min⁻¹ mL/1.73m²
   4 │    53       90  F       yr      umol/L     65.9318  min⁻¹ mL/1.73m²

julia> df = DataFrame(AGEYRS = [20, 30, 40, 53], CREAT = [60, 70, 80, 90], GENDER = ["M", "M", "F", "F"], AGEUNI = :yr, RACEC = "BLACK OR AFRICAN AMERICAN", CREATUNI = "umol/L")
4×6 DataFrame
 Row │ AGEYRS  CREAT  GENDER  AGEUNI  RACEC                      CREATUNI 
     │ Int64   Int64  String  Symbol  String                     String   
─────┼────────────────────────────────────────────────────────────────────
   1 │     20     60  M       yr      BLACK OR AFRICAN AMERICAN  umol/L
   2 │     30     70  M       yr      BLACK OR AFRICAN AMERICAN  umol/L
   3 │     40     80  F       yr      BLACK OR AFRICAN AMERICAN  umol/L
   4 │     53     90  F       yr      BLACK OR AFRICAN AMERICAN  umol/L

julia> est_glomerular_filtration_rate(df; age = :AGEYRS, creat = :CREAT, sex = :GENDER, race = :RACEC, age_unit = :AGEUNI, creat_unit = :CREATUNI, col = :EGFR, formula=:mdrd)
4×8 DataFrame
 Row │ AGEYRS  CREAT  GENDER  AGEUNI  RACEC                      CREATUNI  EGFR      EGFRU           
     │ Int64   Int64  String  Symbol  String                     String    Float64   String          
─────┼───────────────────────────────────────────────────────────────────────────────────────────────
   1 │     20     60  M       yr      BLACK OR AFRICAN AMERICAN  umol/L    180.576   min⁻¹ mL/1.73m²
   2 │     30     70  M       yr      BLACK OR AFRICAN AMERICAN  umol/L    139.206   min⁻¹ mL/1.73m²
   3 │     40     80  F       yr      BLACK OR AFRICAN AMERICAN  umol/L     83.5172  min⁻¹ mL/1.73m²
   4 │     53     90  F       yr      BLACK OR AFRICAN AMERICAN  umol/L     68.8551  min⁻¹ mL/1.73m²

julia> df = DataFrame(AGE = [20, 30, 40, 53], CYSTBL = [0.6, 0.7, 0.8, 0.9], SEX = ["M", "M", "F", "F"], RACE = "ASIAN", AGEU = :yr, CYSTBLU = "mg/L")
4×6 DataFrame
 Row │ AGE    CYSTBL   SEX     RACE    AGEU    CYSTBLU
     │ Int64  Float64  String  String  Symbol  String
─────┼─────────────────────────────────────────────────
   1 │    20      0.6  M       ASIAN   yr      mg/L
   2 │    30      0.7  M       ASIAN   yr      mg/L
   3 │    40      0.8  F       ASIAN   yr      mg/L
   4 │    53      0.9  F       ASIAN   yr      mg/L

julia> est_glomerular_filtration_rate(df, formula="ckd-epi-cyst-2012")
4×8 DataFrame
 Row │ AGE    CYSTBL   SEX     RACE    AGEU    CYSTBLU  EGFRBL   EGFRBLU         
     │ Int64  Float64  String  String  Symbol  String   Float64  String          
─────┼───────────────────────────────────────────────────────────────────────────
   1 │    20      0.6  M       ASIAN   yr      mg/L     141.704  min⁻¹ mL/1.73m²
   2 │    30      0.7  M       ASIAN   yr      mg/L     126.058  min⁻¹ mL/1.73m²
   3 │    40      0.8  F       ASIAN   yr      mg/L     105.594  min⁻¹ mL/1.73m²
   4 │    53      0.9  F       ASIAN   yr      mg/L      85.72   min⁻¹ mL/1.73m²
ADaM.expand_dose_events — Method
expand_dose_events(df::DataFrame; start_dtm, end_dtm, trt_start_dtm, dose_freq)
  • Returns expanded dataset when you pass EX (dose) dataset as input with all columns.
  • Creates unique row for every dosing in a dosing period for a subject.
  • Generates NFRLT (Nominal time relative to first dose) that varies across every row based on dosing frequency.
  • All other column values will be duplicated on expansion.

Arguments

  • start_dtm: Column name containing the start datetime for the dosing period
  • end_dtm: Column name containing the end datetime for the dosing period
  • trt_start_dtm: Column name containing the first analyte dose datetime (used as reference for NFRLT calculation)
  • dose_freq: Column name containing the dosing frequency

Default Columns

The following default column names are used for DataFrame input:

  • start_dtm = :ASTDTM
  • end_dtm = :AENDTM
  • trt_start_dtm = :FANLDTM
  • dose_freq = :EXDOSFRQ

Notes

If dosing frequency == 'ONCE' or start date == end date; no expansion happens.

Example

julia> df = DataFrame(USUBJID = "1", EVID = 1, ASTDTM = DateTime.(["2025-01-01", "2025-02-01", "2025-03-01"]), AENDTM = DateTime.(["2025-01-31", "2025-02-28", "2025-03-31"]), EXDOSFRQ = "QD", FANLDTM = DateTime.("2025-01-01"))
3×6 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM
     │ String   Int64  DateTime             DateTime             String    DateTime
─────┼─────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00
   2 │ 1            1  2025-02-01T00:00:00  2025-02-28T00:00:00  QD        2025-01-01T00:00:00
   3 │ 1            1  2025-03-01T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00

julia> expand_dose_events(df) # QD
90×7 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM              NFRLT
     │ String   Int64  DateTime             DateTime             String    DateTime             Float64
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00      0.0
   2 │ 1            1  2025-01-02T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00     24.0
   3 │ 1            1  2025-01-03T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00     48.0
   4 │ 1            1  2025-01-04T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00     72.0
   5 │ 1            1  2025-01-05T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00     96.0
   6 │ 1            1  2025-01-06T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00    120.0
   7 │ 1            1  2025-01-07T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00    144.0
   8 │ 1            1  2025-01-08T00:00:00  2025-01-31T00:00:00  QD        2025-01-01T00:00:00    168.0
  ⋮  │    ⋮       ⋮             ⋮                    ⋮              ⋮               ⋮              ⋮
  84 │ 1            1  2025-03-25T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   1992.0
  85 │ 1            1  2025-03-26T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2016.0
  86 │ 1            1  2025-03-27T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2040.0
  87 │ 1            1  2025-03-28T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2064.0
  88 │ 1            1  2025-03-29T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2088.0
  89 │ 1            1  2025-03-30T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2112.0
  90 │ 1            1  2025-03-31T00:00:00  2025-03-31T00:00:00  QD        2025-01-01T00:00:00   2136.0
                                                                                         75 rows omitted

julia> df = DataFrame(USUBJID = "1", EVID = 1, ASTDTM = DateTime.(["2025-01-01", "2025-02-01", "2025-03-01"]), AENDTM = DateTime.(["2025-01-31", "2025-02-28", "2025-03-31"]), EXDOSFRQ = "EVERY WEEK", FANLDTM = DateTime.("2025-01-01"))
3×6 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ    FANLDTM             
     │ String   Int64  DateTime             DateTime             String      DateTime            
─────┼───────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00
   2 │ 1            1  2025-02-01T00:00:00  2025-02-28T00:00:00  EVERY WEEK  2025-01-01T00:00:00
   3 │ 1            1  2025-03-01T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00

julia> expand_dose_events(df) # EVERY WEEK
14×7 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ    FANLDTM              NFRLT   
     │ String   Int64  DateTime             DateTime             String      DateTime             Float64 
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00      0.0
   2 │ 1            1  2025-01-08T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00    168.0
   3 │ 1            1  2025-01-15T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00    336.0
   4 │ 1            1  2025-01-22T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00    504.0
   5 │ 1            1  2025-01-29T00:00:00  2025-01-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00    672.0
   6 │ 1            1  2025-02-01T00:00:00  2025-02-28T00:00:00  EVERY WEEK  2025-01-01T00:00:00    744.0
   7 │ 1            1  2025-02-08T00:00:00  2025-02-28T00:00:00  EVERY WEEK  2025-01-01T00:00:00    912.0
   8 │ 1            1  2025-02-15T00:00:00  2025-02-28T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1080.0
   9 │ 1            1  2025-02-22T00:00:00  2025-02-28T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1248.0
  10 │ 1            1  2025-03-01T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1416.0
  11 │ 1            1  2025-03-08T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1584.0
  12 │ 1            1  2025-03-15T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1752.0
  13 │ 1            1  2025-03-22T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00   1920.0
  14 │ 1            1  2025-03-29T00:00:00  2025-03-31T00:00:00  EVERY WEEK  2025-01-01T00:00:00   2088.0

Example with custom column names using kwargs

julia> df_custom = DataFrame(USUBJID = "1", EVID = 1, START_TIME = DateTime.(["2025-01-01", "2025-01-08"]), END_TIME = DateTime.(["2025-01-07", "2025-01-14"]), FREQUENCY = ["QD", "BID"], FIRST_DOSE = DateTime.("2025-01-01"))
2×6 DataFrame
 Row │ USUBJID  EVID   START_TIME           END_TIME             FREQUENCY  FIRST_DOSE
     │ String   Int64  DateTime             DateTime             String     DateTime
─────┼──────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00
   2 │ 1            1  2025-01-08T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00

julia> expand_dose_events(df_custom, start_dtm = :START_TIME, end_dtm = :END_TIME, trt_start_dtm = :FIRST_DOSE, dose_freq = :FREQUENCY)
20×7 DataFrame
 Row │ USUBJID  EVID   START_TIME           END_TIME             FREQUENCY  FIRST_DOSE           NFRLT
     │ String   Int64  DateTime             DateTime             String     DateTime             Float64
─────┼───────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00      0.0
   2 │ 1            1  2025-01-02T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00     24.0
   3 │ 1            1  2025-01-03T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00     48.0
   4 │ 1            1  2025-01-04T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00     72.0
   5 │ 1            1  2025-01-05T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00     96.0
   6 │ 1            1  2025-01-06T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00    120.0
   7 │ 1            1  2025-01-07T00:00:00  2025-01-07T00:00:00  QD         2025-01-01T00:00:00    144.0
   8 │ 1            1  2025-01-08T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    168.0
  ⋮  │    ⋮       ⋮             ⋮                    ⋮               ⋮               ⋮              ⋮
  14 │ 1            1  2025-01-11T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    240.0
  15 │ 1            1  2025-01-11T12:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    252.0
  16 │ 1            1  2025-01-12T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    264.0
  17 │ 1            1  2025-01-12T12:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    276.0
  18 │ 1            1  2025-01-13T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    288.0
  19 │ 1            1  2025-01-13T12:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    300.0
  20 │ 1            1  2025-01-14T00:00:00  2025-01-14T00:00:00  BID        2025-01-01T00:00:00    312.0
                                                                                           5 rows omitted

Example with ONCE dosing (no expansion)

julia> df_once = DataFrame(USUBJID = "1", EVID = 1, ASTDTM = DateTime.("2025-01-01"), AENDTM = DateTime.("2025-01-01"), EXDOSFRQ = "ONCE", FANLDTM = DateTime.("2025-01-01"))
1×6 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM             
     │ String   Int64  DateTime             DateTime             String    DateTime            
─────┼─────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-01T00:00:00  ONCE      2025-01-01T00:00:00

julia> expand_dose_events(df_once)
1×7 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM              NFRLT   
     │ String   Int64  DateTime             DateTime             String    DateTime             Float64 
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-01T00:00:00  ONCE      2025-01-01T00:00:00      0.0

Example with different dosing frequencies

julia> df_freq = DataFrame(USUBJID = ["1", "1", "1"], EVID = [1, 1, 1], ASTDTM = DateTime.(["2025-01-01", "2025-01-01", "2025-01-01"]), AENDTM = DateTime.(["2025-01-03", "2025-01-03", "2025-01-02"]), EXDOSFRQ = ["TID", "Q8H", "Q12H"], FANLDTM = DateTime.(["2025-01-01", "2025-01-01", "2025-01-01"]))
3×6 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM             
     │ String   Int64  DateTime             DateTime             String    DateTime            
─────┼─────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00
   2 │ 1            1  2025-01-01T00:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00
   3 │ 1            1  2025-01-01T00:00:00  2025-01-02T00:00:00  Q12H      2025-01-01T00:00:00

julia> expand_dose_events(df_freq)
17×7 DataFrame
 Row │ USUBJID  EVID   ASTDTM               AENDTM               EXDOSFRQ  FANLDTM              NFRLT   
     │ String   Int64  DateTime             DateTime             String    DateTime             Float64 
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1            1  2025-01-01T00:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00      0.0
   2 │ 1            1  2025-01-01T08:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00      8.0
   3 │ 1            1  2025-01-01T16:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00     16.0
   4 │ 1            1  2025-01-02T00:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00     24.0
   5 │ 1            1  2025-01-02T08:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00     32.0
   6 │ 1            1  2025-01-02T16:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00     40.0
   7 │ 1            1  2025-01-03T00:00:00  2025-01-03T00:00:00  TID       2025-01-01T00:00:00     48.0
   8 │ 1            1  2025-01-01T00:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00      0.0
   9 │ 1            1  2025-01-01T08:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00      8.0
  10 │ 1            1  2025-01-01T16:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00     16.0
  11 │ 1            1  2025-01-02T00:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00     24.0
  12 │ 1            1  2025-01-02T08:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00     32.0
  13 │ 1            1  2025-01-02T16:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00     40.0
  14 │ 1            1  2025-01-03T00:00:00  2025-01-03T00:00:00  Q8H       2025-01-01T00:00:00     48.0
  15 │ 1            1  2025-01-01T00:00:00  2025-01-02T00:00:00  Q12H      2025-01-01T00:00:00      0.0
  16 │ 1            1  2025-01-01T12:00:00  2025-01-02T00:00:00  Q12H      2025-01-01T00:00:00     12.0
  17 │ 1            1  2025-01-02T00:00:00  2025-01-02T00:00:00  Q12H      2025-01-01T00:00:00     24.0
ADaM.interdose_interval — Method
interdose_interval(df::DataFrame)

Returns Inter-dose Interval(II hours) when a valid dose freqency is passed. Dose frequencies are defined using CDISC SDTM Controlled Terminology. The assumptions made are

  • A day is 24 hours
  • A week is 7 days
  • A month is 30 days
  • An year is 52 weeks

Example

julia> interdose_interval("QD")
24

julia> interdose_interval("EVERY AFTERNOON")
24

julia> interdose_interval("Q45MIN")
0.75

julia> interdose_interval("QY")
8736

julia> interdose_interval("ONCE WEEKLY")
168

julia> df = DataFrame(USUBJID = "1", EXDOSFRQ = ["QD", "BID", "QD", "TID"])
4×2 DataFrame
 Row │ USUBJID  EXDOSFRQ 
     │ String   String   
─────┼───────────────────
   1 │ 1        QD
   2 │ 1        BID
   3 │ 1        QD
   4 │ 1        TID

julia> @rtransform! df :II = interdose_interval(:EXDOSFRQ)
4×3 DataFrame
 Row │ USUBJID  EXDOSFRQ  II   
     │ String   String    Real 
─────┼─────────────────────────
   1 │ 1        QD        24
   2 │ 1        BID       12.0
   3 │ 1        QD        24
   4 │ 1        TID        8.0
ADaM.join_columns — Method
join_columns(target, reference; on, order, rev_order, keep, filter_ref, filter_join, mode)

Add columns from a reference DataFrame to a target DataFrame with flexible matching and filtering.

Positional Arguments

  • target: Main DataFrame to join to
  • reference: DataFrame to join columns from

Keyword Arguments

  • on: Column(s) to match on (join keys)
  • order: Column(s) to sort matches by (optional)
  • rev_order: Boolean vector indicating descending sort for each order column (default: all ascending)
  • keep: Vector of source => destination pairs for column mapping (default: all reference columns)
  • filter_ref: Function to pre-filter reference rows (default: include all)
  • filter_join: Function (target_row, ref_row) to filter matches (default: include all)
  • mode: Take :first or :last match after sorting (default: :first)

Examples

julia> patients = DataFrame(SUBJID = [1, 2, 3], VISIT_DATE = [Date("2023-01-15"), Date("2023-01-20"), Date("2023-01-25")])
3×2 DataFrame
 Row │ SUBJID  VISIT_DATE
     │ Int64   Dates.Date
─────┼────────────────────
   1 │      1  2023-01-15
   2 │      2  2023-01-20
   3 │      3  2023-01-25

julia> lab_data = DataFrame(SUBJID = [1, 1, 1, 2, 2, 3], LAB_DATE = [Date("2023-01-01"), Date("2023-01-10"), Date("2023-01-18"), Date("2023-01-05"), Date("2023-01-25"), Date("2023-01-20")], LAB_VALUE = [100, 95, 88, 110, 105, 92])
6×3 DataFrame
 Row │ SUBJID  LAB_DATE    LAB_VALUE 
     │ Int64   Date        Int64     
─────┼───────────────────────────────
   1 │      1  2023-01-01        100
   2 │      1  2023-01-10         95
   3 │      1  2023-01-18         88
   4 │      2  2023-01-05        110
   5 │      2  2023-01-25        105
   6 │      3  2023-01-20         92

julia> join_columns(patients, lab_data; on = [:SUBJID], order = [:LAB_DATE], keep = [:LAB_VALUE => :PRIOR_LAB, :LAB_DATE => :PRIOR_LAB_DATE], filter_join = (t, r) -> r.LAB_DATE < t.VISIT_DATE, mode = :last)
3×4 DataFrame
 Row │ SUBJID  VISIT_DATE  PRIOR_LAB  PRIOR_LAB_DATE
     │ Int64   Dates.Date  Int64?     Dates.Date?
─────┼───────────────────────────────────────────────
   1 │      1  2023-01-15         95  2023-01-10
   2 │      2  2023-01-20        110  2023-01-05
   3 │      3  2023-01-25         92  2023-01-20
ADaM.make_dose_interruption — Method
make_dose_interruption(df::DataFrame)
  • Creates dose interruption columns INTRFL (Interruption Flag) and INTRDUR (Interruption Duration) on the EX (dose) dataset where doses are recorded as intervals.
  • If there is any gap(more than a day) between end date AENDT of one dosing interval and start date ASTDT of the next interval within a subject, the interruption will be recorded in rows. If there is no interruption, these columns will be missing.

The EX dataset needs to be prepared with the following columns :

  • ASTDT (Analysis Start Date)
  • AENDT (Analysis End Date)
  • FANLDTM (First Anaalyte dose DateTime)

Example

julia> df = DataFrame(USUBJID = "1", EXDOSFRQ = "QD", EVID = 1, ASTDT = Date.(["2025-01-01", "2025-02-01", "2025-03-03"]), AENDT = Date.(["2025-01-31", "2025-02-28", "2025-03-31"]), FANLDTM = Date.("2025-01-01"))
3×6 DataFrame
 Row │ USUBJID  EXDOSFRQ  EVID   ASTDT       AENDT       FANLDTM    
     │ String   String    Int64  Date        Date        Date       
─────┼──────────────────────────────────────────────────────────────
   1 │ 1        QD            1  2025-01-01  2025-01-31  2025-01-01
   2 │ 1        QD            1  2025-02-01  2025-02-28  2025-01-01
   3 │ 1        QD            1  2025-03-03  2025-03-31  2025-01-01

julia> make_dose_interruption(df)
4×10 DataFrame
 Row │ USUBJID  EXDOSFRQ  EVID   ASTDT       AENDT       FANLDTM     ASTDY  AENDY  INTRFL   INTRDUR 
     │ String   String    Int64  Date        Date        Date        Int64  Int64  String?  Int64?  
─────┼──────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1        QD            1  2025-01-01  2025-01-31  2025-01-01      1     31  missing  missing 
   2 │ 1        QD            1  2025-02-01  2025-02-28  2025-01-01     32     59  missing  missing 
   3 │ 1        QD            1  2025-03-01  2025-03-02  2025-01-01     60     61  Y              2
   4 │ 1        QD            1  2025-03-03  2025-03-31  2025-01-01     62     90  missing  missing
ADaM.make_dtm — Method
make_dtm(df::DataFrame, col; prefix, fmt, on_error)
  • Derives a DateTime column from a DateTime-convertible String/Date/DateTime column in a DataFrame.
  • The derived column is named <prefix>DTM (default prefix "A").
  • fmt controls parsing of String columns:
    • :iso8601_auto (default) — dispatch by string length on the six CDISC ISO 8601 DTC precisions (yyyy, yyyy-mm, yyyy-mm-dd, yyyy-mm-ddTHH, yyyy-mm-ddTHH:MM, yyyy-mm-ddTHH:MM:SS).
    • DateFormat or format String — single fixed format.
    • Vector{DateFormat} — try each in order; first match wins.
  • on_error controls behavior on unparseable strings:
    • :error (default) — throw, today's behavior.
    • :missing — return missing for that row.

Example

julia> df = DataFrame(DTC = ["2024", "2025", "2026"])
3×1 DataFrame
 Row │ DTC
     │ String
─────┼────────
   1 │ 2024
   2 │ 2025
   3 │ 2026

julia> make_dtm(df, "DTC")
3×2 DataFrame
 Row │ DTC     ADTM
     │ String  DateTime
─────┼─────────────────────────────
   1 │ 2024    2024-01-01T00:00:00
   2 │ 2025    2025-01-01T00:00:00
   3 │ 2026    2026-01-01T00:00:00

julia> df = DataFrame(DTC = Date.(["2024", "2025", "2026"]))
3×1 DataFrame
 Row │ DTC
     │ Date
─────┼────────────
   1 │ 2024-01-01
   2 │ 2025-01-01
   3 │ 2026-01-01

julia> make_dtm(df, "DTC", prefix = "B")
3×2 DataFrame
 Row │ DTC         BDTM
     │ Date        DateTime
─────┼─────────────────────────────────
   1 │ 2024-01-01  2024-01-01T00:00:00
   2 │ 2025-01-01  2025-01-01T00:00:00
   3 │ 2026-01-01  2026-01-01T00:00:00

Input col and prefix can be passed as either String or Symbol.

julia> df = DataFrame(DTC = ["2024", "2025", "2026"])
3×1 DataFrame
 Row │ DTC
     │ String
─────┼────────
   1 │ 2024
   2 │ 2025
   3 │ 2026

julia> make_dtm(df, :DTC, prefix = :AST)
3×2 DataFrame
 Row │ DTC     ASTDTM
     │ String  DateTime
─────┼─────────────────────────────
   1 │ 2024    2024-01-01T00:00:00
   2 │ 2025    2025-01-01T00:00:00
   3 │ 2026    2026-01-01T00:00:00

:iso8601_auto handles mixed-precision ISO 8601 DTC columns in a single pass.

julia> df = DataFrame(DTC = ["2024-01-15", "2024-01-15T08:30", "2024-01-15T08:30:45"])
3×1 DataFrame
 Row │ DTC
     │ String
─────┼─────────────────────
   1 │ 2024-01-15
   2 │ 2024-01-15T08:30
   3 │ 2024-01-15T08:30:45

julia> make_dtm(df, :DTC)
3×2 DataFrame
 Row │ DTC                  ADTM
     │ String               DateTime
─────┼──────────────────────────────────────────
   1 │ 2024-01-15           2024-01-15T00:00:00
   2 │ 2024-01-15T08:30     2024-01-15T08:30:00
   3 │ 2024-01-15T08:30:45  2024-01-15T08:30:45

For a different DateTime Format. Valid Julia Date Formats

julia> df = DataFrame(DTC = ["2024-May-21T01:10", "2024-Jun-22T02:20", "2024-Aug-23T03:30"])
3×1 DataFrame
 Row │ DTC
     │ String
─────┼───────────────────
   1 │ 2024-May-21T01:10
   2 │ 2024-Jun-22T02:20
   3 │ 2024-Aug-23T03:30

julia> make_dtm(df, "DTC", prefix = "C", fmt = "yyyy-u-ddTHH:MM")
3×2 DataFrame
 Row │ DTC                CDTM
     │ String             DateTime
─────┼────────────────────────────────────────
   1 │ 2024-May-21T01:10  2024-05-21T01:10:00
   2 │ 2024-Jun-22T02:20  2024-06-22T02:20:00
   3 │ 2024-Aug-23T03:30  2024-08-23T03:30:00

julia> make_dtm(df, :DTC, prefix = "C", fmt = dateformat"yyyy-u-ddTHH:MM") # dateformat object
3×2 DataFrame
 Row │ DTC                CDTM
     │ String             DateTime
─────┼────────────────────────────────────────
   1 │ 2024-May-21T01:10  2024-05-21T01:10:00
   2 │ 2024-Jun-22T02:20  2024-06-22T02:20:00
   3 │ 2024-Aug-23T03:30  2024-08-23T03:30:00
ADaM.make_duration — Method
make_duration(; start_dtm, end_dtm, output_unit)
make_duration(df, col; start_dtm, end_dtm, output_unit)

Computes the duration between two dates or datetimes and returns the result in the specified unit. Dates can be passed as Scalars or Vectors via DataFrame.`

Arguments:

  • start_dtm: The starting date or datetime.
  • end_dtm: The ending date or datetime.
  • output_unit: The unit for the output duration (e.g., :h for hours, :min for minutes). Default is :h.

Valid time units supported by DynamicQuantities.jl.

Returns:

  • The DataFrame with new columns (col) for the duration, its unit, and the stripped value.

Examples

Scalar Dates

julia> make_duration(start_dtm=DateTime(2024, 1, 1, 8), end_dtm=DateTime(2024, 1, 1, 10), output_unit=:h)
2.0 h

julia> make_duration(start_dtm=Date(2024, 1, 1), end_dtm=Date(2024, 1, 2), output_unit=:h)
24.0 h

Date columns

julia> df = DataFrame(ASTDT = [Date(2024,1,1), Date(2024,2,1)], AENDT = [Date(2024,3,1), Date(2024,4,1)])
2×2 DataFrame
 Row │ ASTDT       AENDT      
     │ Date        Date       
─────┼────────────────────────
   1 │ 2024-01-01  2024-03-01
   2 │ 2024-02-01  2024-04-01

julia> make_duration(df, :ADUR; start_dtm=:ASTDT, end_dtm=:AENDT, output_unit=:day)
2×4 DataFrame
 Row │ ASTDT       AENDT       ADUR     ADURU     
     │ Date        Date        Float64  Symbolic… 
─────┼────────────────────────────────────────────
   1 │ 2024-01-01  2024-03-01     60.0  day
   2 │ 2024-02-01  2024-04-01     60.0  day

DateTime columns

julia> df = DataFrame(ASTDTM = [DateTime(2024,1,1,8), DateTime(2024,1,1,9)], AENDTM = [DateTime(2024,1,1,10), DateTime(2024,1,1,12)])
2×2 DataFrame
 Row │ ASTDTM               AENDTM              
     │ DateTime             DateTime            
─────┼──────────────────────────────────────────
   1 │ 2024-01-01T08:00:00  2024-01-01T10:00:00
   2 │ 2024-01-01T09:00:00  2024-01-01T12:00:00

julia> make_duration(df, :ADUR; start_dtm=:ASTDTM, end_dtm=:AENDTM, output_unit=:h)
2×4 DataFrame
 Row │ ASTDTM               AENDTM               ADUR     ADURU     
     │ DateTime             DateTime             Float64  Symbolic… 
─────┼──────────────────────────────────────────────────────────────
   1 │ 2024-01-01T08:00:00  2024-01-01T10:00:00      2.0  h
   2 │ 2024-01-01T09:00:00  2024-01-01T12:00:00      3.0  h
   
julia> make_duration(df, :ADUR; start_dtm=:ASTDTM, end_dtm=:AENDTM, output_unit=:day)
2×4 DataFrame
 Row │ ASTDTM               AENDTM               ADUR       ADURU     
     │ DateTime             DateTime             Float64    Symbolic… 
─────┼────────────────────────────────────────────────────────────────
   1 │ 2024-01-01T08:00:00  2024-01-01T10:00:00  0.0833333  day
   2 │ 2024-01-01T09:00:00  2024-01-01T12:00:00  0.125      day
ADaM.make_nominal_time — Method
derive_nominal_time(pc, ex, domain; visitdy_col, atptn_col)

Derives the nominal time column NFRLT for PC and EX SDTM datasets.

Arguments

  • pc: PC DataFrame (must contain ATPTN)
  • ex: EX DataFrame
  • domain: The output DataFrame (PC or EX)
  • visitdy_col: Column name for VISITDY (Symbol or String, default = :VISITDY)
  • atptn_col: Column name for ATPTN (Symbol or String, default = :ATPTN)

Output

  • Returns a DataFrame (PC or EX) with a new NFRLT column representing nominal time in hours.

Notes

  • For PC: NFRLT = (VISITDY - 1) * 24 + ATPTN
  • For EX: NFRLT = (VISITDY - 1) * 24
  • If VISITDY is missing in either dataset, it is joined from the other dataset using common keys (excluding ATPTN for PC).
  • Throws an error if required columns are missing or if join keys cannot be determined.

Examples

julia> pc = DataFrame(DOMAIN = ["PC", "PC"], USUBJID = ["01", "01"], VISITDY = [2, 3], ATPTN = [0, 24]) # Example PC and EX DataFrames
2×4 DataFrame
 Row │ DOMAIN  USUBJID  VISITDY  ATPTN 
     │ String  String   Int64    Int64 
─────┼─────────────────────────────────
   1 │ PC      01             2      0
   2 │ PC      01             3     24

julia> ex = DataFrame(DOMAIN = ["EX", "EX"], USUBJID = ["01", "01"], VISITDY = [2, 3])
2×3 DataFrame
 Row │ DOMAIN  USUBJID  VISITDY 
     │ String  String   Int64   
─────┼──────────────────────────
   1 │ EX      01             2
   2 │ EX      01             3

julia> make_nominal_time(pc, ex, "PC") # Example 1: Standard usage for PC
2×5 DataFrame
 Row │ DOMAIN  USUBJID  VISITDY  ATPTN  NFRLT 
     │ String  String   Int64    Int64  Int64 
─────┼────────────────────────────────────────
   1 │ PC      01             2      0     24
   2 │ PC      01             3     24     72

julia> make_nominal_time(pc, ex, "EX") # Example 2: Standard usage for EX
2×4 DataFrame
 Row │ DOMAIN  USUBJID  VISITDY  NFRLT 
     │ String  String   Int64    Int64 
─────┼─────────────────────────────────
   1 │ EX      01             2     24
   2 │ EX      01             3     48

julia> rename!(pc, :VISITDY => :VISDY, :ATPTN => :TIMEPT) # Example 3: Custom column names
2×4 DataFrame
 Row │ DOMAIN  USUBJID  VISDY  TIMEPT 
     │ String  String   Int64  Int64  
─────┼────────────────────────────────
   1 │ PC      01           2       0
   2 │ PC      01           3      24

julia> make_nominal_time(pc, ex, "PC"; visitdy_col=:VISDY, atptn_col=:TIMEPT)
2×5 DataFrame
 Row │ DOMAIN  USUBJID  VISDY  TIMEPT  NFRLT 
     │ String  String   Int64  Int64   Int64 
─────┼───────────────────────────────────────
   1 │ PC      01           2       0     24
   2 │ PC      01           3      24     72
ADaM.make_time_fl — Method
make_time_fl(df::DataFrame, col; group, order, strict)

This function creates flag columns (FN (Flag Numeric) and FC (Flag comment)), that flags the time information based on following criteria.

  • Missing Times
  • Negative Times
  • Non-ascending Times

Non-ascending time detection flags the entire sequence of rows that fall below the running maximum of previously seen valid times (within each group), not just the single row where the drop occurs. The sequence continues to be flagged until a row ascends back above the running maximum.

When strict = true, equal consecutive times are also flagged as non-ascending (i.e., strictly increasing times are required). The default strict = false accepts equal times.

The points to be considered for data validation can be referred through this link: https://www.lexjansen.com/phuse-us/2020/as/AS06.pdf#page=3

order and group variables can be used while flagging.

Example

julia> df = DataFrame(USUBJID = 1, AFRLT = [0, -1, -2, 3, missing, 5, 4])
7×2 DataFrame
 Row │ USUBJID  AFRLT
     │ Int64    Int64?
─────┼──────────────────
   1 │       1        0
   2 │       1       -1
   3 │       1       -2
   4 │       1        3
   5 │       1  missing
   6 │       1        5
   7 │       1        4

julia> make_time_fl(df, :AFRLT, group = [], order = []) # group variables can be kept empty for dataframe without group.
7×4 DataFrame
 Row │ USUBJID  AFRLT    AFRLTFN  AFRLTFC
     │ Int64    Int64?   Int64    String
─────┼───────────────────────────────────────────────
   1 │       1        0        0
   2 │       1       -1        1  Negative Time
   3 │       1       -2        1  Negative Time
   4 │       1        3        0
   5 │       1  missing        1  Missing Time
   6 │       1        5        0
   7 │       1        4        1  Non-ascending Time

julia> df2 = DataFrame(USUBJID = 1, AFRLT = [0, 5, 10, 3, 4, 7, 12])
7×2 DataFrame
 Row │ USUBJID  AFRLT
     │ Int64    Int64
─────┼────────────────
   1 │       1      0
   2 │       1      5
   3 │       1     10
   4 │       1      3
   5 │       1      4
   6 │       1      7
   7 │       1     12

julia> make_time_fl(df2, :AFRLT)
7×4 DataFrame
 Row │ USUBJID  AFRLT  AFRLTFN  AFRLTFC
     │ Int64    Int64  Int64    String
─────┼─────────────────────────────────────────────
   1 │       1      0        0
   2 │       1      5        0
   3 │       1     10        0
   4 │       1      3        1  Non-ascending Time
   5 │       1      4        1  Non-ascending Time
   6 │       1      7        1  Non-ascending Time
   7 │       1     12        0

julia> df3 = DataFrame(USUBJID = 1, AFRLT = [0, 1, 1, 2, 3])
5×2 DataFrame
 Row │ USUBJID  AFRLT
     │ Int64    Int64
─────┼────────────────
   1 │       1      0
   2 │       1      1
   3 │       1      1
   4 │       1      2
   5 │       1      3

julia> make_time_fl(df3, :AFRLT)  # strict=false (default): equal times accepted
5×4 DataFrame
 Row │ USUBJID  AFRLT  AFRLTFN  AFRLTFC
     │ Int64    Int64  Int64    String
─────┼──────────────────────────────────
   1 │       1      0        0
   2 │       1      1        0
   3 │       1      1        0
   4 │       1      2        0
   5 │       1      3        0

julia> make_time_fl(df3, :AFRLT, strict = true)  # equal times flagged
5×4 DataFrame
 Row │ USUBJID  AFRLT  AFRLTFN  AFRLTFC
     │ Int64    Int64  Int64    String
─────┼─────────────────────────────────────────────
   1 │       1      0        0
   2 │       1      1        0
   3 │       1      1        1  Non-ascending Time
   4 │       1      2        0
   5 │       1      3        0
ADaM.merge_columns — Method
 merge_columns(df1, df2; filter_func, keep, kwargs...)
  • leftjoin's new column(s) to the primary dataset based on columns from a secondary dataset.

Positional Arguments

  • df1: Main DataFrame to join to
  • df2: DataFrame to leftjoin columns from

Keyword Arguments

  • keep: Vector of source => destination pairs for column mapping (default: all reference columns)
  • filter_func: Function to filter rows from secondary dataset (default: include all)
  • kwargs...: Keyword aguments of leftjoin function can be passed. Eg: on, makeunique etc.

Example

julia> dm = DataFrame(USUBJID = ["01-701-1015", "01-701-1023", "01-701-1028"], SEX = ["M", "F", "F"], COUNTRY = ["USA", "USA", "NOR"])
3×3 DataFrame
 Row │ USUBJID      SEX     COUNTRY
     │ String       String  String
─────┼──────────────────────────────
   1 │ 01-701-1015  M       USA
   2 │ 01-701-1023  F       USA
   3 │ 01-701-1028  F       NOR

julia> vs = DataFrame([("01-701-1015", "WEIGHT", 50, "kg"), ("01-701-1015", "HEIGHT", 150, "cm"), ("01-701-1023", "WEIGHT", 60, "kg"), ("01-701-1023", "HEIGHT", 160, "cm"), ("01-701-1028", "WEIGHT", 70, "kg"), ("01-701-1028", "HEIGHT", 170, "cm")], [:USUBJID, :VSTESTCD, :VSSTRESN, :VSSTRESU])
6×4 DataFrame
 Row │ USUBJID      VSTESTCD  VSSTRESN  VSSTRESU 
     │ String       String    Int64     String   
─────┼───────────────────────────────────────────
   1 │ 01-701-1015  WEIGHT          50  kg
   2 │ 01-701-1015  HEIGHT         150  cm
   3 │ 01-701-1023  WEIGHT          60  kg
   4 │ 01-701-1023  HEIGHT         160  cm
   5 │ 01-701-1028  WEIGHT          70  kg
   6 │ 01-701-1028  HEIGHT         170  cm

julia> merge_ht = merge_columns(dm, vs, filter_func = r -> r.VSTESTCD == "HEIGHT", keep = [:VSSTRESN => :HTBL, :VSSTRESU => :HTBLU], on = [:USUBJID])
3×5 DataFrame
 Row │ USUBJID      SEX     COUNTRY  HTBL    HTBLU   
     │ String       String  String   Int64?  String? 
─────┼───────────────────────────────────────────────
   1 │ 01-701-1015  M       USA         150  cm
   2 │ 01-701-1023  F       USA         160  cm
   3 │ 01-701-1028  F       NOR         170  cm

julia> merge_ht_wt = merge_columns(merge_ht, vs, filter_func = r -> r.VSTESTCD == "WEIGHT", keep = ["VSSTRESN" => "WTBL", "VSSTRESU" => "WTBLU"], on = ["USUBJID"])
3×7 DataFrame
 Row │ USUBJID      SEX     COUNTRY  HTBL    HTBLU    WTBL    WTBLU   
     │ String       String  String   Int64?  String?  Int64?  String? 
─────┼────────────────────────────────────────────────────────────────
   1 │ 01-701-1015  M       USA         150  cm           50  kg
   2 │ 01-701-1023  F       USA         160  cm           60  kg
   3 │ 01-701-1028  F       NOR         170  cm           70  kg

julia> merge_columns(dm, vs, filter_func = r -> r.VSTESTCD == "HEIGHT", on = [:USUBJID]) # without specifying keep
3×6 DataFrame
 Row │ USUBJID      SEX     COUNTRY  VSTESTCD  VSSTRESN  VSSTRESU 
     │ String       String  String   String?   Int64?    String?  
─────┼────────────────────────────────────────────────────────────
   1 │ 01-701-1015  M       USA      HEIGHT         150  cm
   2 │ 01-701-1023  F       USA      HEIGHT         160  cm
   3 │ 01-701-1028  F       NOR      HEIGHT         170  cm

julia> merge_columns(dm, vs, on = [:USUBJID]) # without specifying keep and filter_func
6×6 DataFrame
 Row │ USUBJID      SEX     COUNTRY  VSTESTCD  VSSTRESN  VSSTRESU 
     │ String       String  String   String?   Int64?    String?  
─────┼────────────────────────────────────────────────────────────
   1 │ 01-701-1015  M       USA      WEIGHT          50  kg
   2 │ 01-701-1015  M       USA      HEIGHT         150  cm
   3 │ 01-701-1023  F       USA      WEIGHT          60  kg
   4 │ 01-701-1023  F       USA      HEIGHT         160  cm
   5 │ 01-701-1028  F       NOR      WEIGHT          70  kg
   6 │ 01-701-1028  F       NOR      HEIGHT         170  cm
ADaM.merge_covariates — Method
merge_covariates(df1, df2; domain, covariates, filter_func, baseline=true, kwargs...)
  • Iteratively merges covariate columns from a secondary DataFrame (df2) into a primary DataFrame (df1) using ADaM.merge_columns for each covariate in the covariates vector.
  • This is designed for SDTM-style domains (e.g., LB, VS) where a test code column (e.g., LBTESTCD) identifies the covariate.

Positional Arguments

  • df1: Main DataFrame to join to (primary dataset)
  • df2: DataFrame to leftjoin columns from (secondary dataset)

Keyword Arguments

  • domain: domain prefix (String or Symbol) used to construct test code and value column names (e.g., LBTESTCD, LBSTRESN, LBSTRESU)
  • covariates: Vector of covariate names to merge iteratively
  • filter_func: Function to filter rows from the secondary dataset for each covariate.
  • baseline: If true, appends "BL"/"BLU" to output column names (default: true)
  • kwargs...: Keyword aguments of leftjoin function can be passed. Eg: on, makeunique etc.

Examples

julia> dm = DataFrame(USUBJID = ["01-701-1015"], SEX = ["M"], COUNTRY = ["USA"])
1×3 DataFrame
 Row │ USUBJID      SEX     COUNTRY 
     │ String       String  String  
─────┼──────────────────────────────
   1 │ 01-701-1015  M       USA

julia> lb = DataFrame(USUBJID = fill("01-701-1015", 16), LBTESTCD = repeat(["AST", "ALT", "CREAT", "BILI"], inner = 4), LBSTRESN = [35, 30, 28, 33, 40, 32, 29, 35, 1, 0, 1, 1, 0, 1, 1, 0], LBSTRESU = vcat(fill("U/L", 8), fill("mg/dL", 8)), LBBLFL = repeat(["Y", "", "", ""], 4))
16×5 DataFrame
 Row │ USUBJID      LBTESTCD  LBSTRESN  LBSTRESU  LBBLFL 
     │ String       String    Int64     String    String 
─────┼───────────────────────────────────────────────────
   1 │ 01-701-1015  AST             35  U/L       Y
   2 │ 01-701-1015  AST             30  U/L
   3 │ 01-701-1015  AST             28  U/L
   4 │ 01-701-1015  AST             33  U/L
   5 │ 01-701-1015  ALT             40  U/L       Y
   6 │ 01-701-1015  ALT             32  U/L
   7 │ 01-701-1015  ALT             29  U/L
   8 │ 01-701-1015  ALT             35  U/L
   9 │ 01-701-1015  CREAT            1  mg/dL     Y
  10 │ 01-701-1015  CREAT            0  mg/dL
  11 │ 01-701-1015  CREAT            1  mg/dL
  12 │ 01-701-1015  CREAT            1  mg/dL
  13 │ 01-701-1015  BILI             0  mg/dL     Y
  14 │ 01-701-1015  BILI             1  mg/dL
  15 │ 01-701-1015  BILI             1  mg/dL
  16 │ 01-701-1015  BILI             0  mg/dL

julia> merge_covariates(dm, lb; domain = "LB", covariates = ["AST", "ALT", "CREAT", "BILI"], filter_func = r -> coalesce(r.LBBLFL, "") == "Y", on = [:USUBJID])
1×11 DataFrame
 Row │ USUBJID      SEX     COUNTRY  ASTBL   ASTBLU   ALTBL   ALTBLU   CREATBL  CREATBLU  BILIBL  BILIBLU 
     │ String       String  String   Int64?  String?  Int64?  String?  Int64?   String?   Int64?  String? 
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 01-701-1015  M       USA          35  U/L          40  U/L            1  mg/dL          0  mg/dL

julia> merge_covariates(dm, lb; domain = "LB", covariates = ["AST", "ALT", "CREAT", "BILI"], on = [:USUBJID]) # The baseline filter will be automatically determined if appropriate columns are present (LBBLFL == "Y", LBLOBXFL == "Y", VISIT == "Screening")
1×11 DataFrame
 Row │ USUBJID      SEX     COUNTRY  ASTBL   ASTBLU   ALTBL   ALTBLU   CREATBL  CREATBLU  BILIBL  BILIBLU
     │ String       String  String   Int64?  String?  Int64?  String?  Int64?   String?   Int64?  String?
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 01-701-1015  M       USA          35  U/L          40  U/L            1  mg/dL          0  mg/dL
ADaM.nci_liver_score — Method
nci_liver_score(bili::Number, ast::Number, uln_bili::Number, uln_ast::Number; kwargs...)
nci_liver_score(bili::Quantity, ast::Number, uln_bili::Quantity, uln_ast::Quantity; kwargs...)
nci_liver_score(df::DataFrame; kwargs...)

Compute NCI (National Cancer Institute) hepatic impairment score (numeric, categorical) from Bilirubin and AST values which can be provided as Quantitys, Scalars or Vectors via DataFrame. Refer NCI Criteria

Arguments

  • bili: Bilirubin value.
  • ast: AST value.
  • uln_bili: Upper limit of normal for bilirubin.
  • uln_ast: Upper limit of normal for AST.
  • bili_unit: Bilirubin unit. (default: "umol/L")
  • ast_unit: Column name for AST unit (default: "U/L").

Default Columns

The following default column names are used for DataFrame input:

  • bili = :BILIBL
  • ast = :ASTBL
  • uln_bili = :ULN_BILIBL
  • uln_ast = :ULN_ASTBL
  • bili_unit = :BILIBLU
  • ast_unit = :ASTBLU
  • col = :NCI

Output

  • Numeric NCI liver score (1=Normal, 2=Mild, 3=Moderate, 4=Severe, or missing)
  • Categorical NCI liver score ("Normal", "Mild", "Moderate", "Severe", or missing)

Result output in form of DataFrame Columns or a 2-element Tuple.

Notes

  • The function implements the NCI hepatic impairment criteria:
    • Severe: bili > 3 * uln_bili
    • Moderate: 1.5 * uln_bili < bili <= 3 * uln_bili
    • Mild: (bili > uln_bili && bili <= 1.5 * uln_bili) || (ast > uln_ast)
    • Normal: bili <= uln_bili && ast <= uln_ast
  • Returns missing for both outputs if any argument is missing or does not match a category.
  • Output columns can be customized via the col keyword argument.
  • The units of bili and uln_bili must match; otherwise, an error is thrown.

Examples

julia> nci_liver_score(7.0, 20.0, 2.0, 30.0, bili_unit="mg/dL", ast_unit="U/L")
(4, "Severe")

julia> nci_liver_score(4.0, 20.0, 2.0, 30.0)
(3, "Moderate")

julia> nci_liver_score(2.2, 20.0, 2.0, 15.0)
(2, "Mild")

julia> nci_liver_score(1.0, 10.0, 2.0, 15.0)
(1, "Normal")

julia> nci_liver_score(7us"mg/dL", 20, 2us"mg/dL", 30)
(4, "Severe")

julia> nci_liver_score(4us"mg/dL", 20, 2us"mg/dL", 30)
(3, "Moderate")

julia> nci_liver_score(2.2us"mg/dL", 20, 2us"mg/dL", 15)
(2, "Mild")

julia> nci_liver_score(1us"mg/dL", 10, 2us"mg/dL", 15)
(1, "Normal")

julia> df = DataFrame(BILIBL = [7.0, 4.0, 2.2, 1.0, missing], ASTBL = [20.0, 20.0, 20.0, 10.0, 10.0], ULN_BILIBL = [2.0, 2.0, 2.0, 2.0, 2.0], ULN_ASTBL = [30.0, 30.0, 15.0, 15.0, 15.0], BILIBLU = "mg/dL", ASTBLU = "U/L")
5×6 DataFrame
 Row │ BILIBL     ASTBL    ULN_BILIBL  ULN_ASTBL  BILIBLU  ASTBLU 
     │ Float64?   Float64  Float64     Float64    String   String 
─────┼────────────────────────────────────────────────────────────
   1 │       7.0     20.0         2.0       30.0  mg/dL    U/L
   2 │       4.0     20.0         2.0       30.0  mg/dL    U/L
   3 │       2.2     20.0         2.0       15.0  mg/dL    U/L
   4 │       1.0     10.0         2.0       15.0  mg/dL    U/L
   5 │ missing       10.0         2.0       15.0  mg/dL    U/L

julia> nci_liver_score(df)
5×8 DataFrame
 Row │ BILIBL     ASTBL    ULN_BILIBL  ULN_ASTBL  BILIBLU  ASTBLU  NCIN     NCIC     
     │ Float64?   Float64  Float64     Float64    String   String  Int64?   String?  
─────┼───────────────────────────────────────────────────────────────────────────────
   1 │       7.0     20.0         2.0       30.0  mg/dL    U/L           4  Severe
   2 │       4.0     20.0         2.0       30.0  mg/dL    U/L           3  Moderate
   3 │       2.2     20.0         2.0       15.0  mg/dL    U/L           2  Mild
   4 │       1.0     10.0         2.0       15.0  mg/dL    U/L           1  Normal
   5 │ missing       10.0         2.0       15.0  mg/dL    U/L     missing  missing

julia> nci_liver_score(df, col=:MYNCI)
5×8 DataFrame
 Row │ BILIBL     ASTBL    ULN_BILIBL  ULN_ASTBL  BILIBLU  ASTBLU  MYNCIN   MYNCIC   
     │ Float64?   Float64  Float64     Float64    String   String  Int64?   String?  
─────┼───────────────────────────────────────────────────────────────────────────────
   1 │       7.0     20.0         2.0       30.0  mg/dL    U/L           4  Severe
   2 │       4.0     20.0         2.0       30.0  mg/dL    U/L           3  Moderate
   3 │       2.2     20.0         2.0       15.0  mg/dL    U/L           2  Mild
   4 │       1.0     10.0         2.0       15.0  mg/dL    U/L           1  Normal
   5 │ missing       10.0         2.0       15.0  mg/dL    U/L     missing  missing
ADaM.occasion — Method
occasion(df::AbstractDataFrame; group, order, anchor_filter, occ_col, pre_first)

Derive an occasion (OCC) column on a longitudinal dataset (typically an ADPPK BDS dataset). The dataframe is sorted by [group..., order...] upfront; within each group, OCC is the cumulative count of anchor rows seen so far in that sort order. Rows before the first anchor receive pre_first. The returned dataframe is in sorted order.

The caller supplies an anchor_filter predicate that returns true for rows that should increment the occasion counter. This is a deliberate flexibility: OCC is spec-driven (see CDISC ADaM PopPK IG v1.0 — "defined by the modeler"), and different studies use different rules. Examples:

  • Every dose starts a new occasion (NM-style): r -> r.EVID in (1, 4)
  • Every dose starts a new occasion (AMT-style): r -> r.AMT > 0
  • Washout events only (e.g. crossover reset): r -> r.EVID == 4
  • Visit-based occasions: r -> r.VISITNUM != r.PREV_VISITNUM (after pre-computing :PREV_VISITNUM with ShiftedArrays.lag)
  • Period-based occasions (crossover): r -> r.APERIOD != r.PREV_APERIOD
  • Formulation switch as anchor: r -> r.FORMULATION != r.PREV_FORMULATION
  • Only doses adjacent to a PK sample (NMproject-style): caller pre-computes :IS_ANCHOR via groupby + block membership, then passes r -> r.IS_ANCHOR
  • Trough-at-dose-time convention (sample at exact dose time belongs to next OCC): pre-compute :IS_ANCHOR as the first row at each (ID, time) whose time matches a dose time, then pass r -> r.IS_ANCHOR
  • Pre-dose-marker spec: r -> coalesce(r.PCTPTNUM, 0) ≈ -0.01
  • Missing-safe filter: r -> coalesce(r.EVID, 0) == 1

Keyword Arguments

  • group: subject grouping column(s); default [] (no grouping). Accepts a Symbol, AbstractString, or a Vector of either.
  • order: within-group ordering column(s) used for sorting; default [] (preserve within-group input order). Typical: :AFRLT or [:ADT, :EVID].
  • anchor_filter::Function: predicate row -> Bool selecting anchor rows.
  • occ_col: name for the derived column; default :OCC.
  • pre_first: value for rows before the first anchor; default 0. Pass missing to produce a Union{Missing,Int} OCC column.

Caller Notes

  • anchor_filter sees one row at a time. Rules needing lookahead/lookbehind (e.g. "dose adjacent to a sample") must be precomputed as a flag column upstream, then passed via anchor_filter = r -> r.IS_ANCHOR.
  • Two anchor-flagged rows at the same order value bump OCC twice. Deduplicate upstream if that's not intended.
  • anchor_filter errors abort the call. Use coalesce / ismissing if anchor columns can be missing.

Examples

Every dose starts a new occasion; pre-first samples get 0:

julia> df = DataFrame(USUBJID = ["A","A","A","A","A","B","B","B"],
                      EVID    = [0, 1, 0, 1, 0, 0, 1, 0]);

julia> occasion(df; group = :USUBJID, anchor_filter = r -> r.EVID == 1)
8×3 DataFrame
 Row │ USUBJID  EVID   OCC
     │ String   Int64  Int64
─────┼───────────────────────
   1 │ A            0      0
   2 │ A            1      1
   3 │ A            0      1
   4 │ A            1      2
   5 │ A            0      2
   6 │ B            0      0
   7 │ B            1      1
   8 │ B            0      1

Treat both regular dose (EVID=1) and washout (EVID=4) as anchors:

julia> df = DataFrame(USUBJID = "A", EVID = [1, 4, 0, 1, 0]);

julia> occasion(df; anchor_filter = r -> r.EVID in (1, 4))
5×3 DataFrame
 Row │ USUBJID  EVID   OCC
     │ String   Int64  Int64
─────┼───────────────────────
   1 │ A            1      1
   2 │ A            4      2
   3 │ A            0      2
   4 │ A            1      3
   5 │ A            0      3

Use pre_first = missing so pre-first rows are NA instead of 0:

julia> df = DataFrame(USUBJID = "A", EVID = [0, 0, 1, 0, 1, 0]);

julia> occasion(df; anchor_filter = r -> r.EVID == 1, pre_first = missing)
6×3 DataFrame
 Row │ USUBJID  EVID   OCC
     │ String   Int64  Int64?
─────┼─────────────────────────
   1 │ A            0  missing
   2 │ A            0  missing
   3 │ A            1        1
   4 │ A            0        1
   5 │ A            1        2
   6 │ A            0        2

Choose a custom output column name:

julia> df = DataFrame(USUBJID = "A", FLAG = [false, true, false, true]);

julia> occasion(df; anchor_filter = r -> r.FLAG, occ_col = :MY_OCC)
4×3 DataFrame
 Row │ USUBJID  FLAG   MY_OCC
     │ String   Bool   Int64
─────┼────────────────────────
   1 │ A        false       0
   2 │ A         true       1
   3 │ A        false       1
   4 │ A         true       2

Sample-bearing-block rule (matches R NMproject::make_OCC_every_dose): only dose blocks containing a sample advance OCC; unsampled dose blocks carry forward. Caller precomputes the anchor flag, then passes it in:

julia> df = DataFrame(USUBJID = "A", EVID = [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 4, 0]);

julia> anchored = @chain df begin
           groupby(:USUBJID)
           @transform :_PERIOD = cumsum(:EVID .!= 0)
           groupby([:USUBJID, :_PERIOD])
           @transform begin
               :_SAMPLED = any(:EVID .== 0)
               :_FIRST = (1:length(:_PERIOD)) .== 1
           end
           @rtransform :IS_ANCHOR = :_FIRST && :_SAMPLED && :_PERIOD > 0
       end;

julia> occasion(anchored; group = :USUBJID, anchor_filter = r -> r.IS_ANCHOR)
13×7 DataFrame
 Row │ USUBJID  EVID   _PERIOD  _SAMPLED  _FIRST  IS_ANCHOR  OCC
     │ String   Int64  Int64    Bool      Bool    Bool       Int64
─────┼─────────────────────────────────────────────────────────────
   1 │ A            0        0      true    true      false      0
   2 │ A            1        1      true    true       true      1
   3 │ A            0        1      true   false      false      1
   4 │ A            0        1      true   false      false      1
   5 │ A            1        2     false    true      false      1
   6 │ A            1        3     false    true      false      1
   7 │ A            1        4     false    true      false      1
   8 │ A            1        5      true    true       true      2
   9 │ A            0        5      true   false      false      2
  10 │ A            1        6      true    true       true      3
  11 │ A            0        6      true   false      false      3
  12 │ A            4        7      true    true       true      4
  13 │ A            0        7      true   false      false      4
ADaM.population_flag_summary — Method
population_flag_summary(df, flags; style = :simple, as_df = false, kwargs...) -> Union{SummaryTables.Table, DataFrame}

Population-flag cross-tabulation by treatment group, rendered as a SummaryTables.Table. Two layouts are available via style:

  • :simple (default) — flat table. Rows are population flags, columns are treatment groups, cells are n (pct%) (or another format — see format kwarg) of subjects in that population × treatment.
  • :table_one — SummaryTables.table_one categorical layout. Each flag is a bolded section header with one indented row per observed level (e.g. Y and N); the leftmost group column is Total; per-treatment columns share a top-level header spanning treatment.
  • :listingtable — SummaryTables.listingtable layout. Same per-flag rows and n (pct%) cells as :simple, but rendered with listingtable's treatment-spanning column header. Honours format, percent_digits, n_label, show_total, and show_n_row (computed from the same pre-aggregated DataFrame as :simple).

Accepts either subject-level (ADSL) or record-level (ADPPK / ADNCA) input. For record-level input, a subject is counted as in the population when any of their rows passes the inclusion test (configurable via included_flag_value).

The rendered Table bolds the per-treatment N header row via the internal style_total_rows helper. Pass as_df = true to skip rendering and get the underlying display DataFrame.

Required Columns

The input DataFrame must contain the columns referenced by subject and treatment (defaults :USUBJID, :TRT01A), plus every column listed in flags.

Positional Arguments

  • df::AbstractDataFrame — input DataFrame (ADSL- or ADPPK-shaped).
  • flags::AbstractVector — each element is a Symbol / AbstractString or column => <label>. When the bare-column form is used, the display label comes from the column's "label" metadata (DataFrames.colmetadata(df, col, "label")) if set, otherwise the stringified column name. The Pair form always overrides metadata; the label may be a String, Annotated, Cell, etc.

Keyword Arguments

  • style::Symbol = :simple — rendering layout. One of :simple (flat simple_table), :table_one (SummaryTables.table_one categorical-by-treatment), or :listingtable (SummaryTables.listingtable with a spanning treatment header). The kwargs format, percent_digits, n_label, and included_flag_value apply to :simple and :listingtable only — :table_one follows SummaryTables.table_one's defaults and shows one row per observed flag level.

  • as_df::Bool = false — when true, return the long-form DataFrame (Population, per-treatment, and total_label columns; first row is the N headcount) with raw Int subject counts as cells. Display-only options (format, percent_digits) are ignored — the DataFrame keeps numeric values so downstream code can derive its own ratios/percents. The shape is the same regardless of style.

  • treatment::Union{Symbol,AbstractString} = :TRT01A — subject-constant treatment column (defaults to actual treatment for period 1; pass :TRT01P for planned). The rendered Table uses this column's "label" metadata (DataFrames.colmetadata(df, treatment, "label")) for the spanning treatment header when set; falls back to the stringified column name.

  • subject::Union{Symbol,AbstractString} = :USUBJID — unique subject id.

  • included_flag_value = "Y" — flag value(s) indicating inclusion. Accepts a scalar, an AbstractVector / Set / Tuple of accepted values, or a unary predicate x -> Bool. Missing-safe for the non-function forms.

  • show_n_row::Bool = true — prepend a per-treatment "N" header row.

  • show_total::Bool = true — append a pooled column.

  • percent_digits::Int = 1 — decimals for the inclusion percentage.

  • population_name::Union{Symbol,AbstractString} = :Population — name of the row-label column.

  • treatment_order::Union{Nothing,AbstractVector} = nothing — explicit display order. Falls back to categorical levels (if applicable) or sorted unique values. Throws ArgumentError when the two sides disagree — either data has values not listed in treatment_order, or treatment_order lists values not present in the data.

  • n_label::AbstractString = "N" — label for the per-treatment N row.

  • total_label::AbstractString = "Pooled" — header for the pooled column.

  • format::Symbol = :n_pct — cell rendering convention. One of:

    • :n_pct → "3 (60.0%)" (default)
    • :n_only → "3"

    The per-treatment denominator already appears in the N header row, so n/N-style cell formats are intentionally not offered.

  • warn_inconsistent_treatment::Bool = true — emit a @warn when one or more subjects have ≥2 distinct non-missing values in treatment (the function then uses the first non-missing value for those subjects; combine with orderby to make the tie-break deterministic).

  • orderby = nothing — sort df by these column(s) before the per-subject reduction. Only matters for record-level input with inconsistent within-subject treatment (see warn_inconsistent_treatment): the arm assignment picks the first non-missing value in the given order, so the result is reproducible regardless of the caller's incoming row order. Accepts any column spec DataFrames.sort accepts (Symbol, String, Vector, or DataFrames.order(col; rev=true) for descending).

Example

Minimal ADSL-shaped fixture with one row per subject; as_df returns the underlying DataFrame, then a second call renders a Table with custom flag labels and the :n_only cell format:

julia> adsl = DataFrame(USUBJID = ["S1", "S2", "S3", "S4"], TRT01A = ["Placebo", "Placebo", "Active", "Active"], SAFFL = ["Y", "Y", "Y", "Y"], PKPOPFL = ["Y", "N", "Y", "Y"])
4×4 DataFrame
 Row │ USUBJID  TRT01A   SAFFL   PKPOPFL
     │ String   String   String  String
─────┼───────────────────────────────────
   1 │ S1       Placebo  Y       Y
   2 │ S2       Placebo  Y       N
   3 │ S3       Active   Y       Y
   4 │ S4       Active   Y       Y

julia> out = population_flag_summary(adsl, [:SAFFL, :PKPOPFL]; as_df = true);

julia> propertynames(out)
4-element Vector{Symbol}:
 :Population
 :Active
 :Placebo
 :Pooled

julia> out.Population
3-element Vector{Any}:
 "N"
 "SAFFL"
 "PKPOPFL"

julia> out.Active
3-element Vector{Int64}:
 2
 2
 2

julia> out.Pooled
3-element Vector{Int64}:
 4
 4
 3

julia> population_flag_summary(adsl, [:SAFFL => "Safety Population", :PKPOPFL => "PK Population"]; format = :n_only) isa Table
true
ADaM.profile_dataset — Method
profile_dataset(df::DataFrame; unique_threshold=20)
profile_dataset(folder::AbstractString, ext::AbstractString; missingstring=[], desc=false, unique_threshold=20)

Profile clinical dataset(s).

DataFrame form returns a NamedTuple (data, desc):

  • desc extends DataFrames.describe with pct_missing, nunique, and unique_values.
  • unique_values lists distinct non-missing values (insertion order) when nunique <= unique_threshold; missing above the cap.
  • For *DTC columns (CDISC SDTM convention), nunique = distinct stripped lengths and unique_values = one representative sample per length. Non-DTC datetime columns are not auto-detected.
  • For other file formats, load with the appropriate reader (e.g. ReadStatTables, CSV) and pass the resulting DataFrame.
  • For quick visual inspection use first(out.data, n).

Folder form (path + .xpt/.sas7bdat extension) returns a Dict keyed by file stem:

  • desc=false (default): values are raw DataFrames.
  • desc=true: values are the single-dataset NamedTuple form.
  • missingstring is forwarded to convert_to_missing per file.

Example

julia> df = DataFrame(SUBJID = ["S1", "S2", "S3"], SEX = ["M", "F", "M"], AGE = [25, 30, missing])
3×3 DataFrame
 Row │ SUBJID  SEX     AGE
     │ String  String  Int64?
─────┼─────────────────────────
   1 │ S1      M            25
   2 │ S2      F            30
   3 │ S3      M       missing

julia> out = profile_dataset(df); out.desc
3×11 DataFrame
 Row │ variable  mean       median     std            min      max      nmissing  pct_missing  eltype                 nunique  unique_values
     │ Symbol    Float64?   Float64?   Float64?       Int64?   Int64?   Int64     Float64      String                 Int64    Vector
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ SUBJID    missing    missing    missing        missing  missing         0       0.0     String                       3  ["S1", "S2", "S3"]
   2 │ SEX       missing    missing    missing        missing  missing         0       0.0     String                       2  ["M", "F"]
   3 │ AGE            27.5       27.5        3.53553       25       30         1      33.3333  Union{Missing, Int64}        2  [25, 30]
ADaM.record_inclusion_exclusion — Method
record_inclusion_exclusion(df; as_df = false, kwargs...) -> SummaryTables.Table

Inclusion / Exclusion accounting as a stratum × exclusion-category pivot, rendered with SummaryTables.table_one. Rows are the levels of the groupby column(s) (default [:STUDYID]); columns are an Overall total plus one column per distinct exclusion category.

Each cell is n (%) with the percentage computed column-relative — n / column_total, the denominator table_one uses — so each column sums to its header N. A cell therefore reads as this stratum's share of that category, not as that stratum's exclusion rate: a study contributing 1 of the 5 assay errors in the pooled data shows 1 (20%) regardless of whether that study has 10 records or 10000. Divide by the stratum's Overall count to recover a per-stratum rate.

A record is "Not excluded" when its EXCLF value is 0; any other value marks the record as excluded. Excluded records are categorised by their reason column (default :EXCLFCOM); excluded rows with a missing or blank reason fall into the missing_reason_label category. Category columns are ordered not_excluded_label, then the reasons alphabetically, then missing_reason_label — categories absent from the data are dropped rather than rendered as empty columns.

Pass as_df = true to skip rendering and get the input DataFrame enriched with a derived category column (named by category_label, default "Exclusion Category") instead of the rendered Table. The added column is an ordered CategoricalArray carrying that same level order.

Required Columns

The input DataFrame must contain the columns referenced by groupby and flag (defaults [:STUDYID], :EXCLF), both with no missing values — every record must carry a stratification value and an exclusion code. The reason column (default :EXCLFCOM) is required only when at least one record is excluded; an all-zero flag needs no reason column.

flag must be numeric (or a CategoricalArray over a numeric level type), since 0 versus non-0 is the entire include/exclude test. A character flag — "0"/"1" off an .xpt, or a CDISC "Y"/"N" — is rejected rather than silently compared unequal to 0 for every record. An inverted numeric convention, where 1 means include, cannot be detected and will produce a correctly-typed but wrong table.

Keyword Arguments

  • as_df::Bool = false — when true, return the input DataFrame with an added category column (named by category_label) instead of the rendered Table. Throws if a column of that name already exists.
  • groupby = [:STUDYID] — stratification column(s); accepts Symbol, AbstractString, or a Vector of either. Each unique combination becomes a row in the rendered Table. At least one column is required.
  • flag::Union{Symbol,AbstractString} = :EXCLF — numeric exclusion-code column. A value of 0 marks a record as Not excluded; any other value marks it excluded.
  • reason::Union{Symbol,AbstractString} = :EXCLFCOM — human-readable exclusion reason. Used only for excluded records (i.e. when EXCLF != 0).
  • not_excluded_label::AbstractString = "Not excluded" — column label used for the included-records category.
  • missing_reason_label::AbstractString = "(reason not specified)" — column label used for excluded records whose reason cell is missing or blank.
  • overall_label::AbstractString = "Overall" — column header for the per-stratum row total (the leftmost numeric column).
  • category_label::AbstractString = "Exclusion Category" — name of the derived category column. Used both as the column name added when as_df = true and as the group header spanning the category columns in the rendered Table.

Example

julia> adppk = DataFrame(STUDYID = ["A","A","A","A","B","B","B","C","C","C"], EXCLF = [0, 0, 1, 2, 0, 0, 6, 0, 1, 6], EXCLFCOM = [missing, missing, "Predose flag failure", "Below LLOQ", missing, missing, "Assay error", missing, "Predose flag failure", missing])
10×3 DataFrame
 Row │ STUDYID  EXCLF  EXCLFCOM
     │ String   Int64  String?
─────┼──────────────────────────────────────
   1 │ A            0  missing
   2 │ A            0  missing
   3 │ A            1  Predose flag failure
   4 │ A            2  Below LLOQ
   5 │ B            0  missing
   6 │ B            0  missing
   7 │ B            6  Assay error
   8 │ C            0  missing
   9 │ C            1  Predose flag failure
  10 │ C            6  missing

julia> record_inclusion_exclusion(adppk) isa Table
true

julia> out = record_inclusion_exclusion(adppk; as_df = true);

julia> String.(out[!, Symbol("Exclusion Category")])
10-element Vector{String}:
 "Not excluded"
 "Not excluded"
 "Predose flag failure"
 "Below LLOQ"
 "Not excluded"
 "Not excluded"
 "Assay error"
 "Not excluded"
 "Predose flag failure"
 "(reason not specified)"

julia> levels(out[!, Symbol("Exclusion Category")])
5-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "Not excluded"
 "Assay error"
 "Below LLOQ"
 "Predose flag failure"
 "(reason not specified)"
ADaM.round_columns — Method
round_columns(df::DataFrame, digit::Int64)

Rounds values across Float columns to specific number of digits.

Example

julia> df = DataFrame(Col1 = [1, 1.0023], Col2 = [3.14159, 2], Col3 = [3, 1.7], Col4 = [1, 4])
2×4 DataFrame
 Row │ Col1     Col2     Col3     Col4  
     │ Float64  Float64  Float64  Int64 
─────┼──────────────────────────────────
   1 │  1.0     3.14159      3.0      1
   2 │  1.0023  2.0          1.7      4


julia> round_columns(df, 2)
2×4 DataFrame
 Row │ Col1     Col2     Col3     Col4    
     │ Float64  Float64  Float64  Float64 
─────┼────────────────────────────────────
   1 │     1.0     3.14      3.0      1.0
   2 │     1.0     2.0       1.7      4.0
ADaM.samples_per_subject_df — Method
samples_per_subject_df(df; subject, groupby, evid, dv, blqfn, exclude_blq, count_col)

Count collected samples per subject in a CDISC ADPPK-shaped DataFrame.

Filters to evid == 0 AND !ismissing(dv), optionally drops blqfn == 1 rows, then counts rows per subject (or per subject × stratum when groupby is given). Subjects with zero collected samples are absent.

Keyword Arguments

  • subject: Subject identifier column (default :USUBJID).
  • groupby: Column(s) to stratify by — a Symbol, AbstractString, or AbstractVector of either. Default is empty (no stratification).
  • evid: Numeric event-identifier column (default :EVID).
  • dv: Numeric measurement column (default :DV). Pass nothing to count every evid == 0 row (scheduled-slot semantics).
  • blqfn: Numeric 0/1/missing BLQ flag column (default :BLQFN), read only when exclude_blq = true.
  • exclude_blq: Drop blqfn == 1 rows before counting (default false).
  • count_col: Output count column name (default :NOBS). Rejected if it collides with an input column.

Returns

One row per subject (or per subject × stratum), with columns [subject, groupby_cols..., count_col], sorted by subject then groupby.

Examples

julia> adppk = DataFrame(USUBJID = ["S1","S1","S1","S2","S2","S3"], EVID = [0,0,1,0,0,0], DV = [10.0,4.0,missing,12.0,5.0,8.0], BLQFN = [0,1,0,0,0,0])
6×4 DataFrame
 Row │ USUBJID  EVID   DV         BLQFN
     │ String   Int64  Float64?   Int64
─────┼──────────────────────────────────
   1 │ S1           0       10.0      0
   2 │ S1           0        4.0      1
   3 │ S1           1  missing        0
   4 │ S2           0       12.0      0
   5 │ S2           0        5.0      0
   6 │ S3           0        8.0      0

julia> samples_per_subject_df(adppk)
3×2 DataFrame
 Row │ USUBJID  NOBS
     │ String   Int64
─────┼────────────────
   1 │ S1           2
   2 │ S2           2
   3 │ S3           1

julia> samples_per_subject_df(adppk; exclude_blq = true)
3×2 DataFrame
 Row │ USUBJID  NOBS
     │ String   Int64
─────┼────────────────
   1 │ S1           1
   2 │ S2           2
   3 │ S3           1

Passing groupby yields one row per subject × stratum:

julia> crossover = DataFrame(USUBJID = ["S1","S1","S1","S1","S2","S2"], TRTA = ["A","A","B","B","A","A"], EVID = [0,0,0,0,0,0], DV = [10.0,4.0,9.0,3.0,12.0,5.0])
6×4 DataFrame
 Row │ USUBJID  TRTA    EVID   DV
     │ String   String  Int64  Float64
─────┼─────────────────────────────────
   1 │ S1       A           0     10.0
   2 │ S1       A           0      4.0
   3 │ S1       B           0      9.0
   4 │ S1       B           0      3.0
   5 │ S2       A           0     12.0
   6 │ S2       A           0      5.0

julia> samples_per_subject_df(crossover; groupby = :TRTA)
3×3 DataFrame
 Row │ USUBJID  TRTA    NOBS
     │ String   String  Int64
─────┼────────────────────────
   1 │ S1       A           2
   2 │ S1       B           2
   3 │ S2       A           2

julia> samples_per_subject_df(crossover)
2×2 DataFrame
 Row │ USUBJID  NOBS
     │ String   Int64
─────┼────────────────
   1 │ S1           4
   2 │ S2           2

julia> samples_per_subject_df(crossover; groupby = :TRTA, count_col = :N_SAMPLES)
3×3 DataFrame
 Row │ USUBJID  TRTA    N_SAMPLES
     │ String   String  Int64
─────┼────────────────────────────
   1 │ S1       A               2
   2 │ S1       B               2
   3 │ S2       A               2

Plotting

With a within-subject groupby (:TRTA, :CMT), the same subject appears in several rows; give each stratum its own axis rather than pooling:

using DataFrames, CairoMakie

adppk = DataFrame(
    USUBJID = [fill("S1", 5); fill("S2", 4); fill("S3", 3); fill("S4", 4)],
    TRTA = ["A", "A", "A", "B", "B", "A", "A", "B", "B", "A", "B", "B", "A", "A", "A", "B"],
    EVID = fill(0, 16),
    DV = collect(1.0:16.0),
)

out = samples_per_subject_df(adppk; groupby = :TRTA)
fig = Figure()
for (i, sub) in enumerate(DataFrames.groupby(out, :TRTA))
    ax = Axis(fig[1, i]; title = string(first(sub.TRTA)),
              xlabel = "samples per subject", ylabel = "subjects")
    hist!(ax, sub.NOBS)
end
ADaM.sampling_time_df — Method
sampling_time_df(df; time, time_unit, evid, blqfl, exclude_blq)

Prepare a sampling-time histogram DataFrame from a CDISC ADPPK-shaped input.

Filters to evid == 0 AND !ismissing(time), optionally drops blqfl == 1 rows, then appends a derived time column holding the source time in the requested unit. All other input columns are preserved so the caller can map them to color, faceting, etc.

The derived column name is the source column with U appended — :APRLTU by default, :AFRLTU for time = :AFRLT, :NFRLTU for time = :NFRLT, and so on. The U suffix distinguishes the unit-converted output from the raw source column that also travels through.

Keyword Arguments

  • time: Source time column. Must be one of :APRLT (default; actual time from previous dose), :AFRLT (actual from first dose), :NPRLT (nominal from previous dose), or :NFRLT (nominal from first dose). Any other value is rejected.
  • time_unit: :h (alias :hr) or :day (alias :d), matching the DynamicQuantities.Units spellings. Default is :h; pass :day to divide by 24.
  • evid: Numeric event-identifier column (default :EVID). Rows with evid != 0 are dropped.
  • blqfl: Numeric 0/1/missing BLQ flag column (default :BLQFN), read only when exclude_blq = true.
  • exclude_blq: Drop blqfl == 1 rows before counting (default true — the popPK convention is to histogram measurable samples only).

Returns

The input DataFrame filtered to collected samples, with an added <source>U column carrying the chosen time in the requested unit.

Example

julia> adppk = DataFrame(USUBJID = ["S1","S1","S1","S2","S2"], EVID = [0,1,0,0,0], APRLT = [1.0,0.0,2.0,3.0,4.0], AFRLT = [1.0,0.0,2.0,24.0,28.0], BLQFN = [0,0,1,0,0])
5×5 DataFrame
 Row │ USUBJID  EVID   APRLT    AFRLT    BLQFN
     │ String   Int64  Float64  Float64  Int64
─────┼─────────────────────────────────────────
   1 │ S1           0      1.0      1.0      0
   2 │ S1           1      0.0      0.0      0
   3 │ S1           0      2.0      2.0      1
   4 │ S2           0      3.0     24.0      0
   5 │ S2           0      4.0     28.0      0

julia> sampling_time_df(adppk)
3×6 DataFrame
 Row │ USUBJID  EVID   APRLT    AFRLT    BLQFN  APRLTU
     │ String   Int64  Float64  Float64  Int64  Float64
─────┼──────────────────────────────────────────────────
   1 │ S1           0      1.0      1.0      0      1.0
   2 │ S2           0      3.0     24.0      0      3.0
   3 │ S2           0      4.0     28.0      0      4.0
ADaM.set_exclusion — Method
set_exclusion(df, excl_func, comment; group, order)
  • Helps to set exclusions to the dataframe rows based on input excl_func and group variables.
  • The exclusion comment can be passed as a String or from a DataFrame column(Symbol).

Keyword Arguments

  • excl_func: Function condition based on which rows of a dataframe are flagged as excluded.
  • group: Grouping variable(s) for exclusion logic.
  • order: Ordering variable(s) for sorting within groups.

Example

julia> df = DataFrame(ID = [1,1,1,2,2,2,3,3,3], SEQ = [1,2,3,1,2,3,1,2,3], EVID = [1,1,1,0,0,0,0,1,0], STAT = ["NA","NA", "NA", missing, missing, missing, missing, "NA", missing], CONC = [missing,missing,missing,20,40,60,30,missing,70])
9×5 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC
     │ Int64  Int64  Int64  String?  Int64?
─────┼───────────────────────────────────────
   1 │     1      1      1  NA       missing
   2 │     1      2      1  NA       missing
   3 │     1      3      1  NA       missing
   4 │     2      1      0  missing       20
   5 │     2      2      0  missing       40
   6 │     2      3      0  missing       60
   7 │     3      1      0  missing       30
   8 │     3      2      1  NA       missing
   9 │     3      3      0  missing       70

julia> set_exclusion(df, "All missing conc subs", excl_func = group -> all(ismissing, group.CONC), group = :ID)
9×7 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC     EXCLFCOM               EXCLF 
     │ Int64  Int64  Int64  String?  Int64?   String?                Int64 
─────┼─────────────────────────────────────────────────────────────────────
   1 │     1      1      1  NA       missing  All missing conc subs      1
   2 │     1      2      1  NA       missing  All missing conc subs      1
   3 │     1      3      1  NA       missing  All missing conc subs      1
   4 │     2      1      0  missing       20  missing                    0
   5 │     2      2      0  missing       40  missing                    0
   6 │     2      3      0  missing       60  missing                    0
   7 │     3      1      0  missing       30  missing                    0
   8 │     3      2      1  NA       missing  missing                    0
   9 │     3      3      0  missing       70  missing                    0

julia> set_exclusion(df, "No dose subs", excl_func = group -> all(iszero, group.EVID), group = "ID") # Exclusion 2: Subjects with no dosing data
9×7 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC     EXCLFCOM      EXCLF 
     │ Int64  Int64  Int64  String?  Int64?   String?       Int64 
─────┼────────────────────────────────────────────────────────────
   1 │     1      1      1  NA       missing  missing           0
   2 │     1      2      1  NA       missing  missing           0
   3 │     1      3      1  NA       missing  missing           0
   4 │     2      1      0  missing       20  No dose subs      1
   5 │     2      2      0  missing       40  No dose subs      1
   6 │     2      3      0  missing       60  No dose subs      1
   7 │     3      1      0  missing       30  missing           0
   8 │     3      2      1  NA       missing  missing           0
   9 │     3      3      0  missing       70  missing           0

julia> set_exclusion(df, "No conc subs", excl_func = group -> all(isone, group.EVID), group = [:ID]) # Exclusion 3: Subjects with no conc. data
9×7 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC     EXCLFCOM      EXCLF 
     │ Int64  Int64  Int64  String?  Int64?   String?       Int64 
─────┼────────────────────────────────────────────────────────────
   1 │     1      1      1  NA       missing  No conc subs      1
   2 │     1      2      1  NA       missing  No conc subs      1
   3 │     1      3      1  NA       missing  No conc subs      1
   4 │     2      1      0  missing       20  missing           0
   5 │     2      2      0  missing       40  missing           0
   6 │     2      3      0  missing       60  missing           0
   7 │     3      1      0  missing       30  missing           0
   8 │     3      2      1  NA       missing  missing           0
   9 │     3      3      0  missing       70  missing           0

julia> set_exclusion(df, :STAT, excl_func = group -> all(ismissing, group.CONC), group = [:ID])
9×7 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC     EXCLFCOM  EXCLF 
     │ Int64  Int64  Int64  String?  Int64?   String?   Int64 
─────┼────────────────────────────────────────────────────────
   1 │     1      1      1  NA       missing  NA            1
   2 │     1      2      1  NA       missing  NA            1
   3 │     1      3      1  NA       missing  NA            1
   4 │     2      1      0  missing       20  missing       0
   5 │     2      2      0  missing       40  missing       0
   6 │     2      3      0  missing       60  missing       0
   7 │     3      1      0  missing       30  missing       0
   8 │     3      2      1  NA       missing  missing       0
   9 │     3      3      0  missing       70  missing       0

julia> set_exclusion(df, :STAT, excl_func = group -> all(ismissing, group.CONC), group = [:ID, :SEQ])
9×7 DataFrame
 Row │ ID     SEQ    EVID   STAT     CONC     EXCLFCOM  EXCLF 
     │ Int64  Int64  Int64  String?  Int64?   String?   Int64 
─────┼────────────────────────────────────────────────────────
   1 │     1      1      1  NA       missing  NA            1
   2 │     1      2      1  NA       missing  NA            1
   3 │     1      3      1  NA       missing  NA            1
   4 │     2      1      0  missing       20  missing       0
   5 │     2      2      0  missing       40  missing       0
   6 │     2      3      0  missing       60  missing       0
   7 │     3      1      0  missing       30  missing       0
   8 │     3      2      1  NA       missing  NA            1
   9 │     3      3      0  missing       70  missing       0
ADaM.subject_inclusion_exclusion — Method
subject_inclusion_exclusion(df; as_df = false, kwargs...) -> Union{SummaryTables.Table, DataFrame}

Subject-level inclusion / exclusion accounting as a per-group count of treated, excluded, and included subjects, rendered as a SummaryTables.Table. Each row is a group (default :STUDYID); columns are:

  • Treated — total subjects in the group.
  • Excluded (N) — subjects whose every record is excluded.
  • Included Study Participants (N, %) — subjects with at least one included record, formatted as N (pct%) where pct = N / Treated * 100.

A subject counts as Included when any of their records has its exclusion flag equal to 0; otherwise they're Excluded. This is the subject-level companion to the record-level inclusion/exclusion accounting, which categorises individual records rather than subjects.

Pass as_df = true to skip rendering and get the underlying per-group DataFrame. Both paths carry the same values: Treated and Excluded (N) are Int, and the included column is the formatted N (pct%) string, so the (N, %) header means the same thing in either.

Every record counts, including dose records

No filtering is applied to df — every record is fed to the flag == 0 rule as-is. set_exclusion seeds EXCLF = 0 on all rows and only flags the records that fail a criterion, so a dose record (EVID == 1) carries EXCLF == 0 and on its own makes a subject count as Included, even when every one of their observations is excluded. Filter df down to the records the population is defined on (typically EVID == 0) before calling. Unlike bloq_summary and conc_summary, this function takes no evid kwarg and does not drop dose records for you.

No pooled row is emitted unless show_overall = true; the pooled row counts each subject once across all groups.

Required Columns

The input DataFrame must contain the columns referenced by groupby, id, and flag (defaults :STUDYID, :USUBJID, :EXCLF). None of these columns may contain missing values, and flag must be numeric.

Keyword Arguments

  • as_df::Bool = false — when true, return the per-group DataFrame instead of the rendered Table. Same values either way.
  • groupby = :STUDYID — grouping column(s); accepts a Symbol, an AbstractString, or a vector of either. Each unique combination becomes a row in the rendered Table.
  • id::Union{Symbol,AbstractString} = :USUBJID — unique subject id column. The (group, id) pair is what defines a counted subject.
  • flag::Union{Symbol,AbstractString} = :EXCLF — exclusion flag column. A record with flag == 0 counts as included; any non-zero value counts as excluded.
  • treated_label::AbstractString = "Treated" — column label for the per-group subject headcount.
  • excluded_label::AbstractString = "Excluded (N)" — column label for the per-group count of subjects whose every record is excluded.
  • included_label::AbstractString = "Included Study Participants (N, %)" — column label for the per-group count of subjects with ≥1 included record (formatted as N (pct%)).
  • pct_digits::Int = 1 — decimals for the included percentage. Must be non-negative; 0 renders 33%, not 33.0%.
  • show_overall::Bool = false — when true, append a pooled row across all groups, bolded in the rendered Table.
  • overall_label::AbstractString = "All" — label for the pooled row, placed in every groupby column.

Example

julia> adppk = DataFrame(USUBJID = ["S1","S1","S1","S2","S2","S3","S4","S4","S5","S5"], STUDYID = ["A","A","A","A","A","A","B","B","B","B"], EXCLF = [0, 0, 1, 1, 2, 0, 0, 0, 1, 2])
10×3 DataFrame
 Row │ USUBJID  STUDYID  EXCLF
     │ String   String   Int64
─────┼─────────────────────────
   1 │ S1       A            0
   2 │ S1       A            0
   3 │ S1       A            1
   4 │ S2       A            1
   5 │ S2       A            2
   6 │ S3       A            0
   7 │ S4       B            0
   8 │ S4       B            0
   9 │ S5       B            1
  10 │ S5       B            2

julia> subject_inclusion_exclusion(adppk) isa Table
true

julia> subject_inclusion_exclusion(adppk; as_df = true)
2×4 DataFrame
 Row │ STUDYID  Treated  Excluded (N)  Included Study Participants (N, %)
     │ String   Int64    Int64         String
─────┼────────────────────────────────────────────────────────────────────
   1 │ A              3             1  2 (66.7%)
   2 │ B              2             1  1 (50.0%)

julia> subject_inclusion_exclusion(adppk; as_df = true, show_overall = true)
3×4 DataFrame
 Row │ STUDYID  Treated  Excluded (N)  Included Study Participants (N, %)
     │ String   Int64    Int64         String
─────┼────────────────────────────────────────────────────────────────────
   1 │ A              3             1  2 (66.7%)
   2 │ B              2             1  1 (50.0%)
   3 │ All            5             2  3 (60.0%)
ADaM.update_spec — Method
update_spec(path;
    metadata = DataFrame(),
    flags = DataFrame(),
    imputations = DataFrame(),
    append_spec = DataFrame(),
    spec_overrides = DataFrame(),
    drop_variables = String[],
)

Apply targeted edits to an existing four-sheet ADaM specification workbook at path and write it back to the same file. Preserves reviewer-edited content: the template, xpt profile, and codelists are not consulted. Non-standard columns beyond the required contract survive the round-trip.

Loads the four sheets (Metadata, Spec, Data Issue Flags, Imputations), validates their columns, applies the requested edits, revalidates the Spec sheet, and rewrites the workbook. The write is atomic — a .tmp sibling receives the new content and replaces path via mv; a mid-write failure leaves the original file untouched.

Whole-sheet replacements — pass a non-empty DataFrame to swap; an empty DataFrame keeps the sheet as-loaded:

  • metadata::DataFrame — replaces the Metadata sheet.
  • flags::DataFrame — replaces the Data Issue Flags sheet.
  • imputations::DataFrame — replaces the Imputations sheet.

Spec-sheet edits — applied in order to the loaded Spec:

  1. drop_variables::Vector{String} — remove rows whose Variable appears in the list. Names not in the loaded Spec throw ArgumentError.
  2. spec_overrides::DataFrame — long-format cell patches with columns Variable, Field, Value. Each row overwrites one cell. Unknown Variable or Field throws.
  3. append_spec::DataFrame — appended after the survivors. vcat uses cols = :union.

Sheets and required columns

Same as write_spec:

  • Metadata: Dataset, Label, Structure, Keys, Location.
  • Spec: Variable, Label, Units, Type, Length, Rounding, MissingValue, Codelist, Origin, Predecessor, Derivation, Status, Notes. Extra columns are preserved.
  • Data Issue Flags: FlagNumber, FLAGCOM, EXCLN, Scope, Notes.
  • Imputations: Topic, Scope, Algorithm.

Returns the absolute path of the written .xlsx file.

Examples

using DataFrames

overrides = DataFrame(
    Variable = ["AVAL", "AVAL"],
    Field    = ["Units", "Rounding"],
    Value    = ["ng/mL", "3 sig figs"],
)

update_spec(
    "adpc_spec.xlsx";
    spec_overrides = overrides,
    drop_variables = ["RACE"],
)
ADaM.validate_spec — Method
validate_spec(dataset_path, spec_path;
    sheet = "Spec",
    unique_threshold = 20,
)

Compare a derived .xpt dataset to a spec workbook and return a long-form DataFrame with one row per divergence. Columns:

  • :Variable — variable name (String)
  • :Column — spec column being checked (:Label, :Type, :Length, :Codelist), or :Existence for missing / orphan variables
  • :SpecValue — value in the spec
  • :DatasetValue — value observed in the dataset
  • :Severity — :mismatch (variable in both, value differs), :missing_in_spec (dataset has it, spec doesn't), :orphan_in_spec (spec has it, dataset doesn't)

Pure check — does not edit either file. Returns an empty frame when the spec and dataset agree.

The dataset is authoritative for Label (SAS label attribute), Type ("Char" for READSTAT_TYPE_STRING, "Num" otherwise), Length (SAS storage_width), and Codelist (unique non-missing values joined with "; ", for string or categorical columns with nunique ≤ unique_threshold). Length is compared numerically; non-parseable spec Length cells and non-integer dataset widths are silently skipped. Codelist is compared as a set so order doesn't matter.

Throws

  • ArgumentError if dataset_path or spec_path does not exist.
  • ArgumentError if sheet is not a sheet in the workbook.
  • ArgumentError if the Spec sheet has no Variable column, or if any row in it has a missing Variable.
ADaM.write_spec — Method
write_spec(path, template_path, xpt_folder;
    metadata = DataFrame(),
    flags = DataFrame(),
    imputations = DataFrame(),
    append_spec = DataFrame(),
    spec_overrides = DataFrame(),
    drop_variables = String[],
)

Build a four-sheet ADaM specification workbook at path. Overwrites path if it already exists.

Reads the variable template at template_path (a CSV with columns Section, Variable, Label, Units, Type, Origin, Predecessor, DefaultDerivation, Core, SourceDomain), profiles every .xpt under xpt_folder via profile_dataset, then:

  1. drops template rows whose SourceDomain lists any domain not present in the profiled folder (logs each drop via @info);
  2. drops template rows whose Variable appears in drop_variables (project-specific exclusions, e.g. this study doesn't collect race);
  3. maps each surviving template row to a Spec-sheet row (translates DefaultDerivation → Derivation, seeds Status = "draft", translates Core into Notes via Req → "Required", Cond → "Conditional", Perm → "Permissible"; anything else → "");
  4. pulls codelist values from each profile's unique_values into the Codelist column for rows whose Predecessor names a single SDTM column (e.g. DM.SEX);
  5. applies spec_overrides (a long DataFrame with columns Variable, Field, Value) — each row overrides one cell of one variable;
  6. appends append_spec (project-specific variables added by the skill from MAP/SAP context);
  7. falls back to a single TODO row on metadata / flags / imputations when the caller passes empty DataFrames.

Returns the absolute path of the written .xlsx file.

Sheets

  • Metadata — flat table identifying the dataset. Required columns: Dataset, Label, Structure, Keys, Location.
  • Spec — one row per ADaM variable, built from the template + profiles. Columns: Variable, Label, Units, Type, Length, Rounding, MissingValue, Codelist, Origin, Predecessor, Derivation, Status, Notes.
  • Data Issue Flags — FLAGCOM catalog from flags. Required columns: FlagNumber, FLAGCOM, EXCLN, Scope, Notes.
  • Imputations — multi-step algorithms from imputations. Required columns: Topic, Scope, Algorithm.

Arguments

  • path::AbstractString — output xlsx path. Overwrites if it exists.
  • template_path::AbstractString — CSV variable template.
  • xpt_folder::AbstractString — folder containing source .xpt files.

Optional keyword arguments

  • metadata::DataFrame — Metadata sheet rows (typically one per dataset). Empty → single TODO.
  • flags::DataFrame — Data Issue Flags rows. Empty → single TODO.
  • imputations::DataFrame — Imputations rows. Empty → single TODO.
  • append_spec::DataFrame — appended to Spec after the template. Columns should match the Spec sheet (Variable, Label, Units, Type, Length, Rounding, MissingValue, Codelist, Origin, Predecessor, Derivation, Status, Notes); vcat uses cols = :union, so missing columns are padded rather than erroring.
  • spec_overrides::DataFrame — long-format cell patches with columns Variable, Field, Value.
  • drop_variables::Vector{String} — variable names to drop from the template before Spec-row generation. Applied after the SourceDomain filter and before overrides.

Validation

Strict, fail-fast. Missing required column on any sheet → ArgumentError naming the missing column. Column types are not enforced.

Examples

using DataFrames

metadata = DataFrame(
    Dataset = ["ADPC"],
    Label = ["PK Concentrations"],
    Structure = ["One record per timepoint per subject"],
    Keys = ["STUDYID, USUBJID, PCTPTNUM"],
    Location = ["adpc.xpt"],
)

flags = DataFrame(
    FlagNumber = [1, 2],
    FLAGCOM = ["PRE-DOSE BQL", "POST-DOSE BQL"],
    EXCLN = [0, 1],
    Scope = ["ADPC", "ADPC"],
    Notes = ["Set to 0", "Exclude from NCA"],
)

imputations = DataFrame(
    Topic = ["Missing PCDTC", "BLQ handling"],
    Scope = ["ADPC", "ADPC"],
    Algorithm = [
        "If PCDTC missing, carry EXSTDTC forward within PCTPTNUM",
        "Pre-dose BLQ → 0; post-dose BLQ → LLOQ/2 for first record, else NA",
    ],
)

spec_overrides = DataFrame(
    Variable = ["AVAL", "AVAL"],
    Field    = ["Units", "Rounding"],
    Value    = ["ng/mL", "3 sig figs"],
)

write_spec(
    "adpc_spec.xlsx",
    "template.csv",
    "xpt/";
    metadata,
    flags,
    imputations,
    spec_overrides,
)