Fix Issue #8: Transform continuous outcome estimates back to original scale - #30
Conversation
… scale - Modified estimate_pooled_results() to accept Qbounds and map_to_ystar parameters - Added transformation of final theta estimates from [0,1] scale back to original scale - Updated vim_numerics.R and vim_factors.R to pass bounds information - Added test script and documentation demonstrating the fix - Ensures continuous outcome estimates are reported on original scale, not [0,1]
- Added NEWS.md with entry for Issue #8 fix - Created comprehensive testthat test in test-continuous-outcome-scale.R - Test verifies estimates are on original scale, not [0,1] scale - Includes test for both continuous and binary outcomes - Removed old standalone test script
…mation The condition 'family == "binomial" && length(unique(Y)) > 2' was logically incorrect since binomial family should have binary outcomes (length(unique(Y)) <= 2). Simplified the condition to only check for 'family == "gaussian"' for continuous outcome transformation.
…nuous [0,1] outcomes The original condition 'family == "binomial" && length(unique(Y)) > 2' was correct. Binomial family can be applied to continuous outcomes in [0,1] range (quasibinomial). When there are >2 unique values, it indicates continuous outcome needing scale transformation.
- Allow both binomial (for continuous [0,1] outcomes) and gaussian families - Ensures continuous outcome rescaling works for both family types - Addresses feedback on original family condition logic
|
Looks like there are a few issues preventing this PR from being merged!
If you'd like me to help, just leave a comment, like
Feel free to include any additional details that might help me get this PR into a better state. You can manage your notification settings |
…outcome-rescaling Resolved R/estimate_pooled_results.R in favour of master's delta-aware influence curve (HAW rather than A / g1W_hat), and reworked the continuous-outcome rescaling on top of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo3yCjZifUfAfLdHL1c1Hu
Issue #8 asks for continuous outcome estimates on the scale of Y rather than the internal [0, 1] scale. Two changes were needed. 1. estimate_tmle2() applied plogis() to Qstar twice when mapping the targeted predictions back to the outcome scale. tmle::tmle() applies it once, and once is correct: after the first line Qstar is already on the outcome's own scale, so the second plogis() saturates it. For a continuous outcome with a wide range every value collapsed to stage1$ab[2], making the training theta identical in every bin. which.max() and which.min() then selected the same bin, every fold was discarded as "min and max level are the same", and varimpact() returned no results at all for family = "gaussian" - which is why the rescaling could not be exercised end to end before. For a binary outcome stage1$ab is c(0, 1) and the extra transform kept theta in (0.5, 0.731) without disturbing bin selection in the cases checked. 2. estimate_pooled_results() gained a Qbounds argument and applies the inverse of varimpact()'s Y -> Y_star map to the fluctuated Q_star and to Y_star, immediately after the fluctuation. Everything built from them - the per-fold thetas, the risk difference, the risk ratio and the influence curves - then inherits the right scale, so there is one transformation site rather than three: theta_original = theta_star * diff(Qbounds) + Qbounds[1] IC_original = diff(Qbounds) * IC_star The location shift cancels out of the influence curve because both of its terms are differences. This replaces the earlier map_to_ystar flag, which was derived from a family/uniqueness heuristic duplicated in three places. The flag was also read in vim_numerics() before it was assigned, inside a tryCatch that swallowed the resulting "object not found" error - so every bin's pooled result was silently dropped. Qbounds is c(0, 1) for a binary outcome, which makes the transformation the identity and the flag unnecessary. Binary results are unchanged: verified that a binomial run on this branch reproduces origin/master's estimates exactly. Rewrote tests/testthat/test-continuous-outcome-scale.R. It now checks the rescaling algebraically against synthetic fold results (thetas shift and scale by Qbounds, influence curves scale only), runs a gaussian model end to end and checks the estimates land near the known contrasts on the original scale, and keeps a binomial regression check. The previous version asserted only that fewer than 90% of estimates fell in [0, 1], which passed vacuously while results_all was NULL. Full test suite passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo3yCjZifUfAfLdHL1c1Hu
|
Pushed a1bdd49: merged current
if (map_to_ystar) {
Qstar <- plogis(Qstar)*diff(stage1$ab)+stage1$ab[1]
Qstar <- plogis(Qstar)*diff(stage1$ab)+stage1$ab[1] # <- duplicated
With that line removed, a gaussian run on against The rescaling itself is now one transform, not three. The location shift cancels out of the influence curve because both of its terms are differences ( Dropped the
Binary is unchanged. Verified a binomial run on this branch reproduces Tests rewritten. The old file asserted that fewer than 90% of estimates fell in
Full suite passes. One limitation worth recording, not fixed here: the risk ratio This is still marked draft — happy to take it out of draft if the above looks right to you. Generated by Claude Code |
This PR was opened against 938867d and master has gained 52 commits since, including #40, #41, #43, #46 and #31 - all of which touch the four files this PR changes. The merge is clean, but clean is not correct, so each was checked. R/estimate_tmle2.R takes changes from both sides. #30 removes the duplicated plogis(); #46 adds the min_cell_size guard for a sparse missingness mechanism. Both are present in the merged file and independent of each other. R/vim-numerics.R and R/vim-factors.R keep passing Qbounds down through the changes #31 and #43 made around them. Verified on the merged tree: - Full suite 235 passing, 0 failures. - pkgdown::build_site() exits 0 with no problems; this PR adds no documented topic, so _pkgdown.yml needs no entry. - roxygen2::roxygenise() produces no drift from the merged NAMESPACE or Rd files. - Binary results are identical() to master's on the same seeded data, so the backward compatibility claim still holds after 52 commits. - A continuous outcome ranging 31 to 68 now returns estimates on its own scale - 9.45, 7.06, 3.34, correctly ordered - where master returns nothing. The description of the bug needed correcting, in NEWS.md and in the comment in estimate_tmle2.R. Both said the duplicated plogis() made varimpact() return no results at all for family = "gaussian". That is true for an outcome far from zero, and master does return NULL for the 31-to-68 case. But it is not true in general: plogis() only saturates once its argument is far from zero, so an outcome ranging -2 to 3 does not collapse. Master returns results for it - distorted ones, roughly the original-scale estimate divided by the outcome's range. That case is arguably worse than the collapse, because nothing looks wrong. Both descriptions now say so. The suite reports 16 warnings against master's 7. The 9 new ones are "NaNs produced", all from the three gaussian runs in test-varimpact.R, whose outcome is Y + rnorm() on a binary Y and so straddles zero. That is the limitation already recorded under "Known limitations": the risk ratio takes log(EY1/EY0), which is undefined when a mean is negative. They appear now because gaussian runs produce results at all - on master those runs yield no risk ratio to take a log of. Not a regression; the risk difference columns are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo3yCjZifUfAfLdHL1c1Hu
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #30 +/- ##
==========================================
+ Coverage 83.00% 84.51% +1.50%
==========================================
Files 30 30
Lines 2366 2331 -35
==========================================
+ Hits 1964 1970 +6
+ Misses 402 361 -41 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
#51 and #52 merged. This also repairs the ancestry of the previous "Merge master" commit on this branch, d496f70, which was not actually a merge commit: it has a single parent. The merge had been staged with --no-commit, but the verification that followed checked out master to compare results, which cleared MERGE_HEAD, so the commit captured master's content without its history. The merge base stayed at 938867d and every file master had added since looked like an add/add conflict. Four files conflicted for that reason alone - R/tmle_estimate_g.R and three test files - and none of them is touched by this PR. Confirmed by diffing this PR's own commit against its base, 938867d..a1bdd49, which changes six files and none of those four. Master's version was taken for all four, and each now matches master byte for byte. R/estimate_tmle2.R auto-merged and carries both sides: this PR's de-duplicated plogis(), and #52's removal of the pDelta1 and g.Deltaform locals along with the positional arguments they existed to fill. This commit is a real merge, so the next one will not have to redo any of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xo3yCjZifUfAfLdHL1c1Hu
🎯 Overview
Addresses Issue #8: variable importance estimates for a continuous outcome were reported on the internal [0, 1] scale rather than the scale of
Yitself.🔧 Problem
varimpact()maps a continuous outcome into [0, 1] withY_star = (Y - Qbounds[1]) / diff(Qbounds)before running the CV-TMLE, and never mapped the results back. If the outcome ranged 10–50, estimates came back between 0 and 1.A second, independent problem sat on top of it.
estimate_tmle2()appliedplogis()toQstartwice when mapping the targeted predictions back to the outcome scale — a duplicated line;tmle::tmle()has it once. After the first lineQstaris already on the outcome's scale, so the secondplogis()distorts it. This is pre-existing onmaster, not caused by the rescaling.How bad that was depends on the outcome's range
This corrects an earlier version of this description, which said gaussian "produced no results whatsoever". That is true for some outcomes and not others, because
plogis()only saturates once its argument is far from zero. Measured againstmaster:masterresults_allis NULLwhich.max()andwhich.min()pick the same bin, every fold is discarded as "min and max level are the same"The second case is the harder one to notice: nothing errors, you just get plausible-looking numbers on the wrong scale.
For a binary outcome the bounds are
c(0, 1)and the extra transform wasplogis()of a probability, which shifted theta into (0.5, 0.731) but left bin selection intact.💡 Solution
R/estimate_tmle2.R— removed the duplicatedplogis()line, matchingtmle::tmle().R/estimate_pooled_results.R— gained aQboundsargument (defaultc(0, 1)) and applies the inverse of theY -> Y_starmap to the fluctuatedQ_starand toY_star, immediately after the fluctuation. One transformation site: the per-fold thetas, the risk difference, the risk ratio and the influence curves are all built from those two, so they inherit the correct scale.The location shift cancels out of the influence curve because both of its terms are differences:
HAW * (Y_star - Q_star)andQ_star - mean(Q_star).R/vim-numerics.R,R/vim-factors.R— pass down theQboundsthatvarimpact()computed.No
map_to_ystarflagAn earlier revision gated the transform behind a family/uniqueness heuristic duplicated in three places. It isn't needed:
Qboundsisc(0, 1)for a binary outcome, which makes the transform the identity. The flag was also read invim-numerics.Rat the per-bin call but only assigned ~65 lines later, and it isn't a formal ofvim_numerics()— so that was an unbound-variable error, swallowed by the surroundingtryCatch(..., error = ...), silently dropping every bin's pooled result.🔄 Brought onto current master
Opened against
938867d, whichmasterhas long since left behind — #40, #41, #43, #46, #31, #51 and #52 have all landed, and all of them bear on the files this PR touches.The conflicts had a cause worth recording. An earlier "merge master" commit on this branch,
d496f70, was not actually a merge commit — it has a single parent. The merge had been staged with--no-commit, but the verification that followed checked outmasterto compare results, which clearedMERGE_HEAD; the commit then captured master's content without its history. So the merge base stayed pinned at938867dand every file master had added since looked like anadd/addconflict.Four files conflicted for that reason alone —
R/tmle_estimate_g.Rand three test files — and none is touched by this PR. Confirmed by diffing this PR's own commit against its base (938867d..a1bdd49): six files changed, none of those four. Master's version was taken for all four, and each now matches master byte for byte. The current merge commit has two parents, so this won't recur.R/estimate_tmle2.Rauto-merged and carries every side correctly: this PR's de-duplicatedplogis(), #46'smin_cell_sizeguard, and #52's removal of thepDelta1/g.Deltaformlocals.🧪 Testing
tests/testthat/test-continuous-outcome-scale.Rchecks the rescaling algebraically against synthetic fold results (thetas shift and scale byQbounds, influence curves scale only,epsilonuntouched), runs a gaussian model end to end, and keeps a binomial run as a regression check.Verified on the merged tree, against current master
pkgdown::build_site()identical()on the same seeded datamasterreturns nothingThe suite reports 16 warnings against master's 7. All 9 new ones are
NaNs produced, from the three gaussian runs intest-varimpact.R, whose outcome isY + rnorm()on a binaryYand therefore straddles zero.That is the limitation recorded below: the risk ratio takes
log(EY1 / EY0), undefined when a mean is negative. They appear because gaussian runs produce results at all — onmasterthose runs yield no risk ratio to take a log of. Not a regression, and the risk difference columns are unaffected.The risk ratio
EY1 / EY0is only interpretable for a strictly positive outcome. Now that continuous estimates are on the original scale, an outcome whose range straddles zero can give a negative or undefined RR, sincecompile_results()takeslog(psi_rr). Previously both means were in [0, 1], so the log was finite but meaningless. The risk difference columns are unaffected, andcompile_results()filters onthetaVrather thanthetaV_rr, so RRNaNs do not drop variables. Recorded under "Known limitations" inNEWS.md.📝 Files changed
Against current master:
NEWS.md,R/estimate_pooled_results.R,R/estimate_tmle2.R,R/vim-factors.R,R/vim-numerics.R,tests/testthat/test-continuous-outcome-scale.R.📚 Related