Repository navigation
[SC-19150] Add a linear regression validation use case to the ValidMind Library - #578
Conversation
Adds a quickstart notebook that develops and validates a linear regression model end to end, the tests a validator needs for it, and the data behind it. - 18 new tests for linear regression models: specification (VIF, linearity, influential observations, robust standard errors, coefficient stability, expected signs), assumptions (residual normality, heteroscedasticity), uncertainty (prediction intervals, performance confidence intervals), feasible prediction range, sensitivity and stress scenarios, bias and weak spots by segment, calibration, tail performance, and performance over time. - Synthetic credit card balance dataset, with the script that generates it. - Linear regression documentation template next to the notebook. - Fix: RegressionCoeffs drew its confidence-interval bars from the bounds rather than their distance from the coefficient, and always used a normal 95% interval. - Fix: RegressionModelSummary failed on scikit-learn 1.6 and later. - Fix: generating test descriptions could hang in Jupyter when a test produced several Plotly figures; figures are now rendered before the parallel description jobs start. - The regression quickstart now tells users to apply the Regression template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RegressionCoeffs: the coefficient table shows intervals at the chart's confidence level instead of always 95%. - FeatureSensitivity: numeric columns a formula wraps in C() are not shocked. - ResidualBiasBySegment, RegressionWeakspots: missing values form their own segment instead of failing. - ExpectedCoefficientSigns: works on models fitted on numpy arrays. - ScenarioAnalysis rejects a scenario named "Baseline"; PerformanceOverTime reports rows without a period as "Missing"; TailPerformance keeps each tail at its share when values tie. - Say how ResidualNormality and FeatureSensitivity differ from the existing residual and sensitivity tests; fix the sensitivity_analysis tag on RegressionModelSensitivityPlot. - List credit_card_balance and california_housing in the regression datasets package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ResidualNormality: on SciPy 1.17 and later, take the Anderson-Darling p-value from SciPy instead of the critical values it deprecates and removes in 1.19; older SciPy keeps the critical-value comparison. - LinearitySpecification: silence the divide warning lowess raises on regressors with few distinct values; the smooth it returns is finite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cachafla
left a comment
There was a problem hiding this comment.
@juanmleng thanks for this. The fixes to existing tests are correct: the RegressionCoeffs error bars are now distances rather than bounds, RegressionModelSummary works on recent scikit-learn, and the Regression template link in the existing quickstart is fixed. All 50 tests in the template match tests the notebook runs, and the notebook outputs are cleared.
Requesting changes for the new tests below. Each bug was reproduced with a short script against 49bebf44. They mostly show up on inputs the bundled dataset never hits: missing values, models fitted on numpy arrays, and models already fitted with robust errors.
Bugs
validmind/tests/model_validation/statsmodels/statsutils.py:67-71(design_data): models fitted on plain numpy arrays (sm.OLS(y.values, sm.add_constant(X.values))) get regressor namesx1, x2, which aren't dataset columns.df[exog_names]then raisesKeyError, breakingPredictionIntervals,FeatureSensitivityandScenarioAnalysis. The docstring says array models are supported, but only array models fitted on a DataFrame work.validmind/tests/model_validation/statsmodels/PredictionIntervals.py:83-90: for formula models,get_predictionsilently drops rows with a missing regressor, butdataset.ykeeps every row. The test then fails with a shape-mismatchValueError(e.g. 500 vs 300 rows).validmind/tests/model_validation/statsmodels/RobustStandardErrors.py:64-67: the "OLS" errors come fromresults.bse, which are already robust if the model was fitted with.fit(cov_type="HC3"). Every ratio is then 1.0 and the test always passes, which is the case it should catch. The classical errors should be computed explicitly, e.g.get_robustcov_results("nonrobust")orresults.model.fit().validmind/tests/model_validation/statsmodels/CoefficientStability.py:76-82: when a bootstrap resample contains no rows for a rare dummy variable,lstsqreturns 0 for its coefficient, andnp.sign(0)is counted as a sign flip. A dummy covering 2 of 500 rows fails the stability threshold even though its sign never flips, and the zero draws also pull the confidence interval toward 0. Suggest skipping rank-deficient draws, or counting only non-zero draws.validmind/tests/model_validation/Heteroscedasticity.py:104-110: numeric features go straight intohet_breuschpagan/het_whitewithout dropping missing values. Any NaN in a feature raisesMissingDataError. That matters for models that handle NaN natively, such asHistGradientBoostingRegressor.validmind/tests/model_validation/FeatureSensitivity.py:104-111:std,minandmaxare computed without skipping NaN, so a feature with any missing value gets its whole shocked column set to NaN. The table then shows "± nan" and ranks that feature as highly influential.np.nanstd/np.nanmin/np.nanmax(and leaving NaN cells unshocked) would fix it._is_binaryalso treats a 0/1 column with NaNs as non-binary.
Smaller issues
validmind/tests/model_validation/_segments.py:42-49: segment bins are grouped by their display label, which is rounded to 4 significant digits. Large-valued numeric columns such asYYYYMMDDdates end up with every bin under the same label (2.024e+07 – 2.024e+07), so one "segment" holds the whole dataset. This affectsResidualBiasBySegmentandRegressionWeakspots. Grouping by the bin itself and formatting the label only for display fixes it.validmind/tests/model_validation/statsmodels/VarianceInflationFactor.py:94-101vs145-147: the status label uses the unrounded VIF but pass/fail uses the rounded value. A VIF of 9.996 shows as "10.0 / Moderate" yet the test fails.validmind/tests/model_validation/TailPerformance.py:74-77: floating-point error inn * tail_sharemakes the two tails different sizes; n=100 withtail_share=0.29gives 29 top rows and 28 bottom.round(n * tail_share)fixes it.validmind/tests/model_validation/statsmodels/ExpectedCoefficientSigns.py:108-112: an emptyexpected_signsraisesKeyError: 'Result'instead of a clear error.
Notes
validmind/ai/test_descriptions.py:223-229: rendering the figures before the background jobs start is reasonable. The cached image would go stale if a figure were changed afterwards, but nothing does that today.- Unit tests: with the stats extra installed, the 61 statsmodels tests and 52 tests for the other new checks pass.
make test-unitfails on a default environment withNo module named 'statsmodels'unless the stats extra is installed, so CI may need that too.
…ression tests - Match the regressors of models fitted on numpy arrays to the dataset's feature columns by position (PredictionIntervals, FeatureSensitivity, ScenarioAnalysis) - PredictionIntervals: align predictions by row label and exclude rows with missing values instead of failing or counting them as covered - RobustStandardErrors: compare against a plain OLS refit, so a model fitted with robust errors is still assessed - CoefficientStability: skip resamples that cannot estimate every coefficient - Heteroscedasticity: exclude rows with missing feature values - FeatureSensitivity: ignore missing values when sizing shocks and detecting binary features - Segment labels keep one label per bin for large values - VarianceInflationFactor: status and pass/fail use the same rounded value - TailPerformance: both tails have the same size - ExpectedCoefficientSigns: clear error for empty expected_signs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cachafla thanks for the careful review, and for the repro scripts. I reproduced all ten and fixed them in 6f4b2cc. Each fix has a unit test that fails without it. Bugs
Smaller issues
Notes
|
cachafla
left a comment
There was a problem hiding this comment.
@juanmleng thanks. I re-ran my reproduction scripts against 6f4b2ccd, and 9 of the 10 points are fixed. The PR's unit tests pass (116 passed with the stats extra). Your CI note checks out: uv sync --all-extras installs statsmodels.
Still open
2. PredictionIntervals with missing regressors (validmind/tests/model_validation/statsmodels/PredictionIntervals.py:84-90). The crash is gone, but predictions are now paired with the wrong rows. After dropping NaN rows, get_prediction(...).summary_frame() returns a positional index 0..k-1, not df's labels. frame.reindex(df.index) therefore shifts every prediction onto an earlier row and marks the last rows as missing.
- Default index: 400 rows with
xmissing on the first 50. The PR reportsObservations 350, Excluded 50, Coverage 0.434. With predictions kept on their own rows, coverage is 0.954. - Non-default index: for example the shuffled index
train_test_splitreturns, which the notebook'spreprocessuses. Any NaN regressor makes every row miss the reindex, so the test skips with "found no rows without missing values". - Duplicated index (e.g. after
pd.concat): the result showsExcluded 0and coverage 0.40.
test_rows_with_a_missing_regressor_are_excluded only checks the counts (300/200), so it doesn't catch this. One option is to build the design matrix with patsy.dmatrix(results.model.data.design_info, df, return_type="dataframe"), which keeps df's labels on the rows it keeps, then predict on it and align by that index. Either way, the test should assert coverage against a hand-computed value, and include a non-default index.
Fixed (verified on 6f4b2ccd)
- Numpy-fitted array models:
PredictionIntervalsandpredict_framenow work. A model with a different number of regressors thanfeature_columnsskips with a clear message. RobustStandardErrors: an HC3-fitted model now gives the same ratios as a plain fit (1.00 / 1.52 / 1.08).CoefficientStability: the rare dummy now has sign stability 1.0, with no zero draws.Heteroscedasticity: it runs with NaN features (1949 of 2000 observations used).FeatureSensitivity: the shock is finite, the table no longer shows "± nan", and the NaN feature isn't ranked as influential.
- Segment labels: the
YYYYMMDDcolumn now gives 5 distinct segments. - VIF: 9.996 shows as 10.0 / Severe / Fail, so the label and pass/fail agree.
TailPerformance: with n=100, both tails are 29 rows at 0.29 and 7 rows at 0.07.ExpectedCoefficientSigns: an emptyexpected_signsraises a clearValueError.- Notes: acknowledged; no action needed.
…s are missing get_prediction numbers the rows it keeps 0..k-1, so reindexing by the dataset's labels shifted predictions onto other rows. Build the formula design with patsy, which keeps row positions, and align on positions so shuffled or duplicated indexes cannot misalign either. The test now checks coverage against a hand-computed value for default, shuffled and duplicated indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cachafla thanks for checking again, you're right. |
cachafla
left a comment
There was a problem hiding this comment.
@juanmleng thanks, verified on 312bda26. All ten points from my review are now addressed, and from my side this is ready for approval.
2. PredictionIntervals with missing regressors: fixed. Building the formula design with patsy.dmatrix on row positions keeps each prediction on its own row. On my repro (400 rows, 50 with x missing), coverage is now 0.954 with 50 rows excluded. That matches the coverage computed by hand on the complete rows (0.954); 6f4b2ccd reported 0.434. Shuffled and duplicated indexes give the same result, for both formula and array models. The updated test_rows_with_a_missing_regressor_are_excluded now checks coverage, not just counts.
The other nine points were verified on 6f4b2ccd (see my previous review), and 312bda26 only touches PredictionIntervals and its test. The PR's unit tests pass: 116 passed with the stats extra.
cachafla
left a comment
There was a problem hiding this comment.
LGTM, thanks @juanmleng! Verified on 312bda2: all review points addressed, and CI is green.
Pull Request Description
What and why?
Adds a quickstart for documenting and validating a linear regression model end to end, and the tests a validator needs for it.
How to test
make test-unit, or the new files undertests/unit_tests/model_validation/andtests/unit_tests/model_validation/statsmodels/.notebooks/use_cases/regression/linear_regression_template.yamlto a model, then runnotebooks/use_cases/regression/quickstart_linear_regression.ipynbtop to bottom. Expect no cell errors and every template section populated.What needs special review?
RegressionCoeffs,RegressionModelSummary,validmind/ai/test_descriptions.py), then the shared helpers (_segments.py,statsutils.py), the statsmodels tests, the model-agnostic tests, and finally the template and notebook.run_testslightly slower.Dependencies, breaking changes, and deployment notes
Release notes
New linear regression use case: a quickstart notebook and template for documenting and validating a linear regression model, 18 new tests for linear regression models, and a synthetic credit card balance dataset. Also fixes the confidence-interval bars in
RegressionCoeffsand aRegressionModelSummaryfailure on scikit-learn 1.6 and later.Checklist