fix: fuse StandardScaler::transform to single-allocation pass - #450
Conversation
Replace per-column take_column/sub_scalar/div_scalar/build_matrix_from_columns pattern with a single M::fill + element-wise (x - mean) / std loop. Eliminates O(d) medium Vec allocations and several full-matrix temporaries that caused ~9500x wall-time regression and RSS inflation on large matrices (4000x4000: 85.9s → 0.009s per issue #449). Remove now-dead build_matrix_from_columns helper and its test. Add comprehensive test covering all parameter combinations (with_mean, with_std, zero-variance columns, column-count mismatch) verified against numpy.
- Extend guard to also reject zero-feature matrices (n_features == 0) - Improve error message to 'Training data must contain at least one sample and one feature.' - Add test_fit_on_zero_features_returns_error - Add comment on scaffold matrix in test_fit_on_empty_data_returns_error
Add n_samples == 0 || n_features == 0 guards to: - DecisionTreeClassifier::fit - BaseTreeRegressor::fit - RandomForestClassifier::fit - BaseForestRegressor::fit All return FailedError::ParametersError with message: 'Training data must contain at least one sample and one feature.' Previously these could panic (divide-by-zero, empty-range) or produce undefined models when called with zero-row or zero-column matrices. Regression tests added for each guarded path.
ReviewOverall this is a well-scoped, high-quality PR. The
|
Code ReviewThanks for this PR — the
|
| Item | Severity |
|---|---|
| Loop order (column-outer on row-major layout) | 🟡 Worth fixing before merge |
GradientBoosting* not guarded |
🟡 Follow-up issue |
DecisionTreeClassifier missing zero-features test |
🟡 Small gap |
BaseForestRegressor missing zero-features test |
🟡 Small gap |
| Error message consistency grep | 🟢 Trivial check |
| Test naming | 🟢 Non-blocking style |
The core change is correct and the performance improvement is dramatic. Loop order is the only item I'd suggest resolving before merge — everything else can land as-is or in a follow-up.
Swap fused transform loop from column-outer/row-inner to row-outer/ col-inner with pre-computed (mean, std) Vec. DenseMatrix uses row-major layout, so the previous ordering caused strided reads and writes on large matrices. Also add zero-features regression tests for DecisionTreeClassifier and BaseForestRegressor to match BaseTreeRegressor coverage. Audit: ExtraTreesRegressor and RandomForestRegressor both delegate to BaseForestRegressor::fit which already has the guard — no changes needed. Addresses review feedback from Mec-iS on PR #450.
Summary
Three focused fixes building on each other:
1. Fuse StandardScaler::transform (#449)
Replace per-column
take_column/sub_scalar/div_scalar/build_matrix_from_columnspattern with a singleM::fill+ row-outer/col-inner element-wise(x[i][j] - mean[j]) / std[j]loop. Column parameters pre-computed into aVec<(T, T)>outside the hot loop. Eliminates O(d) medium Vec allocations and several full-matrix temporaries that caused ~9500x wall-time regression and RSS inflation on large matrices (4000x4000: 85.9s -> 0.009s). Remove deadbuild_matrix_from_columnshelper. Bump to 0.6.13.2. Harden XGRegressor empty-data guard (#448 review)
Extend the
n_samples == 0guard to also rejectn_features == 0. Improve error message to "Training data must contain at least one sample and one feature." Addtest_fit_on_zero_features_returns_error.3. Guard all tree/ensemble fit methods against empty data
Add
n_samples == 0 || n_features == 0guards toDecisionTreeClassifier::fit,BaseTreeRegressor::fit,RandomForestClassifier::fit, andBaseForestRegressor::fit. Previously these could panic (divide-by-zero, empty-range) or produce undefined models. All returnFailedError::ParametersError.Audit:
ExtraTreesRegressor::fitandRandomForestRegressor::fitboth delegate toBaseForestRegressor::fitwhich already has the guard — no changes needed.Files changed
src/preprocessing/numerical.rs— fused row-major transform, removed dead code, comprehensive testsrc/xgboost/xgb_regressor.rs— extended guard, improved message, new testsrc/tree/decision_tree_classifier.rs— new guard + 2 tests (empty-rows + zero-features)src/tree/base_tree_regressor.rs— new guard + test module (2 tests)src/ensemble/random_forest_classifier.rs— new guard + testsrc/ensemble/base_forest_regressor.rs— new guard + 2 tests (empty-rows + zero-features)Cargo.toml— version bump to 0.6.13Verification
cargo test: 441 unit + 26 integration + 70 doctests all passcargo fmt --check: cleancargo clippy --all-features -- -Drust-2018-idioms -Drust-2024-compatibility -Dwarnings: clean