Skip to content

Make SymPy factory distributions picklable - #445

Open
alejandroschuler wants to merge 1 commit into
masterfrom
fix/sympy-factory-pickle
Open

alejandroschuler wants to merge 1 commit into
masterfrom
fix/sympy-factory-pickle

Conversation

@alejandroschuler

Copy link
Copy Markdown
Collaborator

Problem

A model that uses a distribution made with the SymPy factory (#403) cannot be saved. This applies to Beta, BetaBernoulli, BetaBinomial, BetaBinomialEstN, LogitNormal and any distribution that a user builds with make_distribution:

>>> pickle.dumps(NGBRegressor(Dist=Beta).fit(X, Y))
PicklingError: Can't pickle <class 'ngboost.distns.sympy_utils.Beta'>: attribute lookup Beta on ngboost.distns.sympy_utils failed

make_distribution and make_sympy_log_score build their classes with type(). Every generated class therefore reports ngboost.distns.sympy_utils as its module, where no class of that name exists. Pickle, joblib and skops find a class by its module and name, so all three fail. Releases 0.5.9, 0.5.10 and 0.5.11 have this bug. The pickling tests did not find it, because they use only Normal, Bernoulli, LogNormal and MultivariateNormal.

I checked each way to save or copy a model, for each factory distribution, on master and on this branch:

master this branch
pickle / joblib of a fitted model fails works
skops of a fitted model fails works
pickle of pred_dist(X) fails works
copy.deepcopy, sklearn.clone works works
cross_val_score(n_jobs=2) works (loky uses cloudpickle) works

Fix

The factories now set __module__ of each generated class to the module that calls them, as collections.namedtuple and enum.Enum do. A new module argument overrides it. The class is then found as ngboost.distns.beta.Beta, or as my_module.MyBeta for a user's own distribution. The docs now tell users to give name the name of the variable that holds the class.

Tests

tests/test_pickling.py now does these checks for every factory distribution, and for a distribution built in the test module itself:

  • It pickles the class and a predicted distribution.
  • It pickles fitted models.
  • It saves one fitted model with skops.

Without the fix, all 10 new tests fail. With it, the full suite passes (pytest --slow: 179 passed, 2 xfailed, 22 xpassed).

Not in this PR

  • pickle.dumps(pred_dist) also fails for the hand-written distributions such as Normal, with a RecursionError from __getattr__. Add mu and sigma to LogNormal and raise AttributeError for unknown names #433 already fixes that.
  • LogitNormal.predict is a Monte Carlo mean of 1000 draws. Two calls on the same model can therefore give different values (up to 0.05 apart in my check). For this reason the new model test compares the predicted parameters, not predict().

🤖 Generated with Claude Code

make_distribution and make_sympy_log_score built their classes with
type(), so every generated class reported ngboost.distns.sympy_utils as
its module. Pickle, joblib and skops find a class by its module and
name, so they failed for any model or predicted distribution that used
Beta, BetaBernoulli, BetaBinomial, BetaBinomialEstN, LogitNormal or a
user's own factory distribution.

The factories now set __module__ to the module that calls them, as
collections.namedtuple does, and take a module argument to override it.
The new tests pickle each factory class, a predicted distribution of
each, and fitted models (pickle and skops), including a distribution
built in the test module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant