From 595c7056400f3e3d1932aa4a4bf14ff03979654e Mon Sep 17 00:00:00 2001 From: Aniruddha Adak Date: Tue, 29 Sep 2026 15:18:10 +0530 Subject: [PATCH] data: fix PartialDependence crash on datasets with < 10 rows (#681) _gen_pdp built an individual conditional expectation line for every row of the input data and then kept a random subsample of them as "background_scores" for plotting: ice_lines = ice_lines[ np.random.choice(ice_lines.shape[0], num_ice_samples, replace=False), : ] num_ice_samples defaults to 10 and is not exposed on the PartialDependence constructor. Because the subsample is drawn with replace=False, NumPy refuses to draw more items than the population size, so every dataset with fewer than num_ice_samples rows raised "ValueError: Cannot take a larger sample than population when 'replace=False'" instead of producing an explanation. The underlying data size was never checked or surfaced to the caller. Clamp the requested count to the number of available rows so smaller datasets yield one background line per row rather than crashing. Datasets at or above num_ice_samples are unaffected, since the min() resolves back to the original value. The only consumer of background_scores is interpret/visual/plot.py, which iterates it by row ("for i in range(background_lines.shape[0])"), so a smaller subsample renders correctly with no other change. Add tests/blackbox/test_partialdependence.py with a parametrized case over 1/2/5/9 rows, an end-to-end PartialDependence construction on a 3-row dataset, and a case asserting that a 25-row dataset still keeps exactly num_ice_samples background lines. Fixes #681 Signed-off-by: Aniruddha Adak --- .../interpret/blackbox/_partialdependence.py | 5 ++ .../tests/blackbox/test_partialdependence.py | 74 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 python/interpret-core/tests/blackbox/test_partialdependence.py diff --git a/python/interpret-core/interpret/blackbox/_partialdependence.py b/python/interpret-core/interpret/blackbox/_partialdependence.py index 7fdb6b446..0fbf6699f 100644 --- a/python/interpret-core/interpret/blackbox/_partialdependence.py +++ b/python/interpret-core/interpret/blackbox/_partialdependence.py @@ -51,6 +51,11 @@ def _gen_pdp( mean = np.mean(ice_lines, axis=0) std = np.std(ice_lines, axis=0) + # Sample without replacement, so we cannot draw more background lines than + # there are rows in the data. Datasets with fewer than num_ice_samples rows + # used to raise "Cannot take a larger sample than population when + # 'replace=False'", so clamp the count to the available population. + num_ice_samples = min(num_ice_samples, ice_lines.shape[0]) ice_lines = ice_lines[ np.random.choice(ice_lines.shape[0], num_ice_samples, replace=False), : ] diff --git a/python/interpret-core/tests/blackbox/test_partialdependence.py b/python/interpret-core/tests/blackbox/test_partialdependence.py new file mode 100644 index 000000000..e5aa94104 --- /dev/null +++ b/python/interpret-core/tests/blackbox/test_partialdependence.py @@ -0,0 +1,74 @@ +# Copyright (c) 2023 The InterpretML Contributors +# Distributed under the MIT software license + +import numpy as np +import pytest +from interpret.blackbox._partialdependence import PartialDependence, _gen_pdp +from sklearn.linear_model import LinearRegression + + +def _make_regression_data(n_samples): + rng = np.random.default_rng(42) + X = rng.normal(size=(n_samples, 2)) + y = X[:, 0] * 2.0 - X[:, 1] + 0.5 + return X, y + + +@pytest.mark.parametrize("n_samples", [1, 2, 5, 9]) +def test_gen_pdp_fewer_rows_than_ice_samples(n_samples): + """Datasets with fewer rows than the default num_ice_samples must not crash. + + Before the fix, sampling num_ice_samples (10) rows without replacement + raised "ValueError: Cannot take a larger sample than population when + 'replace=False'" for any dataset with fewer than 10 rows. + """ + X, y = _make_regression_data(n_samples) + model = LinearRegression().fit(X, y) + + pdp = _gen_pdp( + X, + model.predict, + col_idx=0, + feature_type="continuous", + num_points=10, + std_coef=1.0, + ) + + # One background line per available row, no more. + assert pdp["background_scores"].shape[0] == n_samples + assert pdp["background_scores"].shape[1] == len(pdp["names"]) + + +def test_partial_dependence_small_dataset_does_not_crash(): + """PartialDependence must work end-to-end on a dataset with < 10 rows.""" + X, y = _make_regression_data(3) + model = LinearRegression().fit(X, y) + + pdp = PartialDependence( + model, X, feature_names=["a", "b"], feature_types=["continuous", "continuous"] + ) + + assert len(pdp.pdps_) == 2 + for pdp_item in pdp.pdps_: + assert pdp_item["background_scores"].shape[0] == 3 + + explanation = pdp.explain_global() + assert explanation.feature_names == ["a", "b"] + + +def test_gen_pdp_enough_rows_still_caps_at_num_ice_samples(): + """With enough rows, exactly num_ice_samples background lines are kept.""" + X, y = _make_regression_data(25) + model = LinearRegression().fit(X, y) + + pdp = _gen_pdp( + X, + model.predict, + col_idx=0, + feature_type="continuous", + num_points=10, + std_coef=1.0, + num_ice_samples=10, + ) + + assert pdp["background_scores"].shape[0] == 10