More consistent handling of constraints in glex(): $shap and $remainder - #39
Merged
Conversation
Collaborator
Author
|
Turns out it's a bit more complex, because a) Multiclass, once again, makes everything annoying ... anyway, the |
$shap to NA if glex() is called with constraintsglex(): $shap and $remainder
Collaborator
Author
|
This now also goes a bit further and adds the $remainder term in a more principled way to also apply to the xgboost (and any other, actually), thereby superseding the part of #25 I was in favor of. |
This was referenced Jul 18, 2026
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.
Constraining a decomposition via
max_interactionorfeaturesdrops terms. Thecomponents no longer sum to the prediction, so SHAP values reconstructed from them
violate efficiency. Previously they were returned anyway.
Supersedes #18 and #25, closes #13 and #11.
$shapisNAwhen the decomposition is constrainedPreviously the same call warns about nothing, has no
$constrained, and returnsplausible-looking SHAP values (
-0.201 -0.189 1.083 ...) that are silently wrong.Complete decomposition:
A constraint that only drops zero terms is not a constraint:
New
$remainderWhat the dropped terms are collectively worth. Present exactly when
$constrainedisnon-empty, so the prediction reconstructs either way:
rpfobjects already carried a$remainderfrompredict_components(); the otherlearners had none. Now one field, one definition, computed in one place.
On the scale the model is decomposed on, the link scale for xgboost:
Unlike #25, this covers classification and other non-identity links. #25 computed
predict(object, x) - rowSums(m)— a response-scale prediction minus a margin-scaledecomposition — and had to restrict itself to regression.
glex()onrpfnow also returns$shap(class-suffixed for multiclass, like$m),and detects post-hoc constraints, which previously passed silently.
Bug fixes
glex.rpf()compared against the wrong scale and the wrong class. rpf decomposes theraw score;
predict()defaults totype = "prob", which applies rpf's response function(clamp to
[0, 1]forloss = "L2", inverse link for"logit"/"exponential").The inertness check averaged real terms away.
all.equal()reports mean relativedifference, so a discrepancy concentrated in a few observations passed as "all zero",
glex()kept SHAP values that were wrong. Caught by a macOS CI failure. Now judgedelementwise against
$remainder, at the noise floor.Tests
test-constrained-remainder.R: the$remainderinvariant across xgboost, ranger andrpf; both xgboost link scales (
binary:logistic,count:poisson); all three rpf losses.Includes negative assertions that reconstruction must not match the response scale, so
the target cannot be quietly reverted.
test-rpf-sum-identity.R: was comparingrowSums(m)(raw score) againstpredict()'sclamped probabilities, which is why it needed
tolerance = 0.03/0.8and amin()over both class columns. Againsttype = "numeric"the identity is exact(~1e-15). Multiclass now asserts the residual is constant within a class — the class
intercept rpf does not report — instead of merely bounding its magnitude.
given fit. It did on Linux, not on macOS (4e-4). Now uses a constant predictor, which
cannot be split on, so its terms are zero by construction on every platform.
weighting_methodonly applies where glex walks the treesitself (xgboost, ranger).
glex.rpf()ignores it, so the "fastpd" and "path-dependent"variants were asserting the same thing twice.