diff --git a/CHANGELOG.md b/CHANGELOG.md index 6967dc814..e3b27c838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - **Expanded warning coverage for `Sample.from_frame()` ID inference** - Added assertions that validate all three expected warnings are emitted when inferring an `id` column and default weights, including ID guessing, ID string casting, and automatic weight creation. +- **Added focused unit coverage for IPW helpers** + - Added tests for `link_transform()`, and `calc_dev()` to validate behavior for extreme probabilities, and finite 10-fold deviance summaries. # 0.16.0 (2026-02-09) diff --git a/balance/weighting_methods/ipw.py b/balance/weighting_methods/ipw.py index 0a6a15733..a9c5377b6 100644 --- a/balance/weighting_methods/ipw.py +++ b/balance/weighting_methods/ipw.py @@ -30,7 +30,6 @@ logger: logging.Logger = logging.getLogger(__package__) -# TODO: Add tests for model_coefs() # TODO: Improve interpretability of model coefficients, as variables are no longer zero-centered. def model_coefs( model: ClassifierMixin, @@ -94,7 +93,6 @@ def model_coefs( } -# TODO: Add tests for link_transform() def link_transform(pred: np.ndarray) -> np.ndarray: """Transforms probabilities into log odds (link function). @@ -184,7 +182,6 @@ def _convert_to_dense_array( return X_matrix -# TODO: Add tests for calc_dev() def calc_dev( X_matrix: csr_matrix, y: np.ndarray, diff --git a/tests/test_ipw.py b/tests/test_ipw.py index 7c6a7004b..b8617cc0e 100644 --- a/tests/test_ipw.py +++ b/tests/test_ipw.py @@ -22,6 +22,7 @@ from balance.sample_class import Sample from balance.weighting_methods import ipw as balance_ipw from packaging.version import Version +from scipy.sparse import csr_matrix from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import log_loss @@ -37,6 +38,38 @@ class TestIPW( ): """Test suite for Inverse Propensity Weighting (IPW) functionality.""" + def test_link_transform_handles_midpoint_and_extremes(self) -> None: + """link_transform should return finite log-odds for probabilities in [0, 1].""" + + transformed = balance_ipw.link_transform(np.array([0.5, 0.0, 1.0])) + self.assertAlmostEqual(transformed[0], 0.0, places=10) + self.assertTrue(np.isfinite(transformed[1])) + self.assertTrue(np.isfinite(transformed[2])) + self.assertLess(transformed[1], 0) + self.assertGreater(transformed[2], 0) + + def test_calc_dev_returns_finite_mean_and_sd(self) -> None: + """calc_dev should run 10-fold CV and return finite deviance summary.""" + + rng = np.random.RandomState(42) + X = rng.normal(size=(40, 2)) + y = np.array([0] * 20 + [1] * 20) + foldids = np.tile(np.arange(10), 4) + model_weights = np.ones(40) + + dev_mean, dev_sd = balance_ipw.calc_dev( + csr_matrix(X), + y, + LogisticRegression(random_state=0, max_iter=300), + model_weights, + foldids, + ) + + self.assertTrue(np.isfinite(dev_mean)) + self.assertTrue(np.isfinite(dev_sd)) + self.assertGreaterEqual(dev_mean, 0.0) + self.assertGreaterEqual(dev_sd, 0.0) + def test_ipw_weights_order(self) -> None: """Test that IPW assigns correct relative weight ordering.