fix(xgboost): keep one row when subsample truncates the sample to zero (#444) - #445
Conversation
smartcorelib#444) floor(n_samples * subsample) rounds down to 0 on a small dataset, so the tree was fit on an empty index set and 0..sorted_idxs.len() - 1 underflowed. Keep a minimum of one row, as scikit-learn does. Sample sizes of one row or more are unchanged.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #445 +/- ##
===========================================
+ Coverage 43.97% 63.93% +19.95%
===========================================
Files 85 95 +10
Lines 7281 8207 +926
===========================================
+ Hits 3202 5247 +2045
+ Misses 4079 2960 -1119 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
thanks 🙏 i will look into this asap |
Review: ✅ Approve with minor notesThe fix is correct, minimal, and safe. The one-line change in The Core Fix// Before
indices.truncate((population_size as f64 * subsample_ratio) as usize);
// After
indices.truncate(((population_size as f64 * subsample_ratio) as usize).max(1));This is correct and complete. The Test CoverageThe four tests cover the important boundaries well:
One gap worth noting: there is no test for Minor Suggestions
None of these block merging. Great fix overall — clean, targeted, and well-tested. |
Second Pass ReviewHaving looked more carefully at the diff, a few additional observations beyond my first comment. Correctness of the fix in context of
|
…447) * chore(xgboost): apply #445 review follow-ups to the subsample clamp - Document that the one-row minimum holds for a non-empty dataset - Add debug_asserts for the population_size and subsample_ratio bounds - Assert the single-row fit moves the prediction towards the target - Separate adjacent tests with a blank line - Trim the 0.6.12 changelog bullet to user-visible behaviour * fix(svm): stop panics escaping MultiClassSVC Result APIs Predict and fit propagated internal unwraps to callers through Result-returning methods: - PredictorBorrow::predict delegates to the inherent predict, which already returns Failed errors. - MultiClassSVC::fit propagates multiclass_fit failures with ?. - predict returns PredictFailed when called before fit, e.g. on a deserialized model with no classifiers. - predict returns PredictFailed instead of panicking when fit saw fewer than two classes. * refactor(svm): enumerate all Kernels variants in setter matches with_gamma, with_degree, and with_coef0 matched non-applicable kernels with a catch-all arm. A future kernel variant holding these fields would silently ignore the setters. Each setter now lists every variant explicitly. Also marks Kernels #[must_use]. * chore: mark library functions and parameter types #[must_use] clippy::must_use_candidate flagged 149 callables whose results are meaningless to discard: parameter structs and their with_* builders, dataset loaders, metric constructors, error constructors, and linalg factory methods. The 49 *Parameters/*Params config types gain a type-level #[must_use], which covers every builder method returning Self. The remaining library functions gain fn-level #[must_use]. Kernels was already covered in the previous commit.
Description
XGRegressor::fitpanics withattempt to subtract with overflowwhensubsampleis small enough that the sampled index set comes out empty.sample_without_replacementsizes the sample as(population_size as f64 * subsample_ratio) as usize, which floors to 0. Three rows atsubsample = 0.3does it, and so does a single row at any ratio below 1.0.find_best_splitthen runs0..sorted_idxs.len() - 1on an empty slice.The guard in
fitonly rejectssubsample > 1.0andsubsample <= 0.0, so this input gets through.Fixes #444
Current behaviour
New expected behaviour
The sample keeps a minimum of one row, so the fit succeeds and returns a usable model. This is what scikit-learn does for its own
subsampleparameter (max(1, int(subsample * n_samples))insklearn/ensemble/_gb.py).I went this way rather than returning
Failed::because(FailedError::ParametersError, ...)becausesubsampleis a regularisation hyperparameter, not a statement about the data. The parameter is valid on its own; only the pair(subsample, n_samples)is degenerate. Erroring would mean asubsamplethat works on the full dataset starts failing insidecross_validateas k-fold shrinks each fold, which is a surprising place to hit it.Sample sizes of one row or more are byte-identical to before.
.max(1)only engages when the floor is 0, andshuffleconsumes the same RNG state either way.Tests
Four tests, in the module's own
mod tests. Three fail before this change and pass after: 3 rows at 0.3, 1 row at 0.9, and a table over the boundary. The fourth is the regression guard, checking that ratios which already yield one or more rows are untouched:(10, 0.8) -> 8,(100, 0.8) -> 80, indices unique and in range.(2, 0.4)and(2, 0.5)sit either side of the floor boundary, so the pair pins the clamp rather than just testing small numbers.Lib tests go 428 to 432.
Change logs
CHANGELOG.mdgets a### Fixedbullet under a new## [0.6.12], andCargo.tomlis bumped to match, following #443.