Repository navigation
Conversation
Two related numerical stability fixes in the ZTNB EM fitting loop: 1. Initialize new_inner_logL <- -Inf before each ZTNB EM while-loop (initfit_fn and update_Xcoefsspp_fn). The variable is only assigned inside the loop body after a successful gam() fit. If gam() fails and breaks on the first iteration, new_inner_logL is never created, causing 'object new_inner_logL not found' when referenced after the loop. 2. Cap EM augmentation weights (initw) at 100 after each E-step in three locations. initw = P(Y=0)/P(Y>0) diverges to Inf when the NB model assigns near-certainty to Y=0, causing the augmented gam() to produce degenerate estimates. A cap of 100 (~99% posterior probability) is already an extremely strong signal and prevents instability with no effect on well-behaved fits.
fhui28
added a commit
that referenced
this pull request
Apr 27, 2026
Owner
|
Bug 1 has been resolved. I will think about bug 2 a bit more |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi Francis - you might remember the missing 'innerlogL' error that I receive sporadically - this is an attempt at addressing it. I want to be certain that you think this is actually a reasonable thing to do - in particular the capping of infinite probabilty values. And similarly that you think that the selected cap value is reasonable (if indeed you think that the overall approach is reasonable at all). Details below:
Two related numerical stability fixes in the ZTNB EM fitting loop:
Initialize new_inner_logL <- -Inf before each ZTNB EM while-loop (initfit_fn and update_Xcoefsspp_fn). The variable is only assigned inside the loop body after a successful gam() fit. If gam() fails and breaks on the first iteration, new_inner_logL is never created, causing 'object new_inner_logL not found' when referenced after the loop.
Cap EM augmentation weights (initw) at 100 after each E-step in three locations. initw = P(Y=0)/P(Y>0) diverges to Inf when the NB model assigns near-certainty to Y=0, causing the augmented gam() to produce degenerate estimates. A cap of 100 (~99% posterior probability) is already an extremely strong signal and prevents instability with no effect on well-behaved fits.
Background: How CBFM Fits the ZTNB
The zero-truncated negative binomial (ZTNB) distribution has no native
gam()family inmgcv, so CBFM fits it via an EM algorithm that exploits the relationship between the
zero-truncated NB and the full NB. The key insight is that ZTNB data can be viewed as full NB data where zeros were generated
but never observed. The EM algorithm "fills in" those missing zeros iteratively:
E-step: For each positive observation, compute
initw— the posterior odds ratioP(Y=0) / P(Y>0)under the current NB parameters. This represents how many "missing"zeros should be imputed per observed positive. For example, if the model estimates
P(Y=0) = 0.8at some covariate combination, theninitw = 0.8/0.2 = 4: four missingzeros are imputed for every observed positive there.
M-step: Fit a standard
gam(..., family = nb())to an augmented dataset of2nrows — the original observations stacked on top of
nsynthetic zeros — usinginitwas weights on the imputed zero rows. This is equivalent to maximising the ZTNB likelihood
on the positive observations alone.
Iterate until convergence.
This EM loop appears in two functions:
initfit_fn— computes per-species starting values before the outer PQL loopupdate_Xcoefsspp_fn— performs per-species coefficient updates within each PQL iterationBug 1: Uninitialized Variable
new_inner_logLSymptom
Root Cause
Inside the ZTNB EM loop, the
gam()call at each M-step is wrapped intry(). If thiscall fails (e.g. due to a degenerate species — sparse positive counts, no intercept in the
formula, or extreme observation weights), the code executes
breakto exit the loop early.The variable
new_inner_logLis only assigned inside the loop body, after a successfulgam()fit. If the loop breaks on the very first iteration,new_inner_logLis nevercreated. The code immediately after the loop then attempts:
Possible Fix?
Initialize
new_inner_logL <- -Infimmediately before each ZTNB EMwhileloop. Thisensures the variable always exists when the post-loop code runs. A value of
-Infis thecorrect sentinel: it signals that no valid log-likelihood was obtained, allowing downstream
code to handle the failure gracefully (e.g. falling back to random starting values in
initfit_fn).Locations — search strings to find the insertion points:
initfit_fncw_inner_logL <- -Inf(first ZTNB EM block)update_Xcoefsspp_fncw_inner_logL <- -Inf(ZTNB EM block)Change at each location:
Behavioral impact: None on the normal code path. Only affects the failure case, where
previously the code crashed;
Bug 2: Infinite EM Augmentation Weights (
initw)Symptom
The augmented
gam()inside the ZTNB EM loop hangs, producesNaNcoefficients, orcauses downstream numerical failures. No explicit error message — the fit silently produces
garbage or the outer PQL loop diverges.
Root Cause
The E-step weight is:
This is
P(Y=0) / P(Y>0). When the NB model assigns near-certainty toY=0at aparticular covariate combination (i.e.
P(Y=0) → 1), this ratio diverges toInf.An
Infweight in the augmentedgam()means one synthetic zero row has infiniteinfluence, completely dominating the M-step fit. The IRLS solver inside
gam()thenproduces degenerate or
NaNcoefficient estimates.This becomes more likely when:
a wide range of means, so
initwis large for many observationspulls the fitted mean lower, further increasing
P(Y=0)andinitwborderline stable without observation weights tips over to
Infwith themFix
Cap
initwat a finite maximum immediately after each E-step computation:A cap of 100 means the augmented
gam()sees at most 100 synthetic zeros per observedpositive — corresponding to a posterior probability of ~99% that the observation "should
not exist." This is already an extremely strong signal; allowing
initwto exceed thisprovides no meaningful information and introduces severe numerical instability.
The cap value of 100 was chosen conservatively (higher than strictly necessary) to avoid
any effect on well-behaved fits. In practice,
initw > 20is already rare;initw > 100essentially never occurs except in the degenerate cases this fix is designed to handle.
Locations — search strings to find the insertion points:
initfit_fninitw <- dnbinom(0, mu = exp(cw_eta), size = fit0$family$getTheta(TRUE)) / (1 - dnbinom(...(before EM loop)initfit_fninitw <- dnbinom(0, mu = exp(cw_eta), size = 1/fit0$dispparam) / (1 - dnbinom(...(inside EM loop)update_Xcoefsspp_fninitw <- initw / (1 - initw)(beforeinitw <- initw[find_nonzeros])Change at each location:
Behavioral impact: None on well-behaved fits where
initwis finite and reasonable.Only affects cases where
initwwould have beenInfor very large, converting a silentnumerical failure into a stable (if imprecise) fit.
Relationship Between the Two Bugs
Both bugs share a common upstream cause: the
gam()call inside the ZTNB EM loopencountering degenerate inputs. Bug 2 (
initw = Inf) is one specific mechanism that cancause the
gam()to fail, which then triggers Bug 1 (new_inner_logLuninitialized) whenthe loop breaks early. In practice:
augmented
gam()completes but with degenerate weights)initwtoInfFixing both together provides defense in depth: Bug 2's fix prevents the degenerate input;
Bug 1's fix ensures graceful failure even if some other pathway produces a degenerate
gam()in the future.Summary of Changes
initfit_fnnew_inner_logL <- -Infbefore ZTNB EM loopupdate_Xcoefsspp_fnnew_inner_logL <- -Infbefore ZTNB EM loopinitfit_fninitw <- pmin(initw, 100)after first E-stepinitfit_fninitw <- pmin(initw, 100)after second E-step (inside loop)update_Xcoefsspp_fninitw <- pmin(initw, 100)after E-stepTotal: 5 single-line insertions, no deletions, no changes to function signatures or
existing logic.