BLQ & Missing Data
After reading data with read_nca, you can apply transformations to handle below the lower limit of quantification (BLQ) values, sparse sampling designs, or lower limit of quantification (LLQ) truncation using transform_nca.
The typical workflow is: read_nca → transform_nca → run_nca.
transform_nca
NCA.transform_nca — Function
transform_nca(df; [sparse, llq, blq_rules])Transform a DataFrame that is the result from read_nca or read_nca_pumas. Each keyword corresponds to a different transformation that is available.
sparse
If true, collapse all profiles of a given group into one. This operation is only supported for plasma analyses and observations are grouped using the nominal_time column. The time column of the result contains the mean of the time values for all subjects that have been merged because they are not necessarily equal.
This transformation adds a sparsity column to the dataframe which can be used by downstream analysis and TLF functions.
llq
Set the lower limit of quantification. Every non-missing observation with concentration <= llq is flagged as below the limit of quantification (BLQ) by adding a boolean blq column. This transformation can only be used when the DataFrame passed to transform_nca has no blq column. If it already has a precomputed blq mask, omit llq; the existing mask is handled according to blq_rules. To provide a mask with raw input, pass the mask column using the blq keyword of read_nca.
blq_rules
Decide how each below-the-limit-of-quantification (BLQ) observation is handled, based on where it falls in the profile relative to the first and last measurable (non-BLQ) observations:
:first: BLQ observations before the first measurable observation (leading BLQs).:middle: BLQ observations between the first and last measurable observations (embedded BLQs).:last: BLQ observations after the last measurable observation (trailing BLQs).
Pass a dict-like object mapping each of these keys to a rule. Each rule is one of:
:keep: the BLQ value is left unmodified:drop: the BLQ value is replaced withmissing- a number: the BLQ value is replaced with this number (e.g.
0)
The default is Dict(:first => :keep, :middle => :drop, :last => :keep). If every observation in a profile is BLQ, the :first rule is applied to all of them.
Processed values are stored in concentration so downstream functions use the same column with or without BLQ handling. The original observations are preserved in concentration_before_blq as an audit trail.
Examples
The blq_rules apply a separate rule to each BLQ position. Here leading BLQs are replaced with 0, the embedded BLQ is kept, and the trailing BLQ is dropped to missing; concentration shows the result next to the unmodified concentration_before_blq:
julia> using NCA, DataFrames
julia> df = DataFrame(
id = 1,
time = [0, 1, 2, 3, 4, 5],
concentration = [0.3, 5.0, 3.0, 0.4, 1.0, 0.2],
dose = 100.0,
route = "ev",
);
julia> df_r = NCA.read_nca(df);
julia> df_blq = NCA.transform_nca(df_r; llq = 0.5, blq_rules = Dict(:first => 0.0, :middle => :keep, :last => :drop));
julia> df_blq[:, [:time, :concentration_before_blq, :blq, :concentration]]
6×4 DataFrame
Row │ time concentration_before_blq blq concentration
│ Float64 Float64 Bool Float64?
─────┼─────────────────────────────────────────────────────────
1 │ 0.0 0.3 true 0.0
2 │ 1.0 5.0 false 5.0
3 │ 2.0 3.0 false 3.0
4 │ 3.0 0.4 true 0.4
5 │ 4.0 1.0 false 1.0
6 │ 5.0 0.2 true missingThe sparse transformation collapses a sparse-sampling group into a single profile:
julia> using NCA, PharmaDatasets
julia> df_s = dataset("nca/sparse_example");
julia> df_sr = NCA.read_nca(df_s);
julia> df_sparse = NCA.transform_nca(df_sr; sparse = true);
julia> setdiff(names(df_sparse), names(df_sr)) # column added by the sparse design
1-element Vector{String}:
"sparsity"