Reporting NCA bugs

Hello,

Just noticed a couple bugs I’d like to report.

NCA.cmin(ncapop; interval = (t1, t2) gives all values as missing, while NCA.clast doesn’t.

AUCinf_obs and AUCinf_pred came out incorrect for a given subject. I noticed when mean and geomean for a partial AUC came out slightly greater than the AUCinf, and found that a single subject had much smaller AUCinf than partial AUC.

Hello Donald,

PumasAI is working on a new version of NCA that fixes many of these issues.
Do you have any reproducer datasets that show these issues?
Then I will check that the issues are resolved in the next release.

Hi Arno,

Thank you for the response. I have generated a reproducer data set:

ID	TAD	DV	AMT	ROUTE
1	0		15	EV
1	0.017	840		
1	2	1350		
1	4	1450		
1	8	1388		
1	12	1200		
1	24	1100		
1	72	399		

And here is the code:

df.AMT = df.AMT .* 10^6
df.DV = df.DV ./ 1000

pop = read_nca(df,
            id              =   :ID,
            time            =   :TAD,
            observations    =   :DV,
            amt             =   :AMT,
            route           =   :ROUTE)


auc0_72_df = NCA.auc(pop; interval = (0, 3*24))
auc0_648_df = NCA.auc(pop; interval = (0, 27*24))
aucinf_df = NCA.auc(pop)

This subject discontinued early. AUC0_72 and AUCinf are giving me the same results, and the AUC0_648 is adding values, despite no concentration data past 72 hrs. Both of these results appear to be bugs.

In an upcoming breaking release of NCA these computations will give much more sensible values.

using DataFrames
using NCA

df = DataFrame(
id            = 1,
time          = [0,    0.017, 2,    4,    8,    12,   24,   72],
concentration = [missing, 840.0, 1350, 1450, 1388, 1200, 1100, 399],
dose          = [15e6, missing, missing, missing, missing, missing, missing, missing],
route         = “ev”,
)

pop = NCA.read_nca_interleaved(df)

report = run_nca(pop, [
:auclast,
:aucinf_pred,
:auc_partial => (; interval = (0, 72))  => :auc_0_72,
:auc_partial => (; interval = (0, 648)) => :auc_0_648,
:auc_partial => (; interval = (0, Inf)) => :auc_0_inf,
])
julia> auclast_val  = report.auclast[1]
65606.525

julia> aucinf_val   = report.aucinf_pred[1]
86720.28433577472

julia> auc_0_72     = report.auc_0_72[1]
65606.525

julia> auc_0_648    = report.auc_0_648[1]
86427.74510262541

julia> auc_0_inf    = report.auc_0_inf[1]
86720.28433577472

Do these results match your expectations?
Areas past the last observation are based on extrapolating concentration values with an estimated terminal elimination rate, lambda_z.

Hi Arno,

Yes, that matches my expectation.

I have another bug to report. I’m not sure whether the NCA behavior has changed with the latest update, but in Pumas 2.8, it seems that partial AUC will now be missing if the concentrations are zero at the time of end of the interval. I’m setting all BLQ values to 0 concentration, and those subjects with trailing zero concentrations will have partial AUC that are missing.

The upcoming release will also have completely reworked AUC calculations.

Here is a trailing zero example:

julia> using NCA, DataFrames
df = DataFrame(
id = 1,
concentration = [0.0, 8.0, 5.0, 2.0, 0.5, 0.0, 0.0, 0.0, 0.0],
time = [0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 12.0, 16.0, 24.0],
dose = [100.0, fill(missing, 8)…],
route = “ev”,
)

   report = run_nca(
     NCA.read_nca_interleaved(df),
     [
       :tlast,
       :tfinal,
       :lambdaz,
       :auc_partial => (; interval = (0.0, 12.0)) => :auc_0_12,
       :auc_partial => (; interval = (0.0, 24.0)) => :auc_0_24,
       :auc_partial => (; interval = (12.0, 24.0)) => :auc_12_24,
     ],
   )
   
   println("auc_partial 0-12: ", report.auc_0_12[1])
   println("auc_partial 0-24:  ", report.auc_0_24[1])
   println("auc_partial 12-24: ", report.auc_12_24[1])

auc_partial 0-12: 12.25 auc_partial 0-24: 12.25 auc_partial 12-24: 0.0

Anything specific you want me to test?

Thank you, Arno. That’s perfect.

The only other issue with NCA I recall is with numerical issues as I posted a couple years ago here:

Has this been fixed?

Hi Donald,

We are working on this in the new version. The internal time - dose_time subtraction can introduce tiny floating-point differences (e.g., 99.9999 - 72.0 = 27.999899999999997 instead of 27.9999). Previously, this could flip interpolation to extrapolation, requiring lambda_z and producing missing when it wasn’t estimable. Now, we are considering a floating-point tolerance clamps these near-misses back, so your workaround of manually rounding TAD values should no longer be necessary.

using NCA, DataFrames

df = DataFrame(

  id = 1,

  concentration = [missing, 5.0, 2.0, 0.3],

  time = [72.0, 73.0, 74.0, 99.9999],

  dose = [100.0, missing, missing, missing],

  route = "ev",

)

df_r = NCA.read_nca_interleaved(df)




report = run_nca(

  df_r,

  [

:lambdaz,

:tfinal,

:auc_partial => (; interval = (0.0, 27.9999)) => :auc_at_tfinal,

:auc_partial => (; interval = (0.0, 28.001)) => :auc_beyond,

  ],

)

println("lambdaz: ", report.lambdaz[1])

println("tfinal:  ", repr(report.tfinal[1]))

println("auc_partial (0, 27.9999): ", report.auc_at_tfinal[1])

println("auc_partial (0, 28.001):  ", report.auc_beyond[1])
lambdaz: missing

tfinal:  27.999899999999997

auc_partial (0, 27.9999): 35.899885

auc_partial (0, 28.001):  missing

The 27.9999 endpoint is recognized as matching tfinal despite the floating-point mismatch. The 28.001 endpoint is genuinely beyond the data and correctly returns missing since lambda_z is not estimable.

1 Like