--- name: nca-v3-migration description: Migrate Julia code from NCA.jl v2 (Pumas 2.8 and earlier, plots via NCAUtilities) to NCA.jl v3.1 (Pumas 2.9). Use when code calls read_nca, run_nca, summarize, NCA.(pop), NCASubject, NCAPopulation, NCAReport, report, or NCAUtilities plot functions, uses the @nca macro inside a Pumas @model, or when errors mention "NCA.jl v3". --- # NCA.jl v2 to v3 migration NCA v3 is a rewrite with a DataFrame-only interface: `read_nca` returns a `DataFrame` of observations, `run_nca` takes it and returns a `DataFrame` of results. `NCASubject`, `NCAPopulation`, `NCADose`, `NCAReport`, `report`, `showunits`, `pop[i]`, `.reportdf` and direct endpoint calls like `NCA.auc(pop)` are gone. Work through the steps below in order for the whole file, then run it (Step 10) and fix what remains. ## Step 1: packages - `using NCAUtilities` -> delete. Plots and tables are in `NCA` (CairoMakie comes with it). - `using Pumas` no longer loads NCA: add `using NCA` (and NCA to the project) where needed. - `using NCA.Unitful` / `NCA.Unitful` -> `using Unitful` (add Unitful to the project). - `NCA.NullRoute`, `NCA.IVBolus`, `NCA.IVInfusion`, `NCA.EV` in Pumas code -> Step 8. ## Step 2: reading data ### Pick the reader by looking at the data `read_nca` is the default and preferred reader; `read_nca_pumas` is a convenience for one specific layout. Do not translate v2 `read_nca` to `read_nca_pumas` by default. - `read_nca(df; ...)`: tidy data. Every row is an observation; `dose`, `route`, `dose_time`, `tau`, `duration` carry the same value on every row of a profile. Use it whenever the data is, or can trivially be made, tidy. - `read_nca_pumas(df; ...)`: NONMEM/Pumas event style. One row per profile has the dose, all other rows have `missing` in every dose-related column. Use it only for `DataFrame(simobs(...))` tables and data with an `evid`/`amt` dosing row. Same keywords as `read_nca` except `dose_time`. v2 `read_nca` was built around dose rows (it read a constant dose column as a dose at the first observation), so v2 code often turned tidy data into the dose-row layout first, and datasets come in both shapes. The v2 call tells you nothing; for every read, print one subject first: `first(@rsubset(df, :ID == first(df.ID)), 12)`. Dose on one row per profile and `0`/`missing` elsewhere (most CSV exports and PharmaDatasets sets): `read_nca_pumas` after zeros -> `missing`. A dose column constant across the profile (`DOSE`, a dose-group column): `read_nca` with that column. If the v2 code built a dose row from a tidy column (`@rtransform :amt = :Time == 0 ? :Dose : missing`), drop that step and use `read_nca` with `dose = :Dose` (numeric; keep any `parse` step). `read_nca` on interleaved data fails with `Found different values 10.0 and 0.0 in the dose column`: switch reader, do not fix values. ### Rename keywords | v2 keyword | v3 keyword | | --- | --- | | `observations = :conc` | `concentration = :conc` | | `amt = :amt` | `dose = :amt` | | `ii = :ii` | `tau = :ii` (steady state) or `assumed_tau = :ii` (predicted), see below; must name a column (`df.tau .= 24.0` for a constant) | | `ss = :ss` | removed; steady state is implied by a non-missing `tau` | | `timeu`, `concu`, `amtu`, `volumeu` | `time_unit`, `concentration_unit`, `dose_unit`, `volume_unit`, all or none | | `llq`, `concblq`, `missingconc`, `missingvolume`, `sparse`, `verbose` | removed (`MethodError`); `llq`, `concblq`, `sparse` move to `transform_nca` | | `group = [:a, :b]` | unchanged; `group = "SEX"` -> `group = [:SEX]`; `group = [:dose]` -> rename the column first (`:dose`, `:tau`, `:route`, ... are reserved: `Got group column name :dose that is reserved`) | | (no `route` given) | `route` is now required for every read, also for plotting only: add a column (`df.route .= "ev"`) if the data has none | - Units: if any `*_unit` is given, all dose-related columns need one, including `dose_unit` even when v2 had no `amtu` (`Partial unit specification is not supported`; `NoUnits` is rejected). Take the dose unit from context or drop all units. Numeric options then need units too: `llq = 0.001u"mg/L"`, `interval = (0u"hr", 12u"hr")` (`DimensionError` otherwise). - Auto-detection matches only the v3 names exactly (case-insensitive): `id`, `time`, `concentration`, `dose`, `route`, `duration`, `tau`, `blq`, `start_time`, `end_time`, `volume`. `conc`, `amt`, `dv`, `ii`, `observations` are never found: pass `dose = :amt`, `concentration = :conc`. Conversely a helper column named `blq`, `tau` or `duration` is used even without the keyword; rename it if that is not intended. ### Fix the data - Zeros are not "no value". Tables written by older Pumas versions and the v2 tutorial datasets put `0` in `amt`, `ii`, `duration` on observation rows; `read_nca_pumas` needs `missing` there (a zero `tau`/`duration` is tolerated only on the dose row itself, and blank or `.` cells from NONMEM exports already arrive as `missing`). The reader keeps this rule on purpose, so convert before reading: ```julia zero_to_missing(x) = x == 0 ? missing : x df = @rtransform df :AMT = zero_to_missing(:AMT) :II = zero_to_missing(:II) ``` - Dose rows keep a `missing` concentration. Pumas tables have a dose row and an observation row at the same time; the dose row is dropped only while its concentration is `missing`. Filling it (`coalesce`, `dropmissing` then re-adding) gives `Found a profile with the same value 0.0 twice in the time column`. - `blq` must be `Bool`: `df.blq = df.blq .== 1`. - `route`: `"iv"`, `"inf"`, `"ev"` still work; prefer `"ivbolus"`, `"ivinfusion"`, `"ev"`. The output keeps the string, there is no enum. - `time` becomes time-after-dose at read time. `read_nca` takes `dose_time = :col` (repeated per profile) when `time` is absolute; without it, `time` is taken as time-after-dose. Missing times are an error. - One dose per profile (`Found multiple rows with non-missing dose amounts in one profile`). Pumas tables have `dosenum`: `group = [:dosenum]`. Measured data: filter to the dose you want (`@rsubset df :TIME > 24`) or derive an occasion column after zeros -> `missing`: ```julia transform!(groupby(df, :ID), :AMT => (amt -> cumsum(.!ismissing.(amt))) => :dosenum) read_nca_pumas(df; ..., group = [:DOSE, :dosenum]) ``` v2 gave per-dose vectors from `NCA.auc(pop[1])`; v3 gives one row per profile. For one value per subject, filter the result to the dose the v2 code meant (`@rsubset results :dosenum == 1`). v3 has no concept of multiple dosing: a profile is one dose, and the plot functions draw profiles side by side, each starting at its own dose. Do not read a repeated-dosing course as one profile with a constant dose column to keep plots continuous: every endpoint would treat it as a single dose. Once split, always include `dosenum` in a plot's `group`; `summary_observations_vs_time(df_r; group = [:DOSE])` on split data does not error but overlays the per-dose axes. v2 code that used `axis = (; limits = ...)` to cut a whole-course plot into "day 1" and "day 10" windows was isolating single profiles by hand; the per-dose profiles now do that natively: ```julia # v2: one NCASubject per subject with a dose every 24 h, day 10 cut out by hand summary_observations_vs_time(pop; group = [:DOSE], axis = (; limits = ((216, 240), nothing))) # v3: the 10th dose is its own profile, time-after-dose starts at 0 summary_observations_vs_time(@rsubset(df_r, :dosenum == 10); group = [:DOSE]) ``` If a sequence over the whole course is really wanted, plot the raw DataFrame with AlgebraOfGraphics: ```julia means = @by df [:DOSE, :NOMTIME] :mean = mean(skipmissing(:CONC)) :sd = std(skipmissing(:CONC)) draw(data(means) * mapping(:NOMTIME, :mean; color = :DOSE => nonnumeric) * (visual(Lines) + visual(Scatter) + mapping(:sd) * visual(Errorbars))) ``` ### Steady state: `tau` vs `assumed_tau` v2 had `ii` plus the flag `ss`. v3 has two columns with different meanings, each unlocking a different endpoint family. Pick exactly one per profile (both non-missing is an error). - `tau`: the profile was measured at steady state (v2 `ii` with `ss = 1`, a MAD day-N or last-dose profile). Observed interval endpoints: `ctau`, `cavg`, `auc_tau` (= `auctau_pred`; `auctau_obs` on request), `aumc_tau`, `fluctuation`, `fluctuation_tau`, `swing`, `swing_tau`, plus `cmax`, `cmin`, `aucinf_*`. Rename on such data: `cmaxss` -> `cmax`, `cminss` -> `cmin`, `cavgss` -> `cavg`, `auc_tau_obs` -> `auctau_obs`. `cmaxss`, `cminss`, `cavgss`, `accumulation_index` are NOT available (`can only be calculated for non-steady state profiles`), and neither are `cl_obs`, `cl_f_obs`, `vz_obs`, `vz_f_obs` (`Only calculated for non-steady state dosing`): at steady state use `cl_pred`/`cl_f_pred` and `vz_pred`/`vz_f_pred`, which are then `dose / auctau_pred` based. - `assumed_tau`: a single-dose profile with a hypothetical interval (v2 `ii` without `ss`). Predicted endpoints: `cmaxss`, `cminss`, `cavgss`, `accumulation_index`. `ctau`, `auctau_*`, `cavg`, `fluctuation`, `swing` are NOT available (`No tau defined`). - Separate `ii`/`ss` columns: `df.tau = ifelse.(df.ss .== 1, df.ii, missing)` before reading. `ss = 1` without an `ii` value has no equivalent; ask the user for the interval. - Do not read the same data twice to get both families. If a v2 `summarize` list mixes them on steady-state data (`:auc_tau_obs` and `:cavgss`), keep the observed names and drop the rest. - Unavailable endpoints are `missing` with an `_err` reason, never an exception. - `NCA.tau(pop)`, `NCA.doseamt(pop)`, `NCA.dosetype(pop)` -> `meta = run_nca(df_r, Symbol[])` and its `tau`, `dose`, `route` columns. The `read_nca` output has `tau`/`assumed_tau` only if the keyword was passed. ### BLQ, LLQ, sparse: `transform_nca` ```julia df_r = read_nca(df; ...) # blq = :isblq if a Bool column exists # v2 llq = 0.001 [+ concblq = Dict(...)]; this dict is the v2 default and must be spelled out: df_r = transform_nca(df_r; llq = 0.001, blq_rules = Dict(:first => :keep, :middle => :drop, :last => :keep)) # v2 blq = :isblq, concblq = :drop: df_r = transform_nca(df_r; blq_rules = Dict(:first => :drop, :middle => :drop, :last => :drop)) df_r = transform_nca(df_r; sparse = true) # v2 sparse = true ``` - `blq_rules` values are `:keep`, `:drop` or a number to impute (`:last => 0.15`), like `concblq`. - One `transform_nca` call for BLQ, with `llq` and `blq_rules` together; a second call errors with `Tried renaming to :concentration_before_blq`. Originals stay in `concentration_before_blq`. - A `blq` column alone changes nothing. v2 applied the default rules whenever `blq` or `llq` was given; v3 only inside `transform_nca`, so always add the call. - `llq` is rejected when a `blq` column exists, whether passed as `blq = :col` or auto-detected from a column named `blq`. v2 combined them additively; do it by hand before reading: `df.blq = (df.isblq .== 1) .| (df.conc .<= 0.6)`. - `missingconc`/`missingvolume`: rows with a missing concentration are ignored by `run_nca`; impute beforehand if needed. ### Indexing subjects `pop[1]`, `pop[1:n]`, `pop[ids]`, `length(pop)`, `subject.observations`, `subject.dose` do not exist. Use `ids = unique(df_r.id)`, `@rsubset df_r :id == ids[1]`, `length(ids)`, and the `concentration`/`dose` columns. Plot functions take `ids = [...]` (Step 6). ## Step 3: `run_nca` ```julia # v2: report = run_nca(pop; parameters = [:cmax, :aucinf_obs], sigdigits = 3, studyid = "...", ...); report.reportdf df = run_nca(df_r, [:cmax, :aucinf_obs]) ``` - Endpoints are the second positional argument. With no list, all endpoints applicable to the data are computed; for `tau` data that default set has `auc_tau`, `aumc_tau`, `cavg`, `ctau`, `fluctuation`, `swing` (not `auctau_obs`, `cmaxss`, `cavgss`). Pass an explicit list whenever later code (`summarize`, joins, plots) relies on column names. - Metadata keywords (`studyid`, `studytitle`, `author`, `sponsor`, `date`, `conclabel`, `timelabel`, `grouplabels`, `groupsizes`, `versionnumber`, `investigational_product`, `protocol_number`, `verbose`) and `sigdigits`/`sigdig` are removed. Rounding belongs to the presentation layer (SummaryTables and the Quarto report format it); only round the values themselves if old code built tables by hand from them. - Result columns: grouping columns, `id`, `profile` (integer profile index), `dose`, `route`, `duration`, `tau`, `assumed_tau`, one column per endpoint. Replace `x.reportdf` with `x`. - Failed endpoints are `missing` with the reason in `_err` (which is `missing` on success); nothing is logged. Never `dropmissing!(df)` without columns: as soon as one `_err` column exists it is `missing` exactly on the successful rows, and `duration`, `tau`, `assumed_tau` are typically all `missing`, so every row goes. Name the columns: `dropmissing!(df, [:cmax])`. `vcat` results with `cols = :union` because `_err` columns appear only when needed. Join two results on `[:id, :profile]` after `select`ing each down to `[:id, :profile, endpoints...]` (shared metadata columns clash otherwise). - Options: `method` (`:linear`, `:linuplogdown`, `:linlog`), `threshold`, `adjr2factor` are `run_nca` keywords applying to all endpoints: `run_nca(df_r, [...]; method = :linlog, threshold = 3)`. Inline options work only for endpoints that take them, and then an output name is required; the name may not be another endpoint's name (`:aucinf_obs => :auc` errors): ```julia run_nca(df_r, [ :auclast, :auclast => (; method = :linlog) => :auclast_log, :auc_partial => (; interval = (0, 12)) => :auc_0_12, :auc_partial => (; interval = (12, 24)) => :auc_12_24, :cmax_interval => (; interval = (0, 1)) => :cmax_0_1, :lambdaz => :kel, # plain rename ]) ``` Endpoints with inline options: `auc_partial` (`interval`, `method`); `auclast`, `aucall`, `aumclast`, `auctau_obs/pred`, `aumctau_obs/pred`, `ctau`, `aurclast`, `aurcall` (`method`); `cmax_interval`, `cmin_interval`, `tmax_interval`, `tmin_interval` (`interval`); `lambdaz_fit` (`adjr2factor`, `threshold`); `cpred` (`t`, `method`). Everything else (`aumcinf_obs`, `lambdaz`, ...) only via the top-level keywords. ### Endpoint names Apply to `parameters = [...]`, `summarize(parameters = ...)` and `NCA.(pop)` calls. Deprecated aliases still work with a warning; rename anyway. | v2 | v3 | | --- | --- | | `kel` | `lambdaz` | | `thalf` | `half_life` | | `auc_tau_obs`, `auctau` | `auctau_obs` | | `auc_tau_pred` | `auctau_pred` | | `aumc` | `aumcinf_obs` (`pred = true`: `aumcinf_pred`) | | `aumctau` | `aumctau_obs` | | `mrt` | `mrtinf_obs` (`auctype = :last`: `mrtlast`; `pred = true`: `mrtinf_pred`) | | `cl` | `cl_obs` (IV) or `cl_f_obs` (EV); `pred = true`: `cl_pred`, `cl_f_pred` | | `vz` | `vz_obs` (IV) or `vz_f_obs` (EV) | | `vss` | `vss_obs` (`pred = true`: `vss_pred`) | | `accumulationindex` | `accumulation_index` | | `auc_extrap_obs`, `auc_extrap_percent` | `auc_extrap_percent_obs` | | `auc_extrap_pred` | `auc_extrap_percent_pred` | | `auc_back_extrap_obs/pred` | `auc_back_extrap_percent_obs/pred` | | `aumc_extrap_obs/pred`, `aumc_extrap_percent` | `aumc_extrap_percent_obs/pred` | | `aumc_back_extrap_percent` | removed | | `auc_stderr` | `auclast_stderr` | | `aurc`, `aurc_inf_obs` | `aurcinf_obs`; `aurc_last_obs`: `aurclast`; `aurc_inf_pred`: `aurcinf_pred` | | `aurc_extrap_obs/pred`, `aurc_extrap_percent` | `aurc_extrap_percent_obs/pred` | | `rsq_kel` | `lambdazr2` | | `rsq_adj_kel`, `rsq_adjusted_kel` | `lambdazadjr2` | | `corr_kel`, `lambdazr` | `lambdazcorr` | | `n_samples_kel`, `n_points_kel` | `lambdaznpoints` | | `intercept_kel` | `lambdazintercept` | | `kel_t_low` / `kel_t_high` | `lambdaztimefirst` / `lambdaztimelast` | | `cmaxss`, `cminss`, `cavgss` on steady-state (`tau`) data | `cmax`, `cmin`, `cavg` (Step 2) | | `dose`, `route`, `tau` | input columns, always in the result, not endpoints | | `run_status` | removed (use the `_err` columns) | Unchanged: `tmax`, `cmax`, `cmax_dn`, `c0`, `tlast`, `clast`, `clast_pred`, `auclast`, `auclast_dn`, `half_life`, `aucinf_obs/pred`, `aucinf_dn_obs/pred`, `tmin`, `cmin`, `ctau`, `vz_obs`, `cl_obs`, `vz_f_obs`, `cl_f_obs`, `*_pred`, `vss_obs/pred`, `fluctuation`, `fluctuation_tau`, `n_samples`, `aumclast`, `aumcinf_obs/pred`, `mrtlast`, `mrtinf_obs/pred`, `swing`, `swing_tau`, `span`, `tlag`, `cmax_stderr`, and the urine endpoints `tmax_rate`, `max_rate`, `rate_last`, `mid_time_last`, `amount_recovered`, `percent_recovered`, `urine_volume`. ## Step 4: individual endpoint calls `NCA.f(pop; kwargs...)` (DataFrame), `NCA.f(subject)` (number) and `NCA.f(conc, time)` (raw vectors) are all gone. Compute through `run_nca` and select columns: | v2 | v3 | | --- | --- | | `NCA.cmax(pop)` | `run_nca(df_r, [:cmax])` | | `NCA.cmax(pop; normalize = true)` | `[:cmax_dn]` (also `auclast_dn`, `aucinf_dn_obs`, `cmin_dn`) | | `NCA.cmax(pop; interval = (0, 1))`, `NCA.tmax(pop; interval)` | `[:cmax_interval => (; interval = (0, 1)) => :cmax_0_1]`, `:tmax_interval => ...` | | `NCA.auc(pop)` | `[:aucinf_obs]` | | `NCA.auc(pop; auctype = :last)` | `[:auclast]` (no predicted `auclast` exists) | | `NCA.auc(pop; pred = true)` | `[:aucinf_pred]` | | `NCA.auc(pop; normalize = true)` | `[:aucinf_dn_obs]` (`pred = true`: `aucinf_dn_pred`) | | `NCA.auc(pop; interval = (0, 12), method = :linuplogdown)` | `[:auc_partial => (; interval = (0, 12), method = :linuplogdown) => :auc_0_12]` | | `NCA.auc(pop; interval = [(0, 12), (12, 24)])` | one `:auc_partial => ... => :name` per interval | | `NCA.auc(pop; auctype = :last, interval = (0, 4))` | `:auc_partial => (; interval = (0, 4)) => :name` too (`auclast` takes no `interval`) | | `NCA.auc(pop; method = :linlog)`, `NCA.aumc(pop; method = :linlog)` | `run_nca(df_r, [:aucinf_obs, :aumcinf_obs]; method = :linlog)` | | `NCA.auctau(pop)` | `[:auctau_obs]` (needs `tau`) | | `NCA.mrt(pop)`, `NCA.thalf(pop)` | `[:mrtinf_obs, :half_life]` | | `NCA.cl(pop)`, `NCA.vz(pop)` | `[:cl_f_obs, :vz_f_obs]` for EV data, `[:cl_obs, :vz_obs]` for IV | | `NCA.lambdaz(pop; threshold = 3)`, `NCA.lambdaz(pop; adjr2factor = 0.1)` | `run_nca(df_r, [:lambdaz]; threshold = 3)`, `...; adjr2factor = 0.1)` | | `NCA.lambdaz(pop; slopetimes = [8, 12, 16])` | before reading, add a `Bool` `lambdaz_mask` column (`true` on the points to use; per profile all `Bool` or all `missing`): `df.lambdaz_mask = in.(df.TIME, Ref([8, 12, 16]))`; then `[:lambdaz]` | | `NCA.lambdaz(pop; idxs = [2, 3, 4])` | v2 indexed the non-missing observations, so build the mask on the `read_nca` output after dropping rows with a missing concentration: `df_r = dropmissing(df_r, :concentration); transform!(groupby(df_r, [:id, :profile]), :time => (t -> in.(eachindex(t), Ref([2, 3, 4]))) => :lambdaz_mask)`; then `run_nca(df_r, [:lambdaz])` | | `NCA.lambdazr2(pop)`, `lambdazadjr2`, `lambdazr`, `lambdaznpoints`, `lambdazintercept`, `lambdaztimefirst`, `lambdaztimelast`, `span` | `[:lambdazr2, :lambdazadjr2, :lambdazcorr, :lambdaznpoints, ...]`; no prior `lambdaz` call needed | | `NCA.tau(pop)`, `NCA.doseamt(pop)`, `NCA.dosetype(pop)` | not endpoints: `meta = run_nca(df_r, Symbol[])`, then `meta.tau`, `meta.dose`, `meta.route` | | `fluctuation(; usetau = true)`, `swing(; usetau = true)` | `fluctuation_tau`, `swing_tau` | | `NCA.f(subject)` on `pop[i]` | run on the whole frame and filter the result row | | `NCA.lambdaz(conc, time)`, `NCA.tmax(conc, time)`, `NCA.cmax(conc, time)` | `run_nca(read_nca(DataFrame(id = 1, time, concentration, dose = 1.0, route = "ev")), [:lambdaz])`; `run_nca` never takes a bare frame | | other `NCA.f(pop)` | same name (or renamed per the table above) in the endpoint list | Several endpoints in one call replace chains of `innerjoin`: `run_nca(df_r, [:vz_f_obs, :cl_f_obs, :lambdaz, :cmax_dn, :mrtinf_obs, :aumcinf_obs])`. ## Step 5: `summarize` ```julia summarize(df; stratify_by = [:Dose], parameters = [:cmax, :aucinf_obs], stats = [mean, std]) ``` Takes the `run_nca` DataFrame. `stratify_by` defaults to the `group` columns of `read_nca`; `parameters` defaults to all result columns and needs v3 names (no integer indices); `skipmissings` is removed (missing values are always skipped). The output always has `numsamples` and `nonmissing`; default stats are `minimum, maximum, mean, std, geomean, geostd, geomeanCV`. ## Step 6: plots and tables (formerly NCAUtilities) Every plot function returns an `AlgebraOfGraphics.FigureGrid` (`paginate = true`: a `Vector` of them), which displays and saves like a `Figure` (`fg.figure` gives the Makie `Figure`). The figure-position form survives: `parameters_vs_group(fig[1, 1], results; ...)` draws into an existing `Figure` and returns the axis entries, and `AlgebraOfGraphics.legend!(fig[1, 2], entries)` adds the legend as before. The functions keep `figure`, `axis`, `legend`, `facet`, `paginate`, `rows`, `columns` and the style keywords, with these changes: `limit` may not be combined with `rows`/`columns` (drop it); `facet = (; combinelabels = true)` is removed (delete it); `facet = (; linkaxes = true)` -> the AlgebraOfGraphics facet options `facet = (; linkxaxes = :all, linkyaxes = :all)` (values `:all`, `:minimal`, `:none`; they must sit inside `facet`, not at the top level). `separate` on `subject_fits` (seen in old tutorials) was never an option of that function and is now an error; drop it. `subject_fits` always draws one facet per profile. `separate` on `summary_observations_vs_time` is unchanged. `llq = 0.001` draws the LLQ line. `interactive(fig)` still exists. The first argument changed: | v2 | v3 | | --- | --- | | `observations_vs_time(pop)` | `observations_vs_time(df_r)` (the `read_nca` frame) | | `observations_vs_time(pop[1])`, `observations_vs_time(pop[ids])` | `observations_vs_time(df_r; ids = [id1])` (always a vector) | | `summary_observations_vs_time(pop; group = [:DOSE], separate = false, statistic = :geomean)` | same with `df_r` | | `subject_fits(report)`, `subject_fits(pop)` | `subject_fits(df_r)`; fits itself, accepts `threshold`, `adjr2factor`, `ids` | | `parameters_dist(report; parameter)`, `parameters_vs_group(report; parameter, group)` | same with the `run_nca` result | | `groups_check(report)` | `groups_check(df_r)` or `groups_check(results)` (needs `group` columns) | | `power_model(model)`, `dose_vs_dose_normalized(report, :cmax)` | `power_model(model)`, `dose_vs_dose_normalized(results, :cmax)` | | `NCAUtilities.geomean_cv_table(report, ...)` | `NCA.geomean_cv_table(results, [:auclast, :cmax]; variable_names = Dict(...))` | ## Step 7: dose linearity and superposition - `DoseLinearityPowerModel(report, :cmax; level)`, `DoseLinearityRatioTest(...)` -> same with the `run_nca` result (needs its `dose` column). - `NCA.superposition(pop; ii = 24, ndoses = 7, amt = 20000, steadystatetol, method)` -> `NCA.superposition(df_r; assumed_tau = 24, n_doses = 7, maintenance_dose = 20000, loading_dose = 20000, method)`. `ndoses = Inf` -> `n_doses = Inf` (count from the estimated half-life; `max_auto_doses` caps it); `steadystatetol` has no equivalent. Complex schedules: `dose_times`, `doses`. Input profiles must have `tau === missing`. - Output: input columns plus `superposition_dose_number`, `superposition_error`, `superposition_assumed_tau`; `time` is time-after-dose per dose, absolute time is `dose_time + time` (v2 had `abstime`, `observations`, `occasion`, `ii`). `run_nca(sup)` treats the whole superposed curve as one profile. Per dose interval: ```julia sup_input = select(sup, Not([:profile, :dose_time, :nominal_dose_time, :exclusion_mask, :superposition_error, :superposition_assumed_tau, :superposition_dose_number]), :superposition_dose_number => :dose_number) sup_input.tau = ifelse.(sup_input.dose_number .== maximum(sup_input.dose_number), 24.0, missing) run_nca(read_nca(sup_input; group = :dose_number), [:cmax, :cmin, :cavg, :auctau_obs, :fluctuation]) ``` `cmaxss`, `cminss`, `cavgss`, `accumulation_index` are single-dose predictions: request them on the original single-dose data with `assumed_tau`, not on the superposition output. ## Step 8: Pumas integration Pumas 2.9 hands simulations to NCA only through tables: ```julia using Pumas, NCA, DataFrames # v2: DosageRegimen(100; ii = 24, ss = 1, route = NCA.IVBolus); NCASubject.(Subject.(sim)) subject = Subject(; id = 1, events = DosageRegimen(100; ii = 24, ss = 1)) sim = simobs(model, [subject], params; obstimes = 0:1:24) df = DataFrame(sim; include_events = true) df.route .= "ivbolus" nca_data = NCA.read_nca_pumas(df; id = :id, time = :time, concentration = :cp, dose = :amt, route = :route, duration = :duration, tau = :ii, group = [:dosenum]) results = run_nca(nca_data, [:cmax, :auclast, :half_life]) ``` - `route = NCA.IVBolus` in `DosageRegimen`/`read_pumas` is deprecated and ignored. Add a `route` column with the string `"ivbolus"`, `"ivinfusion"` or `"ev"` to the simulation table instead (`missing` for none). If subjects differ in route, give each `Subject` a `route` covariate with that string; it ends up in the table as a column (Pumas then warns `Covariate route is not used in the model`, which is expected). - `NCASubject(subject)`, `NCASubject.(Subject.(sim))`, `read_nca(DataFrame(sim))` -> the recipe above. Rows before the first dose have `dosenum = 0`; filter them out unless wanted. A dose with `ss = 1` passes its `ii` on as `tau`; other `ii` values read as `missing`. Steady-state endpoints follow the `tau` rules of Step 2 (`cmaxss` -> `cmax`). Leave the dose row's concentration `missing` (Step 2). - Several simulations of one subject (`simobs_uncertainty`, `simobs` with a vector of parameter sets, repeated `simobs` calls) share the `id`, so their table fails with `Found multiple rows with non-missing dose amounts in one profile`. Add a sample index before converting and group by it: `dfs = [transform(DataFrame(s; include_events = true), :id => (x -> i) => :sample) for (i, s) in enumerate(sims)]`, `df = vcat(dfs...)`, then `group = [:sample, :dosenum]`. - The `@nca` block in `@observed` (`ncas := @nca dv_cp dv_cm; auccp, auccm = @. NCA.auc(ncas)`) is removed: simulate, then run NCA per observed variable (`concentration = :dv_cp`, then `:dv_cm`), `select` each result to `[:id, :profile, endpoints...]` with renamed endpoints, and `innerjoin` on `[:id, :profile]`. ## Step 9: removed features - `report(...)`, `NCAReport`, PDF generation: removed without replacement in NCA. Reports are Quarto documents: the native Julia engine (QuartoNotebookRunner) and helpers such as QuartoTools make the normal Quarto workflow work with the Pumas stack, so the `summarize` tables and figures go into a `.qmd`. Delete the calls and metadata keywords; keep the `summarize` output and figures they were built from. - `NCAPopulation`/`NCASubject` constructors: build a DataFrame and read it (Step 8). - `read_nca("file.csv")`: `CSV.read(path, DataFrame)` first. `read_nca(df_obs, df_dose)`: join the dose information onto the observations first. - `showunits`, `run_status`, `NCA.cleanblq`, `NCADose`: removed. ## Step 10: run and compare Run the migrated file in a session with NCA 3.1 (Pumas 2.9 needs Julia 1.12.7 or newer) and fix errors in order. If several migrated scripts run in one session, watch for globals such as `data` or `df` shadowing `AlgebraOfGraphics.data` in later scripts. | error | fix | | --- | --- | | `Found different values 10.0 and 0.0 in the dose column` | interleaved data: `read_nca_pumas` after zeros -> `missing`; tidy data: repeat the dose on every row | | `Found multiple rows with non-missing dose amounts in one profile` | values include `0`: zeros -> `missing`; real doses: split with `group` (Step 2) | | `The tau column had invalid content for a profile` | zeros -> `missing` in the `ii`/`tau` column too | | `Found a profile with the same value 0.0 twice in the time column` | the dose row got a concentration; keep dosing rows at `missing` | | `Got group column name :dose that is reserved for input data` | rename the grouping column (e.g. `:dose_group`) before reading | | `A column for route is required but could not be found automatically` | `route` is mandatory; add `df.route .= "ev"` (or the real route) | | `Got non-boolean value in blq column` | `df.blq = df.blq .== 1` | | `A column for concentration is required but could not be found automatically` | `concentration = :CONC` (likewise `dose = :AMT`) | | `got unsupported keyword argument "llq"` (or `observations`, `amt`, `ii`, `ss`, `concblq`, `sparse`, `verbose`) | rename per the keyword table; `llq`/`concblq`/`sparse` go to `transform_nca` | | `Got assumed_tau = Symbol("24") which is not a column` | `tau`/`assumed_tau` name a column; add a constant column | | `Partial unit specification is not supported` | add `dose_unit` (and `volume_unit`) or drop all units | | `DimensionError` in `llq` or `interval` | attach units: `0.001u"mg/L"`, `(0u"hr", 12u"hr")` | | `Cannot transform dataframe with llq = ... because it already has a blq column` | the frame was read with `blq =`, has a column named `blq`, or was transformed already; build one combined `blq` column before reading | | `Got endpoint specification with keywords but without output name` | add `=> :name` | | `Can't rename endpoint ... reserved endpoint name` | choose an output name that is not an endpoint | | `column name :auctau_obs not found` (in `summarize`, joins, plots) | request the endpoint explicitly in `run_nca` | | `no method matching aumcinf_obs(...; method)` (an endpoint refusing a keyword) | pass `method`/`threshold`/`adjr2factor` to `run_nca` itself | | `no method matching auc(::DataFrame)`, `lambdaz(::Vector, ::Vector)` | `run_nca(df_r, [:endpoint])` | | `Got unknown NCA endpoints :dose and :route` | columns, not endpoints: `run_nca(df_r, Symbol[])` | | `Profile source dataframe must have a :profile column` | pass the frame through `read_nca`/`read_nca_pumas` first | | `can only be calculated for non-steady state profiles` / `No tau defined` / `Only calculated for non-steady state dosing` in `_err` | wrong steady-state family for this data (Step 2); at steady state use `cl_pred`, `vz_pred` variants | | `getindex(::DataFrame, ::Int64)` from `pop[1]` | filter by `id` or use `ids = [...]` | | `Cannot specify rows and columns together with limit` | drop `limit` | | `is deprecated in NCA v3, use ...` | rename per the tables | Endpoint values agree with v2 up to the documented v3 fixes (`auctau` extrapolation, `linlog` for IV bolus). When both outputs exist, compare `cmax`, `auclast` and `half_life`.