From 96d887f7925f44f6f1a027f909dd117a63f6f191 Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Fri, 21 Aug 2026 16:41:00 +0200 Subject: [PATCH 1/7] Apply regression target transforms to the unnormalized target [RES-2639] Apply the preset before standardization so log1p receives raw targets. Resolve the none preset to standardization only and pin the ordering, inverse mapping, and finite-input behavior with regression tests. --- changelog/1199.changed.md | 1 + src/tabpfn/preprocessing/target_transform.py | 6 +- src/tabpfn/preprocessing/transform.py | 3 +- src/tabpfn/regressor.py | 2 +- .../test_target_transform.py | 35 ++++++++-- tests/test_regressor_interface.py | 67 +++++++++++++++++++ 6 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 changelog/1199.changed.md diff --git a/changelog/1199.changed.md b/changelog/1199.changed.md new file mode 100644 index 000000000..a7dcdc330 --- /dev/null +++ b/changelog/1199.changed.md @@ -0,0 +1 @@ +Regression target transforms now act on the target in its original units, before standardization. This fixes `1_plus_log` producing non-finite model inputs for positive targets whose z-scores fall below -1. Predictions can change for ensembles using non-identity target transforms, including `safepower`; `None` and `"none"` retain standardization only. diff --git a/src/tabpfn/preprocessing/target_transform.py b/src/tabpfn/preprocessing/target_transform.py index 53fc2d669..e7cdba200 100644 --- a/src/tabpfn/preprocessing/target_transform.py +++ b/src/tabpfn/preprocessing/target_transform.py @@ -49,7 +49,7 @@ def inverse_transform(self, X: np.ndarray) -> np.ndarray: def make_target_transform(transform: Transformer | Pipeline | None) -> Pipeline: - """Standardize raw targets, then apply the optional preset. + """Apply the optional preset to raw targets, then standardize. The inverse returns values in the target's original units. """ @@ -57,9 +57,9 @@ def make_target_transform(transform: Transformer | Pipeline | None) -> Pipeline: return Pipeline(steps=[(STANDARDIZE_STEP, StandardizeTarget())]) return Pipeline( steps=[ - # The preset reshapes the standardized target. - (STANDARDIZE_STEP, StandardizeTarget()), + # Reshape the raw target before standardizing it for the model. (TARGET_TRANSFORM_STEP, transform), + (STANDARDIZE_STEP, StandardizeTarget()), ], ) diff --git a/src/tabpfn/preprocessing/transform.py b/src/tabpfn/preprocessing/transform.py index d593ef423..2e3b91299 100644 --- a/src/tabpfn/preprocessing/transform.py +++ b/src/tabpfn/preprocessing/transform.py @@ -95,7 +95,8 @@ def _transform_labels_one( Args: config: Ensemble config. - y_train: The unprocessed labels. + y_train: Encoded class indices or regression targets. A configured + regression target pipeline expects targets in their original units. Return: The processed labels. """ diff --git a/src/tabpfn/regressor.py b/src/tabpfn/regressor.py index a888b944f..2288b90bb 100644 --- a/src/tabpfn/regressor.py +++ b/src/tabpfn/regressor.py @@ -1024,7 +1024,7 @@ def _initialize_dataset_preprocessing( target_preprocessors: list[Pipeline] = [ make_target_transform( None - if y_target_preprocessor is None + if y_target_preprocessor in (None, "none") else possible_target_transforms[y_target_preprocessor] ) for y_target_preprocessor in ( diff --git a/tests/test_preprocessing/test_target_transform.py b/tests/test_preprocessing/test_target_transform.py index 3ffdbb84d..aa17d4e80 100644 --- a/tests/test_preprocessing/test_target_transform.py +++ b/tests/test_preprocessing/test_target_transform.py @@ -71,18 +71,43 @@ def test__make_target_transform__without_a_transform_only_standardizes() -> None @pytest.mark.parametrize("name", TRANSFORM_NAMES) -def test__make_target_transform__transform_sees_the_standardized_target( +def test__make_target_transform__transform_sees_the_unnormalized_target( name: str, ) -> None: - """The preset receives standardized targets.""" + """The preset reshapes the raw target before standardization.""" y = _target() transform = _get_transform(name, len(y)) got = make_target_transform(transform).fit_transform(y.reshape(-1, 1)).astype(float) - standardized = (y - np.mean(y)) / (np.std(y) + StandardizeTarget.EPSILON) - expected = _get_transform(name, len(y)).fit_transform(standardized.reshape(-1, 1)) - np.testing.assert_allclose(got, np.asarray(expected, dtype=float), rtol=1e-12) + reshaped = np.asarray( + _get_transform(name, len(y)).fit_transform(y.reshape(-1, 1)), dtype=float + ) + expected = (reshaped - reshaped.mean()) / ( + reshaped.std() + StandardizeTarget.EPSILON + ) + np.testing.assert_allclose(got, expected, rtol=1e-10) + + +def test__make_target_transform__log_of_the_target_is_defined() -> None: + """Positive raw targets stay finite even when their z-scores fall below -1.""" + rng = np.random.default_rng(0) + y = rng.normal(100.0, 10.0, size=500) # symmetric, strictly positive + + standardized = (y - np.mean(y)) / np.std(y) + assert not np.isfinite(np.log1p(standardized)).all() + + got = ( + make_target_transform(_get_transform("1_plus_log", len(y))) + .fit_transform(y.reshape(-1, 1)) + .ravel() + ) + + assert np.isfinite(got).all() + expected = np.log1p(y) + np.testing.assert_allclose( + got, (expected - expected.mean()) / expected.std(), rtol=1e-10 + ) @pytest.mark.parametrize("name", TRANSFORM_NAMES) diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index 10056d39f..5f7b57fc7 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -1500,6 +1500,73 @@ def test__fit__member_target_is_the_znormalized_target() -> None: assert np.array_equal(member_y, (y - np.mean(y)) / (np.std(y) + 1e-20)) +def _mk_skewed_reg_dataset( + seed: int, n: int = 200, f: int = 5 +) -> tuple[np.ndarray, np.ndarray]: + """A strictly positive, right-skewed target -- the log-transform case.""" + r = np.random.RandomState(seed) + X = r.randn(n, f) + y = np.exp(1.5 + X @ np.array([1.0, -0.5, 0.3, 0.0, 0.2]) + 0.3 * r.randn(n)) + return X, y + + +def test__fit__target_transform__applied_to_the_unnormalized_target() -> None: + """Apply log1p to raw targets before standardizing model inputs.""" + X, y = _mk_skewed_reg_dataset(0) + reg = TabPFNRegressor( + n_estimators=1, + device="cpu", + random_state=42, + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("1_plus_log",)}, + ) + reg.fit(X, y) + + expected = np.log1p(y) + expected = (expected - expected.mean()) / expected.std() + member_y = np.asarray(reg.executor_.ensemble_members[0].y_train) + + np.testing.assert_allclose(member_y, expected, rtol=1e-8) + + +def test__fit__none_preset_is_resolved_to_no_transform() -> None: + """`"none"` is the identity, so it must not cost an extra pass.""" + X, y = _mk_skewed_reg_dataset(0) + reg = TabPFNRegressor( + n_estimators=1, + device="cpu", + random_state=42, + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("none",)}, + ) + reg.fit(X, y) + + pipeline = reg.executor_.ensemble_members[0].config.target_transform + assert list(pipeline.named_steps) == ["standardize_target"] + assert np.array_equal( + np.asarray(reg.executor_.ensemble_members[0].y_train), + (y - np.mean(y)) / (np.std(y) + 1e-20), + ) + + +@pytest.mark.parametrize("transform", ["1_plus_log", "log", "safepower"]) +def test__predict__target_transform__predictions_are_finite_and_accurate( + transform: str, +) -> None: + """Target transformation and inverse border mapping preserve prediction quality.""" + X, y = _mk_skewed_reg_dataset(0) + X_train, X_test, y_train, y_test = X[:150], X[150:], y[:150], y[150:] + reg = TabPFNRegressor( + n_estimators=2, + device="cpu", + random_state=42, + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": (transform,)}, + ) + reg.fit(X_train, y_train) + pred = reg.predict(X_test) + + assert np.isfinite(pred).all() + assert r2_score(y_test, pred) > 0.7 + + @pytest.mark.parametrize("dtype", ["float32", "float64"]) def test__fit__target_frame_is_independent_of_the_target_units(dtype: str) -> None: """Rescaling the target changes its frame but preserves standardized inputs.""" From d9b54212fa351ea15b926052e333f04f0b8f8cee Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 30 Sep 2026 15:02:09 +0200 Subject: [PATCH 2/7] fix: standardize regression targets before safepower --- src/tabpfn/regressor.py | 9 +++++++- tests/test_regressor_interface.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/tabpfn/regressor.py b/src/tabpfn/regressor.py index 2288b90bb..3e7c36f5d 100644 --- a/src/tabpfn/regressor.py +++ b/src/tabpfn/regressor.py @@ -38,6 +38,7 @@ check_is_fitted, clone, ) +from sklearn.pipeline import Pipeline from tqdm.auto import tqdm from tabpfn.architectures.shared.bar_distribution import FullSupportBarDistribution @@ -124,7 +125,6 @@ if TYPE_CHECKING: import numpy.typing as npt - from sklearn.pipeline import Pipeline from torch.types import _dtype from tabpfn.architectures.interface import ( @@ -1020,6 +1020,13 @@ def _initialize_dataset_preprocessing( num_examples=y.shape[0], # Use length of validated y random_state=random_state, # Use the provided rng ) + # Yeo-Johnson depends on units and offset; fit it on standardized targets. + possible_target_transforms["safepower"] = Pipeline( + steps=[ + ("standardize_input", StandardizeTarget()), + ("safepower", possible_target_transforms["safepower"]), + ] + ) # Each member fits its target pipeline on raw targets. target_preprocessors: list[Pipeline] = [ make_target_transform( diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index 5f7b57fc7..89237c138 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -1528,6 +1528,41 @@ def test__fit__target_transform__applied_to_the_unnormalized_target() -> None: np.testing.assert_allclose(member_y, expected, rtol=1e-8) +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("offset", [0.0, 1e6]) +def test__fit__safepower_standardizes_before_fitting( + dtype: type[np.floating], offset: float +) -> None: + X, y = _mk_skewed_reg_dataset(0) + y = (y + offset).astype(dtype) + reg = TabPFNRegressor( + n_estimators=1, + device="cpu", + random_state=42, + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("safepower",)}, + ) + reg.fit(X, y) + + preset = reg.executor_.ensemble_members[0].config.target_transform.named_steps[ + "target_transform" + ] + standardized = (y - float(np.mean(y))) / (float(np.std(y)) + 1e-20) + np.testing.assert_array_equal( + preset.named_steps["standardize_input"].transform(y.reshape(-1, 1)).ravel(), + standardized, + ) + expected = regressor_module.get_all_reshape_feature_distribution_preprocessors( + num_examples=len(y), random_state=42 + )["safepower"].fit_transform(standardized.reshape(-1, 1)) + np.testing.assert_array_equal(preset.transform(y.reshape(-1, 1)), expected) + pipeline = reg.executor_.ensemble_members[0].config.target_transform + np.testing.assert_allclose( + pipeline.inverse_transform(pipeline.transform(y.reshape(-1, 1))).ravel(), + y, + rtol=1e-5, + ) + + def test__fit__none_preset_is_resolved_to_no_transform() -> None: """`"none"` is the identity, so it must not cost an extra pass.""" X, y = _mk_skewed_reg_dataset(0) From 061d770ae351147e5f6e0219fda8ece933dd2feb Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 30 Sep 2026 15:13:51 +0200 Subject: [PATCH 3/7] docs: clarify preserved safepower behavior in changelog --- changelog/1199.changed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/1199.changed.md b/changelog/1199.changed.md index a7dcdc330..2867bb17e 100644 --- a/changelog/1199.changed.md +++ b/changelog/1199.changed.md @@ -1 +1 @@ -Regression target transforms now act on the target in its original units, before standardization. This fixes `1_plus_log` producing non-finite model inputs for positive targets whose z-scores fall below -1. Predictions can change for ensembles using non-identity target transforms, including `safepower`; `None` and `"none"` retain standardization only. +Regression target transforms now act on targets in their original units before final standardization. This fixes `1_plus_log` producing non-finite model inputs for positive targets whose z-scores fall below -1. The regression `safepower` preset retains input standardization, preserving its previous behavior apart from numerical differences and member-specific statistics when subsampling. `None` and `"none"` retain standardization only. From 92db9d35abcc6d7f571c8bab58a9e5472e13cdc4 Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 28 Oct 2026 13:43:39 +0100 Subject: [PATCH 4/7] fix: validate log targets and warn about exponential transforms --- changelog/1199.changed.md | 2 +- src/tabpfn/regressor.py | 34 +++++++++++++++++++----- tests/test_regressor_interface.py | 43 ++++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/changelog/1199.changed.md b/changelog/1199.changed.md index 2867bb17e..3e494f8f9 100644 --- a/changelog/1199.changed.md +++ b/changelog/1199.changed.md @@ -1 +1 @@ -Regression target transforms now act on targets in their original units before final standardization. This fixes `1_plus_log` producing non-finite model inputs for positive targets whose z-scores fall below -1. The regression `safepower` preset retains input standardization, preserving its previous behavior apart from numerical differences and member-specific statistics when subsampling. `None` and `"none"` retain standardization only. +Regression target transforms now act on raw targets before final standardization. Both `power` and `safepower` retain input standardization; `None` and `"none"` retain standardization only. Invalid `log` and `1_plus_log` target domains now raise clear errors during fitting, and `exp` warns about overflow and precision loss. diff --git a/src/tabpfn/regressor.py b/src/tabpfn/regressor.py index 3e7c36f5d..95eac24b9 100644 --- a/src/tabpfn/regressor.py +++ b/src/tabpfn/regressor.py @@ -1020,13 +1020,35 @@ def _initialize_dataset_preprocessing( num_examples=y.shape[0], # Use length of validated y random_state=random_state, # Use the provided rng ) + for preset in self.inference_config_.REGRESSION_Y_PREPROCESS_TRANSFORMS: + if preset == "log" and np.any(y <= 0): + raise ValueError( + "The 'log' regression target transform requires strictly positive " + "raw targets (y > 0). Use 'safepower' or None for " + "nonpositive targets." + ) + if preset == "1_plus_log" and np.any(y <= -1): + raise ValueError( + "The '1_plus_log' regression target transform requires raw targets " + "greater than -1. Use 'safepower' or None for targets <= -1." + ) + if preset == "exp": + warnings.warn( + "The 'exp' regression target transform acts on raw targets and can " + "overflow, underflow, or lose distinctions between target values " + "during standardization. Predictions may be invalid even when " + "fit succeeds. Prefer 'safepower' or None for numerical stability.", + UserWarning, + stacklevel=2, + ) # Yeo-Johnson depends on units and offset; fit it on standardized targets. - possible_target_transforms["safepower"] = Pipeline( - steps=[ - ("standardize_input", StandardizeTarget()), - ("safepower", possible_target_transforms["safepower"]), - ] - ) + for preset in ("power", "safepower"): + possible_target_transforms[preset] = Pipeline( + steps=[ + ("standardize_input", StandardizeTarget()), + (preset, possible_target_transforms[preset]), + ] + ) # Each member fits its target pipeline on raw targets. target_preprocessors: list[Pipeline] = [ make_target_transform( diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index 89237c138..8dc87a1cb 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -1530,8 +1530,9 @@ def test__fit__target_transform__applied_to_the_unnormalized_target() -> None: @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize("offset", [0.0, 1e6]) -def test__fit__safepower_standardizes_before_fitting( - dtype: type[np.floating], offset: float +@pytest.mark.parametrize("transform", ["power", "safepower"]) +def test__fit__power_standardizes_before_fitting( + dtype: type[np.floating], offset: float, transform: str ) -> None: X, y = _mk_skewed_reg_dataset(0) y = (y + offset).astype(dtype) @@ -1539,7 +1540,7 @@ def test__fit__safepower_standardizes_before_fitting( n_estimators=1, device="cpu", random_state=42, - inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("safepower",)}, + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": (transform,)}, ) reg.fit(X, y) @@ -1553,7 +1554,7 @@ def test__fit__safepower_standardizes_before_fitting( ) expected = regressor_module.get_all_reshape_feature_distribution_preprocessors( num_examples=len(y), random_state=42 - )["safepower"].fit_transform(standardized.reshape(-1, 1)) + )[transform].fit_transform(standardized.reshape(-1, 1)) np.testing.assert_array_equal(preset.transform(y.reshape(-1, 1)), expected) pipeline = reg.executor_.ensemble_members[0].config.target_transform np.testing.assert_allclose( @@ -2296,3 +2297,37 @@ def test__eval_metric_and_tuning_config__round_trip_through_sklearn_clone( round_tripped = TabPFNRegressor().set_params(**params) assert round_tripped.eval_metric == eval_metric assert round_tripped.tuning_config == tuning_config + + +@pytest.mark.parametrize( + ("transform", "minimum", "domain"), + [ + ("log", 0.0, "strictly positive"), + ("log", -1.0, "strictly positive"), + ("1_plus_log", -1.0, "greater than -1"), + ("1_plus_log", -2.0, "greater than -1"), + ], +) +def test__fit__invalid_log_target__raises_clear_error( + transform: str, minimum: float, domain: str +) -> None: + X, y = _mk_skewed_reg_dataset(0) + y[0] = minimum + reg = TabPFNRegressor( + n_estimators=1, + device="cpu", + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": (transform,)}, + ) + with pytest.raises(ValueError, match=f"{transform}.*{domain}"): + reg.fit(X, y) + + +def test__fit__exp_target__warns_about_numerical_risks() -> None: + X, y = _mk_skewed_reg_dataset(0) + reg = TabPFNRegressor( + n_estimators=1, + device="cpu", + inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("exp",)}, + ) + with pytest.warns(UserWarning, match="exp.*raw targets.*overflow"): + reg.fit(X, y / y.max()) From e03adc6752eded0abeed98fd8f3b00f7a10e9c89 Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 28 Oct 2026 13:53:29 +0100 Subject: [PATCH 5/7] fix: warn only when exp targets overflow in float32 --- changelog/1199.changed.md | 2 +- src/tabpfn/regressor.py | 22 ++++++++++++++-------- tests/test_regressor_interface.py | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/changelog/1199.changed.md b/changelog/1199.changed.md index 3e494f8f9..6b223be04 100644 --- a/changelog/1199.changed.md +++ b/changelog/1199.changed.md @@ -1 +1 @@ -Regression target transforms now act on raw targets before final standardization. Both `power` and `safepower` retain input standardization; `None` and `"none"` retain standardization only. Invalid `log` and `1_plus_log` target domains now raise clear errors during fitting, and `exp` warns about overflow and precision loss. +Regression target transforms now act on raw targets before final standardization. Both `power` and `safepower` retain input standardization; `None` and `"none"` retain standardization only. Invalid `log` and `1_plus_log` target domains now raise clear errors during fitting, and `exp` warns when transformed targets would overflow in float32. diff --git a/src/tabpfn/regressor.py b/src/tabpfn/regressor.py index 95eac24b9..a66e0badd 100644 --- a/src/tabpfn/regressor.py +++ b/src/tabpfn/regressor.py @@ -1033,14 +1033,20 @@ def _initialize_dataset_preprocessing( "greater than -1. Use 'safepower' or None for targets <= -1." ) if preset == "exp": - warnings.warn( - "The 'exp' regression target transform acts on raw targets and can " - "overflow, underflow, or lose distinctions between target values " - "during standardization. Predictions may be invalid even when " - "fit succeeds. Prefer 'safepower' or None for numerical stability.", - UserWarning, - stacklevel=2, - ) + with np.errstate(over="ignore"): + overflow_count = np.count_nonzero( + np.isinf(np.exp(np.asarray(y, dtype=np.float32))) + ) + if overflow_count: + warnings.warn( + f"The 'exp' regression target transform would make " + f"{overflow_count} of {y.size} raw targets overflow " + "in float32. " + "Even one infinite value makes target standardization invalid. " + "Rescale the targets or use 'safepower' or None.", + UserWarning, + stacklevel=2, + ) # Yeo-Johnson depends on units and offset; fit it on standardized targets. for preset in ("power", "safepower"): possible_target_transforms[preset] = Pipeline( diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index 8dc87a1cb..337626340 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -6,6 +6,7 @@ import itertools import os import typing +import warnings from collections.abc import Callable from fractions import Fraction from functools import partial @@ -2322,12 +2323,20 @@ def test__fit__invalid_log_target__raises_clear_error( reg.fit(X, y) -def test__fit__exp_target__warns_about_numerical_risks() -> None: +@pytest.mark.parametrize("maximum", [1.0, 88.0, 90.0]) +def test__fit__exp_target__warns_only_for_float32_overflow(maximum: float) -> None: X, y = _mk_skewed_reg_dataset(0) reg = TabPFNRegressor( n_estimators=1, device="cpu", inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": ("exp",)}, ) - with pytest.warns(UserWarning, match="exp.*raw targets.*overflow"): - reg.fit(X, y / y.max()) + y = y / y.max() + y[-1] = maximum + if maximum > 88.0: + with pytest.warns(UserWarning, match="exp.*1 of .*overflow in float32"): + reg.fit(X, y) + else: + with warnings.catch_warnings(): + warnings.filterwarnings("error", message=".*exp.*", category=UserWarning) + reg.fit(X, y) From 0b8de24f11cab21ef2675caffde3c34af309cdba Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 28 Oct 2026 14:13:21 +0100 Subject: [PATCH 6/7] perf: check exp overflow without floating point temporaries --- src/tabpfn/regressor.py | 6 ++---- tests/test_regressor_interface.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/tabpfn/regressor.py b/src/tabpfn/regressor.py index a66e0badd..bfbdd43d9 100644 --- a/src/tabpfn/regressor.py +++ b/src/tabpfn/regressor.py @@ -1033,10 +1033,8 @@ def _initialize_dataset_preprocessing( "greater than -1. Use 'safepower' or None for targets <= -1." ) if preset == "exp": - with np.errstate(over="ignore"): - overflow_count = np.count_nonzero( - np.isinf(np.exp(np.asarray(y, dtype=np.float32))) - ) + overflow_threshold = np.log(np.finfo(np.float32).max) + overflow_count = np.count_nonzero(y >= overflow_threshold) if overflow_count: warnings.warn( f"The 'exp' regression target transform would make " diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index 337626340..f49e231c7 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -2323,8 +2323,19 @@ def test__fit__invalid_log_target__raises_clear_error( reg.fit(X, y) -@pytest.mark.parametrize("maximum", [1.0, 88.0, 90.0]) -def test__fit__exp_target__warns_only_for_float32_overflow(maximum: float) -> None: +@pytest.mark.parametrize( + ("maximum", "warns"), + [ + (1.0, False), + (88.0, False), + (np.nextafter(np.log(np.finfo(np.float32).max), np.float32(-np.inf)), False), + (np.log(np.finfo(np.float32).max), True), + (90.0, True), + ], +) +def test__fit__exp_target__warns_only_for_float32_overflow( + maximum: float, warns: bool +) -> None: X, y = _mk_skewed_reg_dataset(0) reg = TabPFNRegressor( n_estimators=1, @@ -2333,7 +2344,7 @@ def test__fit__exp_target__warns_only_for_float32_overflow(maximum: float) -> No ) y = y / y.max() y[-1] = maximum - if maximum > 88.0: + if warns: with pytest.warns(UserWarning, match="exp.*1 of .*overflow in float32"): reg.fit(X, y) else: From 42329717b93483110fdf8d72d10bd3a22fd9ab3c Mon Sep 17 00:00:00 2001 From: Benjamin Jaeger Date: Wed, 28 Oct 2026 16:31:57 +0100 Subject: [PATCH 7/7] test: pin target transform accuracy checks to float32 --- tests/test_regressor_interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_regressor_interface.py b/tests/test_regressor_interface.py index f49e231c7..2ba4a1090 100644 --- a/tests/test_regressor_interface.py +++ b/tests/test_regressor_interface.py @@ -1594,6 +1594,8 @@ def test__predict__target_transform__predictions_are_finite_and_accurate( reg = TabPFNRegressor( n_estimators=2, device="cpu", + # Keep transform accuracy independent of CPU bfloat16 support. + inference_precision=torch.float32, random_state=42, inference_config={"REGRESSION_Y_PREPROCESS_TRANSFORMS": (transform,)}, )