Skip to content

Include dropped first column in monotonic constraints - #1023

Closed
Matthias Schmidtblaicher (MatthiasSchmidtblaicherQC) wants to merge 6 commits into
mainfrom
fix/monotonic-dropped-first-basis
Closed

Matthias Schmidtblaicher (MatthiasSchmidtblaicherQC) wants to merge 6 commits into
mainfrom
fix/monotonic-dropped-first-basis

Conversation

@MatthiasSchmidtblaicherQC

Copy link
Copy Markdown
Contributor

Closes #1021.

When the first column of a monotonic term is dropped, its coefficient is implicitly zero, but only the remaining coefficients were ordered. This happens for bs() without include_intercept=True and for categoricals with drop_first=True. If the first remaining coefficient had the wrong sign, the fitted effect was not monotone, even though coef_ looked ordered.

This PR adds one row per group to the constraints, sign * c_first >= 0, for such terms. For the example in #1021, an "increasing" constraint on decreasing data now gives a flat fit, as expected.

Changes:

  • _formula.py: detect dropped first columns (default bs() calls and reduced-rank factors) and add the extra constraint.
  • New test checking that predictions are monotone (not only coefficients) with an intercept, for both bs() and C() with drop_first=True.
  • Document monotonic_constraints in the GeneralizedLinearRegressor and GeneralizedLinearRegressorCV docstrings.
  • Formula tutorial: update the wording and add a plot of the fitted BonusMalus effect, constrained vs unconstrained. The plot shows that ordered coefficients are sufficient but not necessary for a monotone spline, so the constraint can bind even when the unconstrained fit is already monotone.

The monotonic benchmark (glum vs pygam) is unchanged: it uses cr(), which keeps all columns.

A related bug found while working on this, string categorical levels being sorted alphabetically, is tracked separately in #1022.

Checklist

  • Added a CHANGELOG.rst entry

bs() without include_intercept and categoricals with drop_first lack their
first column, whose coefficient is implicitly zero. Constrain the first
remaining coefficient against it so the fitted effect is monotone.

Also document monotonic_constraints in the estimator docstrings and plot the
fitted effect in the formula tutorial.

Closes #1021
Comment thread src/glum/_formula.py Outdated


def _drops_first_basis(factor_name: str) -> bool:
"""Whether ``factor_name`` is a ``bs()`` call without ``include_intercept``."""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind expanding a bit on the docstring or inline comments? I would like to understand in plain english what the conditions are doing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I updated the docstring and also made the body a bit more legible. Unfortunately, there are still quite a number of checks necessary to catch all cases.

@MarcAntoineSchmidtQC

Copy link
Copy Markdown
Member

I didn't like the fragile parsing of this PR. I looked at how other libraries dealt with this issue and it seems that passing a flag through tabmat was almost all it took. See #1025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

monotonic_constraints on bs() with an intercept does not give a monotone fitted effect

2 participants