diff --git a/src/aix_page/calibration/calibrators.py b/src/aix_page/calibration/calibrators.py index 01510b9..15004c7 100644 --- a/src/aix_page/calibration/calibrators.py +++ b/src/aix_page/calibration/calibrators.py @@ -17,19 +17,24 @@ def pseudo_rows( return x[keep], y[keep], w[keep] +def _logit(probabilities: np.ndarray) -> np.ndarray: + """Apply the historical probability clipping contract before taking log odds.""" + return np.asarray( + np.log(np.clip(probabilities, 1e-8, 1 - 1e-8) / np.clip(1 - probabilities, 1e-8, 1)) + ) + + class Platt: def __init__(self) -> None: self.model = LogisticRegression(C=1e6) def fit(self, click: np.ndarray, impression: np.ndarray, p: np.ndarray) -> Platt: - x, y, w = pseudo_rows( - click, impression, np.log(np.clip(p, 1e-8, 1 - 1e-8) / np.clip(1 - p, 1e-8, 1)) - ) + x, y, w = pseudo_rows(click, impression, _logit(p)) self.model.fit(x, y, sample_weight=w) return self def predict(self, p: np.ndarray) -> np.ndarray: - logit = np.log(np.clip(p, 1e-8, 1 - 1e-8) / np.clip(1 - p, 1e-8, 1)) + logit = _logit(p) return np.asarray(self.model.predict_proba(logit[:, None])[:, 1], dtype=np.float64) diff --git a/tests/unit/test_calibration_polish.py b/tests/unit/test_calibration_polish.py new file mode 100644 index 0000000..ec43bdf --- /dev/null +++ b/tests/unit/test_calibration_polish.py @@ -0,0 +1,22 @@ +"""Confirm Platt log-odds clipping remains numerically identical.""" + +import numpy as np + +from aix_page.calibration.calibrators import _logit, pseudo_rows + + +def test_logit_reuses_exact_historical_clipping_formula(): + probabilities = np.array([0.0, 1e-9, 0.1, 0.5, 0.9, 1.0]) + expected = np.log(np.clip(probabilities, 1e-8, 1 - 1e-8) / np.clip(1 - probabilities, 1e-8, 1)) + np.testing.assert_array_equal(_logit(probabilities), expected) + + +def test_pseudo_rows_keep_positive_and_negative_weights(): + x, labels, weights = pseudo_rows( + np.array([2.0, 0.0]), + np.array([10.0, 5.0]), + np.array([0.2, 0.8]), + ) + assert x.shape == (3, 1) + assert labels.tolist() == [1.0, 0.0, 0.0] + assert weights.tolist() == [2.0, 8.0, 5.0]