Fix crash in IV estimation when demeaning does not converge#639
Fix crash in IV estimation when demeaning does not converge#639adamaltmejd wants to merge 1 commit intolrberge:masterfrom
Conversation
When the demeaning algorithm does not converge (e.g. with varying slopes and weights), SSR can be NaN. The bare comparisons `my_res$ssr < 1e-10` then produce NA, causing `if (error_endo_no_variation || error_inst_no_expl)` to error with "missing value where TRUE/FALSE needed". Wrapping in isTRUE() lets execution continue so the existing convergence warning surfaces, consistent with the non-IV code path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes an IV estimation crash when the demeaning algorithm fails to converge and produces NaN/NA SSR values, aligning IV behavior with the non-IV path (warning instead of hard error).
Changes:
- Wrap IV first-stage SSR-based error checks in
isTRUE()to preventif()from receivingNA. - Apply the fix in both duplicated IV first-stage blocks (with and without fixed effects).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| # NaN ssr arises from demeaning non-convergence; isTRUE guards against NA in the if() | ||
| error_endo_no_variation = isTRUE(my_res$ssr < 1e-10) | ||
| error_inst_no_expl = isTRUE(abs(my_res$ssr - my_res$ssr_no_inst) < 1e-10) | ||
| if(error_endo_no_variation || error_inst_no_expl){ |
|
Hi Adam, I don't understand: $ssr should never be NA. Did you actually witness this case? |
|
Sorry already deleted the files setup that was creating it but I was running into a problem that seems to have been caused by it. Below are my initial debugging and here is Claude's answer:
#errors
feols(xpd(capital_income_t10_t14_log ~ i(field):parent_degree_in_j | prio + id_margin + id_round + ag_pooled[cutoff_distance, above_cutoff * cutoff_distance] | enrolled:i(field) +
enrolled:i(field):parent_degree_in_j~above_cutoff:i(field) +above_cutoff:i(field):parent_degree_in_j), data = dt, weights = w)
# NOTES: 50,831 observations removed because of NA values (LHS: 50,831).
# 1/0/0/0 fixed-effect singleton was removed (1 observation).
# Error in if (error_endo_no_variation || error_inst_no_expl) { :
# missing value where TRUE/FALSE needed
# also errors
# moving fe to coef
feols(xpd(capital_income_t10_t14_log ~ id_round + i(field):parent_degree_in_j | prio + id_margin + ag_pooled[cutoff_distance, above_cutoff * cutoff_distance] | enrolled:i(field) +
enrolled:i(field):parent_degree_in_j~above_cutoff:i(field) +above_cutoff:i(field):parent_degree_in_j), data = dt, weights = w)
# works
# without weights
feols(xpd(capital_income_t10_t14_log ~ i(field):parent_degree_in_j | prio + id_margin + id_round + ag_pooled[cutoff_distance, above_cutoff * cutoff_distance] | enrolled:i(field) +
enrolled:i(field):parent_degree_in_j~above_cutoff:i(field) +above_cutoff:i(field):parent_degree_in_j), data = dt)
# slightly fewer fixed effects
feols(xpd(capital_income_t10_t14_log ~ i(field):parent_degree_in_j | prio + id_margin + ag_pooled[cutoff_distance, above_cutoff * cutoff_distance] | enrolled:i(field) +
enrolled:i(field):parent_degree_in_j~above_cutoff:i(field) +above_cutoff:i(field):parent_degree_in_j), data = dt, weights = w)
# hints
# without iv it warns about non-convergence
feols(xpd(capital_income_t10_t14_log ~ above_cutoff:i(field) +above_cutoff:i(field):parent_degree_in_j + i(field):parent_degree_in_j | prio + id_margin + id_round +
ag_pooled[cutoff_distance, above_cutoff * cutoff_distance]), data = dt, weights = w)
# NOTES: 50,831 observations removed because of NA values (LHS: 50,831).
# 1/0/0/0 fixed-effect singleton was removed (1 observation).
# OLS estimation, Dep. Var.: capital_income_t10_t14_log
# Observations: 28,849
# Weights: w
# Fixed-effects: prio: 17, id_margin: 11, id_round: 60, ag_pooled: 30
# Varying slopes: cutoff_distance (ag_pooled): 30, above_cutoff * cutoff_distance (ag_pooled): 30
# Standard-errors: NA (not-available)
# Estimate Std. Error t value Pr(>|t|)
# above_cutoff:field::Agriculture NaN NaN NaN NA
# above_cutoff:field::Business NaN NaN NaN NA
# above_cutoff:field::Health NaN NaN NaN NA
# above_cutoff:field::Humanities NaN NaN NaN NA
# above_cutoff:field::Law NaN NaN NaN NA
# above_cutoff:field::Medicine NaN NaN NaN NA
# above_cutoff:field::Natural science NaN NaN NaN NA
# above_cutoff:field::Services NaN NaN NaN NA
# ... 25 coefficients remaining (display them with summary() or use argument n)
# ---
# Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# # Evaluations: lhs: 6713, rhs: 10000, 5603, 93, 34, 71, 62, 99, 73 -- tol: 1e-06, iter: 10000
# Warning messages:
# 1: In feols(xpd(capital_income_t10_t14_log ~ 0 + above_cutoff:i(field) + :
# There seems to be a convergence problem due to the presence of variables with varying slopes. The precision of the estimates may not be great. As a workaround, and if there are not
too many slopes, you can use the variables with varying slopes as regular variables using the function i (see ?i). Or use a lower 'fixef.tol' (sometimes it works)?
# 2: The demeaning algorithm did not converge, the results are not reliable. (tol: 1e-06, iter: 10000) |
|
I don't think the issue is the ssr. Non convergence should not imply NA coefficients or residuals, I think there's a bug in the demeaning step or right after it. Could you share this example? |
|
Make sense, I'll try with some more debugging. Data is PII and I can't use Claude directly so its not easy, but I'll see if i can generate a reprex... |
|
Ran some more diagnostics on the actual data (PII, can't share). 5 tests on the same data with different configurations: Full diagnostic log (fixest 0.14.1)What this tells us:
So the first-stage SSR shouldn't normally be NaN on its own — the NaN originates from the outcome's non-convergent demeaning propagating through the joint computations. The Generated with Claude Code |
Summary
my_res$ssrisNaN. The IV first-stage checksmy_res$ssr < 1e-10then produceNA, causing theif()to error with "missing value where TRUE/FALSE needed".isTRUE()lets execution continue so the existing convergence warning surfaces, consistent with the non-IV code path.estimation.R(with and without fixed effects) are both fixed.Note
Hard to reproduce without specific data that triggers demeaning non-convergence in the IV first stage — but the fix is minimal and safe (
isTRUE()is a no-op when the value is alreadyTRUE/FALSE).🤖 Generated with Claude Code