Repository navigation
Deprecate the var attribute of the Normal and T distributions - #439
Open
alejandroschuler wants to merge 2 commits into
Open
alejandroschuler wants to merge 2 commits into
alejandroschuler wants to merge 2 commits into
Conversation
Normal, NormalFixedVar, NormalFixedMean, T, TFixedDf, TFixedDfFixedVar and the two Cauchy classes stored the squared scale in an attribute named var. These classes forward unknown names to the scipy.stats object through __getattr__, which runs only when normal lookup fails, so the attribute hid the var() method of scipy. dist.var() raised TypeError on these classes, while mean(), std() and the var() of LogNormal worked. The attribute is now _var, and the scores read _var. dist.var() reaches scipy, and the scores, gradients and metrics are bitwise unchanged. The regression experiment uses forecast.scale, and the SymPy demo notebook calls var(). Code that reads dist.var as an array now gets a method. A deprecation path that also makes var() work now would need var to be an array and a callable at once. That object breaks code that checks callable(), such as DataFrame.assign, misses the warning for many array uses, and for the T classes holds values that differ from what the call returns. So the release note marks the change as breaking and gives dist.scale ** 2 as a replacement that works on every version. Found while assessing #168. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first commit on this branch renamed the var attribute to _var at once. That broke code that reads dist.var as an array, and a code search found such code in BayBE, boost-loss, dionysus, NodeFlow, TreeFlow and other projects. So the maintainer chose to warn for one release first. var is now a property on Normal, T, TFixedDf and TFixedDfFixedVar, and the other Normal and Cauchy classes inherit it. It returns the same array as before and gives a FutureWarning that names the line of the caller. The message says that var will become the var() method of scipy.stats and that dist.scale ** 2 gives the current values. For the T and Cauchy classes it also says that the value is the squared scale, not the variance. One function in distn.py makes the property, so the release that removes it deletes that function and four lines that call it. The scores still read _var, so nothing in ngboost gives the warning. The SymPy demo notebook now uses hw.scale ** 2, which works in both releases. dist.var() raises TypeError until the property goes. The tests that need it stay as strict xfails, as a record of the behavior after the removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Found while assessing #168 (see #433).
This PR first renamed the attribute in one breaking step. The maintainer decided to warn for one release first, because the code search below found downstream projects that read
dist.varas an array. The PR now does that.Problem
Normal,NormalFixedVar,NormalFixedMean,T,TFixedDf,TFixedDfFixedVar,CauchyandCauchyFixedVarsetself.varto the squared scale in__init__. These classes give unknown names to thescipy.statsobject inself.distthrough__getattr__. Python calls__getattr__only when normal lookup fails, so the array hides thevar()method of scipy:mean(),std()and the other scipy methods work on these classes, andvar()works onLogNormaland the other distributions. For the T and Cauchy classes the attribute was also not the variance. It wasscale ** 2, and the variance of the t distribution isscale ** 2 * df / (df - 2).Change
The scores now read a private array
_var, with the same values as the oldvar. The Cauchy classes reuse the T classes and scores, socauchy.pydoes not change.varis a property that returns_varand gives aFutureWarning.deprecated_var()indistn.pymakes the property, andNormal,T,TFixedDfandTFixedDfFixedVareach have one linevar = deprecated_var(...). The other classes inherit it. Withstacklevel=2, the warning names the line of the user that readsvar. The messages follow theLogNormal.locandLogNormal.scalewarnings in Add mu and sigma to LogNormal and raise AttributeError for unknown names #433:Normal.var is deprecated. In a future release, var will be the var() method of scipy.stats, as for the other distributions. Use dist.scale ** 2 for the current values.T.var is deprecated. It is the squared scale, not the variance. In a future release, var will be the var() method of scipy.stats, as for the other distributions. Use dist.scale ** 2 for the current values.The class name in the message is the class of the object, for example
NormalFixedVar.varorCauchy.var.Nothing in ngboost reads
var, so fitting, scoring,pred_distand pickling give no warning.examples/experiments/regression_exp.pyusedforecast.var * rmseas the scale of aNormalFixedVarforecast.forecast.varwas all ones, the same asforecast.scale, so the script now usesforecast.scale.notebooks/sympy_normal_demo.ipynbnow useshw.scale**2in place ofhw.var. This works in this release and after the removal.I searched
ngboost/,tests/,examples/,notebooks/,figures/anddocs/, including the code cells of the notebooks. There are no other readers of the attribute.dist.var()still raisesTypeErrorin this release, because the property hides the scipy method. Until the removal,dist.dist.var()gives the variance.Why warn first
gh search codefound this third-party code that reads.varas an array on an ngboost distribution. All of it usesNormal.baybe/surrogates/ngboost.py)torch.from_numpy(dists.var), withngboost>=0.3.12,<1src/boost_loss/sklearn.py)predict_varreturnsdist.varnp.sqrt(y_dists.var)dist.var.reshape(-1, 1)mean, var = dist.loc, dist.varnp.sqrt(dist.var)BayBE is the largest of these, with about 520 stars, and its pin allows the next ngboost release. With an immediate rename, its NGBoost surrogate would raise
TypeErroron that release. With this PR, all of this code keeps working and shows the warning, which tells the authors to usedist.scale ** 2. That works on old and new versions. A comment in Snowmmmm/RL_Agent says thatvaron an ngboost distribution is an attribute and not a method, which is the confusion this change removes. No hit called.var()on an ngboost distribution. ucgmsim/ml_vs30_model defines its own distribution with its ownvar, and this change does not affect it. The search found no subclass of these classes that assignsself.var. Such code would now raiseAttributeError, because the property has no setter.The search gives a lower bound. The code search API indexes default branches only and ignores punctuation, and some of my queries hit its rate limit.
Alternatives
dist.varfromdist.var(), so the only way to makedist.var()work in this release and keepdist.varas an array is anndarraysubclass with a__call__. I built a prototype and rejected it.df.assign(var=dist.var)calls the object with the DataFrame and raisesTypeError.v.reshape(-1, 1),v[0],np.asarray(v)andnp.concatenategive no warning. For the T and Cauchy classes, the array would holdscale ** 2and the call would return the variance.The removal release
deprecated_varfromdistn.py,T_VAR_NOTEfromt.py, and the fourvar = deprecated_var(...)lines._varstays as the array that the scores read.dist.var()then reaches scipy through__getattr__, asmean()does.tests/test_distn_var.py, deletetest_var_warns_and_returns_squared_scaleand remove thexfailmark fromtest_future_var_is_scipy_var. The mark is strict, so the suite fails until someone removes it. I checked this in a scratch copy: without the property lines, the 8 cases pass, so they show as strict XPASS failures.dist.varbecomes a method. For the T and Cauchy classes,dist.var()gives the variance, not the squared scale:inffor df in (1, 2] andnanfor Cauchy.Compatibility
score,d_score,gradand, where it exists,metricon 50 random parameter vectors are bitwise identical to master, withFutureWarningas an error.Normal(LogScoreandCRPScore),NormalFixedMean,T,TFixedDfandCauchymodels on master and loaded them on this branch. They load and predict with noFutureWarning,locandscaleare identical, andpred.vargives oneFutureWarningand the same array as the master attribute. I did not add a pickle made on master to the test suite, because it would depend on the scikit-learn version. The suite tests a pickle round trip withFutureWarningas an error.__getattr__of each class and adds one toDistn. Thevarproperty is on the class, so normal lookup finds it before either__getattr__. After the removal,Distn.__getattr__forwardsvarto scipy, becausevaris indir(self.dist). I applied Add mu and sigma to LogNormal and raise AttributeError for unknown names #433 on top of this branch. With--slowandFutureWarningfrom ngboost as an error,test_distn_var.py,test_score.py,test_distn_attributes.pyandtest_pickling.pygive 103 passed, 1 skipped (skops is not installed) and 8 xfailed.deprecated_varis at the end ofdistn.py, away from the__getattr__that Add mu and sigma to LogNormal and raise AttributeError for unknown names #433 adds.NormalFixedVar.__init__andNormalFixedMean.__init__innormal.py, andparamsint.py. This PR changes one line in each of those two__init__methods, so a merge can need a one-line fix there.Example
pred.varpred.var()scale ** 2TypeErrorscale ** 2, with aFutureWarningTypeErrorpred.scale ** 2gives the same array in all three, with no warning.Tests
tests/test_distn_var.py(new):test_var_warns_and_returns_squared_scale, for each of the 8 classes: readingvargives oneFutureWarningwith the file and line of the test, the message has the class name, the scipy note anddist.scale ** 2, the T and Cauchy messages also have the squared-scale note, and the value isscale ** 2as before. These 8 fail on master, which gives no warning.test_ngboost_gives_no_var_warning, for each class withLogScoreand the Normal classes withCRPScore(11 cases): with everyFutureWarningas an error, it evaluatestotal_scoreandgradof the scores, fits anNGBRegressor, callspred_dist, and pickles and loads the model. These pass on master too. I checked that they catch an internal read: when I changed one line ofNormalLogScoreto readself.var, the test failed.test_future_var_is_scipy_var, for each class:var()equalsdist.dist.var()andstd() ** 2. These 8 record the behavior after the removal. They are markedxfail(raises=TypeError, strict=True)with the reason "dist.var() works once the deprecated var attribute is removed", and a comment above them says to remove the mark with the attribute.tests/test_score.py: no test checked theLogScoreofNormalFixedVarandNormalFixedMean, and this PR changes its lines. Both are now inTEST_METRIC, sotest_dists_gradchecks their gradients and the slowtest_dists_metricchecks their metrics. These 4 tests pass on master and on this branch.test_dists_gradcases. The 2 new skips are the slow metric cases, and the 8 xfails are the future tests.test_score.pyandtest_distn_var.pywith--slowgive 73 passed and 8 xfailed.FutureWarningfrom ngboost as an error: 155 passed, 61 skipped, 8 xfailed. The command waspytest tests -W error::FutureWarning:ngboost -o "filterwarnings=error::FutureWarning:ngboost". The-opart is necessary. pytest matches the module of a command-line-Wfilter exactly, so-W error::FutureWarning:ngboostalone catches only warnings from the modulengboostitself and not fromngboost.distns.normal. I checked this with the same plantedself.varread: the-Wflag alone let the test pass, and the-ofilter made it fail.black,isort,flake8andpylintare clean on the changed files, with the options in.pre-commit-config.yaml.notebooks/sympy_normal_demo.ipynbwithnbconvert --execute. It gives noFutureWarning, and 13 of 14 code cells print the same text as the stored outputs. The other cell shows a figure size of 1500x400 in place of 1080x288, because of the figure DPI of the environment. That cell does not usevar, and the run of the previous commit gave the same difference.🤖 Generated with Claude Code