Give each regression member its own target pipeline [RES-2639] - #1198
Give each regression member its own target pipeline [RES-2639]#1198bejaeger wants to merge 2 commits into
Conversation
Preparation for moving the target transform later in the pipeline, with no intended change in behaviour. The regressor z-normalised the target before handing it to the ensemble preprocessing. Everything downstream then had to be expressed relative to that: a member's target transform received standardized values, and its `inverse_transform` was expected to return standardized values so that the bar-distribution borders and the sanity limits applied to them stayed in z-units. Each member now owns one invertible pipeline from the target, in its own units, to the target the model is fitted on, and standardizes the target itself. `y_train_mean_`/`y_train_std_` keep only the role they always had for `raw_space_bardist_`: the affine frame the ensemble is aggregated in and decoded from. The estimator owns that frame and applies it to the borders after the inverse, so nothing is baked into a pipeline when it is built and nothing has to be rebound when it is refitted on another split. Aggregation stays in the frame of the checkpoint rather than moving to the target's own units, deliberately: `translate_probs_across_borders` resolves positions within a bucket in float32, and for a target with a large offset the borders in original units are spaced below float32 resolution at that magnitude. The sanity limits keep the same frame, and therefore their meaning of "this many standard deviations of the target". The frame reaches the two other border-mapping sites explicitly: per dataset in `predict_batched`, where the refitted worker's own attributes only hold the last dataset's, and through `RegressorBatch` for fine-tuning, where `fit_from_preprocessed` never sees the target and the attributes were previously never set at all. Neutrality: `StandardizeTarget` reproduces the estimator's former arithmetic exactly, so every model input is bit-identical. Over 64 configurations (5 targets including a 1e8-offset and a 1e-6-scale one, 4 model versions, 1/2/8 estimators, float64 inference, plus `predict_batched` and three non-default target transforms), 32 are bit-identical and the worst relative deviation is 2.4e-5, on a 0.9 quantile of a heavy-tailed target. It comes from a transformed member's borders now being narrowed to the dtype they arrived in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d5e41e0. Configure here.
| znorm_space_bardist=znorm_space_bardist_, | ||
| X_query_raw=x_test_raw, | ||
| y_query_raw=y_test_raw, | ||
| y_train_mean=float(train_mean), |
There was a problem hiding this comment.
Finetuning uses unfitted target pipelines
Medium Severity
RegressorBatch still takes configs=list(conf) — the preprocessor template list — while this refactor makes every member carry a fitted target_transform that border mapping must use. With n_preprocessing_jobs > 1, those transforms are fitted only on the worker copies returned on ensemble_members; the template list stays unfitted. The same path in predict_batched already switches to [m.config for m in members] for that reason, so finetuning now hits AttributeError or wrong borders on the parallel path that previously skipped mapping when target_transform was None.
Additional Locations (1)
Triggered by learned rule: Use executor_.ensemble_members configs, not ensemble_configs_, for fitted transforms
Reviewed by Cursor Bugbot for commit d5e41e0. Configure here.
| return Pipeline( | ||
| steps=[ | ||
| # The preset reshapes the standardized target, as it always has; | ||
| # the ordering of these two steps is what RES-2639 changes. |
There was a problem hiding this comment.
Comment narrates ticket history
Low Severity
The step-order comment describes what RES-2639 will change and how the ordering “always has” worked, which narrates ticket history rather than stating current behavior only. Team convention is that comments and docstrings document the present contract, not planned or prior arrangements.
Triggered by learned rule: Docstrings describe current behavior only — no call sites or code history
Reviewed by Cursor Bugbot for commit d5e41e0. Configure here.


Refactor for RES-2639, no intended behaviour change. PR 2 of this stack does the actual ticket and should be read after this one.
Why
The regressor z-normalised the target before handing it to the ensemble preprocessing, and everything downstream was then expressed relative to that: a member's target transform received standardized values, and its
inverse_transformhad to return standardized values, so that the bar-distribution borders and the±1e3sanity limits applied to them stayed in z-units. Reordering the transform under that arrangement forces the frame's statistics to be baked into every pipeline when it is built, and rebound whenever it is refitted on a different split.What
Each member now owns one invertible pipeline from the target, in its own units, to the target the model is fitted on, and standardizes the target itself.
y_train_mean_/y_train_std_keep only the role they always had forraw_space_bardist_: the affine frame the ensemble is aggregated in and decoded from. The estimator owns that frame and applies it to the borders after the inverse, so nothing is baked into a pipeline and nothing needs rebinding.The frame now reaches the two other border-mapping sites explicitly:
predict_batched— per dataset. The worker is refitted per dataset, so its own attributes only ever hold the last one's; a new test with targets three orders of magnitude apart pins this.RegressorBatch.fit_from_preprocessednever sees the target, andy_train_mean_/y_train_std_were previously never set on that path at all, so a member with a target transform would have hit anAttributeErroronce the border mapping needed them.Aggregation deliberately stays in the frame of the checkpoint rather than moving to the target's own units.
translate_probs_across_bordersresolves positions within a bucket in float32; for a target with a large offset (1e8 with σ≈1e2) the borders in original units are spaced below float32 resolution at that magnitude and would collapse into duplicates. Keeping that frame also keeps the sanity limits meaning "this many standard deviations of the target" — as an absolute bound in the target's own units they would reject every border of any large-magnitude target.Neutrality
StandardizeTargetreproduces the estimator's former arithmetic (np.mean,np.std + 1e-20) exactly, so every model input is bit-identical — verified directly on the fitted members, not just at the output.Predictions were compared against
mainover 64 configurations: 5 targets (including a 1e8-offset one and a 1e-6-scale one), 4 model versions, 1/2/8 estimators, float64 inference, pluspredict_batchedand three non-default target transforms.medianand the quantiles, 8e-8 formeanThe residual comes from a transformed member's borders now being narrowed to the dtype they arrived in; the frame round trip itself runs in float64, because in float32 its two halves cancel and cost about six digits of the borders.
Incidentally fixes a latent inconsistency: a row-subsampled estimator's target was normalised with the whole training set's statistics while its borders were mapped back as if the normalisation had been its own.
Tests
New
tests/test_preprocessing/test_target_transform.py, plus regressor-level tests for the bit-identical member target, frame independence from the target's units, and the per-dataset frame inpredict_batched. Full local suite passes (1639 tests).🤖 Generated with Claude Code