From 7e671657116b75010bcb2c9af4bae13b013ddfca Mon Sep 17 00:00:00 2001 From: Johan Larsson Date: Thu, 27 Aug 2026 09:00:48 +0200 Subject: [PATCH] fix: center the dual before computing the gap --- objective.py | 5 ++++- tests/conftest.py | 5 +++++ tests/test_objective.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_objective.py diff --git a/objective.py b/objective.py index b654eb0..b7bffad 100644 --- a/objective.py +++ b/objective.py @@ -47,7 +47,10 @@ def evaluate_result(self, beta): ) # compute dual - theta = diff + theta = diff.copy() + if self.fit_intercept: + # An unpenalized intercept imposes sum(theta) = 0 in the dual. + theta -= np.mean(theta) theta /= max(1, self._dual_norm_slope(theta, self.alphas)) d_obj = (norm(y) ** 2 - norm(y - theta * n_samples) ** 2) / (2 * n_samples) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..26ab220 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import sys +from pathlib import Path + + +sys.path.insert(0, str(Path(__file__).parents[1])) diff --git a/tests/test_objective.py b/tests/test_objective.py new file mode 100644 index 0000000..64ca9f3 --- /dev/null +++ b/tests/test_objective.py @@ -0,0 +1,15 @@ +import numpy as np + +from objective import Objective + + +def test_intercept_dual_point_is_centered(): + objective = Objective(fit_intercept=True) + objective.X = np.array([[-1.0], [1.0]]) + objective.y = np.ones(2) + objective.alphas = np.ones(1) + + result = objective.evaluate_result(beta=np.array([0.75, 0.0])) + + np.testing.assert_allclose(result["duality_gap"], 0.03125) + assert result["duality_gap"] >= 0.0