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.TableBuild 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.
style | rendered layout |
|---|---|
:table_one | SummaryTables.table_one pivot of :BLQC over [groupby..., :<time>C] |
:simple_table | SummaryTables.simple_table of the per-(group × time) summary, with Pooled/All rows bolded |
:summarytable | SummaryTables.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— whentrue, return the underlying DataFrame (display-ready per-(group × time) summary for:simple_table, filtered observations enriched with:BLQCand:<time>Ccategoricals for:table_one, or filtered observations enriched with:<time>Cand the raw numericblqflcolumn preserved for:summarytable) instead of the renderedTable.groupby = []— stratification column(s); acceptsSymbol,AbstractString, or aVectorof either.blqfl::Union{Symbol,AbstractString} = :BLQFN— numeric BLQ flag column (0/1;missingcounts as non-BLQ). Callers with a character flag column (e.g.BLQFL = "Y"/"N") should deriveBLQFN = BLQFL .== "Y"first.time::Union{Symbol,AbstractString} = :NPRLT— nominal time column (default: time relative to previous dose).evid::Union{Symbol,AbstractString} = :EVID— rows withevid != 0are 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-groupPooledrow.show_overall::Bool = true— when grouped, append a cross-groupAllrow.placeholder_missing::Union{Missing,AbstractString} = missing— value used in:NBLQandSymbol("BLQ [%]")body cells when the BLQ count is zero. Pass a string (e.g."-"or"NC") to render a visible placeholder instead ofmissing.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:BLQCcolumn (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
truePass 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.0ADaM.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 = :WTBLheight = :HTBLweight_unit = :WTBLUheight_unit = :HTBLUcol = :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⁻² kgADaM.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-duboishaycockgehan-georgeboydfujimototakahira
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 = :HTBLweight = :WTBLheight_unit = :HTBLUweight_unit = :WTBLUcol = :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
DataFramefor 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-
Stringcategorical columns (Symbol, Bool, Integer, CategoricalValue) toStringfor stacking, and aggregates to per-(covariate, category, [stratum])subject counts. - Returns a long-format
DataFramewith one row per(covariate, category[, stratum])combination and subject counts in thecount_colcolumn. Whengroupbyis 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 of0so a dodged barplot renders a zero-height bar rather than dropping the category from one stratum. See the "Plotting" section below — becausevalueis a plainAbstractStringcolumn (not a per-covariateCategoricalArray), a naivemapping(: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 byvariablefirst. - Continuous (
Float) covariates are rejected.
Required Columns
- The
subjectcolumn (default:USUBJID). - Every column referenced in
covariate_cols. - The
groupbycolumn, 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 bareSymbol/AbstractString(column name) or aSymbol => label/AbstractString => labelpair (column name with display label).subject: Unique subject identifier column (default:USUBJID).groupby: One or more columns to stratify counts by. Accepts aSymbol, anAbstractString, or anAbstractVectorof either (e.g.:STUDYIDor[:STUDYID, :TRTA]). Defaultnothing.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 (defaulttrue). Whenfalse,missingbecomes 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 3Plotting
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)
endADaM.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 0ADaM.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 - 1denominator).: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— onlyNis reported; every other stat ismissing.n < 2—SD,CV,GeoCV,GSD,Q1,Q3aremissing(no spread).- Any non-positive value in the cell —
GeoMean,GeoCV,GSDaremissing(log undefined).
Keyword Arguments
style::Symbol = :simple— rendering layout::simple— flat table (one row per group × time), rendered withSummaryTables.simple_table. Backwards-compatible default.:table_one— group hierarchy across the top, stats stacked vertically on the left, rendered withSummaryTables.table_one.:summarytable— group hierarchy on the left, time across the top, full stat panel per cell, rendered withSummaryTables.summarytable.
as_df::Bool = false— whentrue, return the underlying long-form DataFrame (one row per group × time, one column per requested stat) regardless ofstyle.groupby = [:TRTA]— stratification column(s); acceptsSymbol,AbstractString, or aVectorof either. Nested outsidetimein the group hierarchy. Default stratifies by actual treatment. Pass an empty vector to aggregate across all subjects;:summarytablerequires at least one column here since it usesgroupbyas the row axis. For a custom sort order, pre-cast the relevant column to aCategoricalArraywithordered = true.time::Union{Symbol,AbstractString} = :NFRLT— nominal time column.conc::Union{Symbol,AbstractString} = :DV— concentration column.evid::Union{Symbol,AbstractString} = :EVID— rows withEVID != 0are 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;:Nis alwaysIntand never rounded.time_as_string::Bool = false— display-only. Whentrue, the time column in the rendered Table is converted to aStringformatted to exactlytime_digitsdecimal places. Use this for rendering withSummaryTables.simple_table, which otherwise strips trailing zeros and makes consecutive times like239.77 / 240.0 / 240.5look identical ("240"). Sorting happens on the numeric values before conversion. Ignored whenas_df = true.missing_placeholder = nothing— display-only. When set (e.g."NC"or"-"), everymissingcell in the rendered Table is replaced with this string viaSummaryTables.ReplaceMissing. Ignored whenas_df = true; the returned DataFrame keepsmissingfor downstream numeric processing.table_kwargs::NamedTuple = NamedTuple()— extra kwargs forwarded to theSummaryTablesrenderer forstyle(e.g.footnotes,show_n). Merged after internal defaults so entries here win — e.g.(show_n = true,)withstyle = :table_onere-enables the N column. Ignored whenas_df = true. Invalid kwargs raiseMethodError, thoughsimple_table/summarytablesilently accept anyCellTablefield (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.3Default — 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
trueADaM.continuous_covariate_distribution_df — Method
continuous_covariate_distribution_df(df; covariate_cols, subject, groupby, variable, value, drop_missing)- Prepares a
DataFramefor 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
variablecolumn is an orderedCategoricalArraywhose 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
subjectcolumn (default:USUBJID). - Every column referenced in
covariate_cols. - The
groupbycolumn, 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 bareSymbol/AbstractString(column name) or aSymbol => label/AbstractString => labelpair (column name with display label).CategoricalArraycovariates whose level type is<: Real(e.g.CategoricalArray{Int}from.xptround-trips) are accepted.subject: Unique subject identifier column (default:USUBJID).groupby: One or more categorical columns preserved alongside the stacked covariates. Accepts aSymbol, anAbstractString, or anAbstractVectorof either (e.g.:STUDYIDor[:STUDYID, :TRTA]). Not renamed. Defaultnothing.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 (defaulttrue).
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.5Plotting
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)
endADaM.convert_to_missing — Method
convert_to_missing(df::DataFrame, NaStr::Vector)- Converts values to
missing. The values that need to be converted tomissingcan be passed as aVector. - 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 4ADaM.covariate_pairplot_df — Method
covariate_pairplot_df(df; covariate_cols, subject, groupby, drop_missing)- Prepares a
DataFramefor 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 => Stringpairs. - Returns a wide-format
DataFrame(one row per unique subject, one column per covariate, plus anygroupbycolumns carried through). Ready to feed into amapping(color=:col)-style API (AlgebraOfGraphics, Vega, etc.); when the downstream isPairPlots.jl(which has nocolor=:colmapping), split bygroupbyfirst — see the second example below.
Required Columns
- The
subjectcolumn (default:USUBJID). - Every column referenced in
covariate_cols. - The
groupbycolumn, when specified.
Keyword Arguments
covariate_cols: Continuous covariates to include. Each element is a bareSymbol(e.g.:AGE) or aSymbol => Stringpair (e.g.:AGE => "Age [yr]"). At least 2 covariates are required.CategoricalArraycovariates whose level type is<: Real(e.g.CategoricalArray{Int}from.xptround-trips) are accepted; truly categorical covariates go viagroupby.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 aSymbol, anAbstractString, or anAbstractVectorof either (e.g.:STUDYIDor[:STUDYID, :TRTA]). Not renamed. Defaultnothing.drop_missing: Drop subjects with any missing covariate value (defaulttrue).
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.5Splitting 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 = :AGEweight = :WTBLcreat = :CREATBLsex = :SEXage_unit = :AGEUweight_unit = :WTBLUcreat_unit = :CREATBLUcol = :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⁻¹ mLADaM.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_indexbody_surface_areacreatinine_clearanceest_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 3ADaM.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(requiresraceparameter)
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 = :AGEsex = :SEXcreat = :CREATBLcyst = :CYSTBLrace = :RACEage_unit = :AGEUcreat_unit = :CREATBLUcyst_unit = :CYSTBLUcol = :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 periodend_dtm: Column name containing the end datetime for the dosing periodtrt_start_dtm: Column name containing the first analyte dose datetime (used as reference forNFRLTcalculation)dose_freq: Column name containing the dosing frequency
Default Columns
The following default column names are used for DataFrame input:
start_dtm = :ASTDTMend_dtm = :AENDTMtrt_start_dtm = :FANLDTMdose_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.0Example 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 omittedExample 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.0Example 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.0ADaM.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.0ADaM.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 toreference: 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 ofsource => destinationpairs 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:firstor:lastmatch 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-20ADaM.make_dose_interruption — Method
make_dose_interruption(df::DataFrame)- Creates dose interruption columns
INTRFL(Interruption Flag) andINTRDUR(Interruption Duration) on theEX(dose) dataset where doses are recorded as intervals. - If there is any gap(more than a day) between end date
AENDTof one dosing interval and start dateASTDTof the next interval within a subject, the interruption will be recorded in rows. If there is no interruption, these columns will bemissing.
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 missingADaM.make_dtm — Method
make_dtm(df::DataFrame, col; prefix, fmt, on_error)- Derives a
DateTimecolumn from aDateTime-convertibleString/Date/DateTimecolumn in aDataFrame. - The derived column is named
<prefix>DTM(default prefix"A"). fmtcontrols parsing ofStringcolumns::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).DateFormator formatString— single fixed format.Vector{DateFormat}— try each in order; first match wins.
on_errorcontrols behavior on unparseable strings::error(default) — throw, today's behavior.:missing— returnmissingfor 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:00Input 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:45For 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:00ADaM.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.,:hfor hours,:minfor 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 hDate 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 dayDateTime 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 dayADaM.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 containATPTN)ex: EX DataFramedomain: The output DataFrame (PCorEX)visitdy_col: Column name forVISITDY(Symbol or String, default = :VISITDY)atptn_col: Column name forATPTN(Symbol or String, default = :ATPTN)
Output
- Returns a DataFrame (PC or EX) with a new
NFRLTcolumn representing nominal time in hours.
Notes
- For PC:
NFRLT = (VISITDY - 1) * 24 + ATPTN - For EX:
NFRLT = (VISITDY - 1) * 24 - If
VISITDYis missing in either dataset, it is joined from the other dataset using common keys (excludingATPTNfor PC). - Throws an error if required columns are
missingor 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 72ADaM.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 0ADaM.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 todf2: DataFrame to leftjoin columns from
Keyword Arguments
keep: Vector ofsource => destinationpairs 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,makeuniqueetc.
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 cmADaM.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) usingADaM.merge_columnsfor each covariate in thecovariatesvector. - 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 iterativelyfilter_func: Function to filter rows from the secondary dataset for each covariate.baseline: Iftrue, appends "BL"/"BLU" to output column names (default: true)kwargs...: Keyword aguments of leftjoin function can be passed. Eg:on,makeuniqueetc.
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/dLADaM.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 = :BILIBLast = :ASTBLuln_bili = :ULN_BILIBLuln_ast = :ULN_ASTBLbili_unit = :BILIBLUast_unit = :ASTBLUcol = :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
- Severe:
- Returns
missingfor both outputs if any argument is missing or does not match a category. - Output columns can be customized via the
colkeyword argument. - The units of
bilianduln_bilimust 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 missingADaM.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_VISITNUMwithShiftedArrays.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_ANCHORvia groupby + block membership, then passesr -> r.IS_ANCHOR - Trough-at-dose-time convention (sample at exact dose time belongs to next OCC): pre-compute
:IS_ANCHORas the first row at each(ID, time)whose time matches a dose time, then passr -> 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 aSymbol,AbstractString, or aVectorof either.order: within-group ordering column(s) used for sorting; default[](preserve within-group input order). Typical::AFRLTor[:ADT, :EVID].anchor_filter::Function: predicaterow -> Boolselecting anchor rows.occ_col: name for the derived column; default:OCC.pre_first: value for rows before the first anchor; default0. Passmissingto produce aUnion{Missing,Int}OCC column.
Caller Notes
anchor_filtersees 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 viaanchor_filter = r -> r.IS_ANCHOR.- Two anchor-flagged rows at the same
ordervalue bump OCC twice. Deduplicate upstream if that's not intended. anchor_filtererrors abort the call. Usecoalesce/ismissingif 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 1Treat 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 3Use 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 2Choose 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 2Sample-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 4ADaM.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 aren (pct%)(or another format — seeformatkwarg) of subjects in that population × treatment.:table_one—SummaryTables.table_onecategorical layout. Each flag is a bolded section header with one indented row per observed level (e.g.YandN); the leftmost group column isTotal; per-treatment columns share a top-level header spanningtreatment.:listingtable—SummaryTables.listingtablelayout. Same per-flag rows andn (pct%)cells as:simple, but rendered with listingtable's treatment-spanning column header. Honoursformat,percent_digits,n_label,show_total, andshow_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 aSymbol/AbstractStringorcolumn => <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. ThePairform always overrides metadata; the label may be aString,Annotated,Cell, etc.
Keyword Arguments
style::Symbol = :simple— rendering layout. One of:simple(flatsimple_table),:table_one(SummaryTables.table_onecategorical-by-treatment), or:listingtable(SummaryTables.listingtablewith a spanning treatment header). The kwargsformat,percent_digits,n_label, andincluded_flag_valueapply to:simpleand:listingtableonly —:table_onefollowsSummaryTables.table_one's defaults and shows one row per observed flag level.as_df::Bool = false— whentrue, return the long-form DataFrame (Population, per-treatment, andtotal_labelcolumns; first row is theNheadcount) with rawIntsubject 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 ofstyle.treatment::Union{Symbol,AbstractString} = :TRT01A— subject-constant treatment column (defaults to actual treatment for period 1; pass:TRT01Pfor 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, anAbstractVector/Set/Tupleof accepted values, or a unary predicatex -> 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. ThrowsArgumentErrorwhen the two sides disagree — either data has values not listed intreatment_order, ortreatment_orderlists 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
Nheader row, son/N-style cell formats are intentionally not offered.warn_inconsistent_treatment::Bool = true— emit a@warnwhen one or more subjects have ≥2 distinct non-missing values intreatment(the function then uses the first non-missing value for those subjects; combine withorderbyto make the tie-break deterministic).orderby = nothing— sortdfby these column(s) before the per-subject reduction. Only matters for record-level input with inconsistent within-subject treatment (seewarn_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 specDataFrames.sortaccepts (Symbol, String, Vector, orDataFrames.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
trueADaM.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):
descextendsDataFrames.describewithpct_missing,nunique, andunique_values.unique_valueslists distinct non-missing values (insertion order) whennunique <= unique_threshold;missingabove the cap.- For
*DTCcolumns (CDISC SDTM convention),nunique= distinct stripped lengths andunique_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 resultingDataFrame. - 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 rawDataFrames.desc=true: values are the single-datasetNamedTupleform.missingstringis forwarded toconvert_to_missingper 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.TableInclusion / 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— whentrue, return the input DataFrame with an added category column (named bycategory_label) instead of the renderedTable. Throws if a column of that name already exists.groupby = [:STUDYID]— stratification column(s); acceptsSymbol,AbstractString, or aVectorof 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 of0marks 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. whenEXCLF != 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 whosereasoncell ismissingor 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 whenas_df = trueand as the group header spanning the category columns in the renderedTable.
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.0ADaM.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 — aSymbol,AbstractString, orAbstractVectorof either. Default is empty (no stratification).evid: Numeric event-identifier column (default:EVID).dv: Numeric measurement column (default:DV). Passnothingto count everyevid == 0row (scheduled-slot semantics).blqfn: Numeric 0/1/missing BLQ flag column (default:BLQFN), read only whenexclude_blq = true.exclude_blq: Dropblqfn == 1rows before counting (defaultfalse).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 1Passing 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 2Plotting
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)
endADaM.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 theDynamicQuantities.Unitsspellings. Default is:h; pass:dayto divide by 24.evid: Numeric event-identifier column (default:EVID). Rows withevid != 0are dropped.blqfl: Numeric 0/1/missing BLQ flag column (default:BLQFN), read only whenexclude_blq = true.exclude_blq: Dropblqfl == 1rows before counting (defaulttrue— 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.0ADaM.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
Stringor 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 0ADaM.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 asN (pct%)wherepct = 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.
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— whentrue, return the per-group DataFrame instead of the rendered Table. Same values either way.groupby = :STUDYID— grouping column(s); accepts aSymbol, anAbstractString, 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 withflag == 0counts 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 asN (pct%)).pct_digits::Int = 1— decimals for the included percentage. Must be non-negative;0renders33%, not33.0%.show_overall::Bool = false— whentrue, append a pooled row across all groups, bolded in the rendered Table.overall_label::AbstractString = "All"— label for the pooled row, placed in everygroupbycolumn.
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:
drop_variables::Vector{String}— remove rows whoseVariableappears in the list. Names not in the loaded Spec throwArgumentError.spec_overrides::DataFrame— long-format cell patches with columnsVariable, Field, Value. Each row overwrites one cell. UnknownVariableorFieldthrows.append_spec::DataFrame— appended after the survivors.vcatusescols = :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:Existencefor 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
ArgumentErrorifdataset_pathorspec_pathdoes not exist.ArgumentErrorifsheetis not a sheet in the workbook.ArgumentErrorif the Spec sheet has noVariablecolumn, or if any row in it has a missingVariable.
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:
- drops template rows whose
SourceDomainlists any domain not present in the profiled folder (logs each drop via@info); - drops template rows whose
Variableappears indrop_variables(project-specific exclusions, e.g. this study doesn't collect race); - maps each surviving template row to a Spec-sheet row (translates
DefaultDerivation→Derivation, seedsStatus = "draft", translatesCoreintoNotesviaReq → "Required",Cond → "Conditional",Perm → "Permissible"; anything else →""); - pulls codelist values from each profile's
unique_valuesinto theCodelistcolumn for rows whosePredecessornames a single SDTM column (e.g.DM.SEX); - applies
spec_overrides(a long DataFrame with columnsVariable, Field, Value) — each row overrides one cell of one variable; - appends
append_spec(project-specific variables added by the skill from MAP/SAP context); - falls back to a single TODO row on
metadata/flags/imputationswhen 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.xptfiles.
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);vcatusescols = :union, so missing columns are padded rather than erroring.spec_overrides::DataFrame— long-format cell patches with columnsVariable, 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,
)