from contextlib import nullcontext as does_not_raise import matplotlib.pyplot as plt import numpy as np import pytest from pytest import param import shap @pytest.fixture def data_explainer_shap_values(): RandomForestRegressor = pytest.importorskip("sklearn.ensemble").RandomForestRegressor # train model X, y = shap.datasets.california(n_points=500) model = RandomForestRegressor(n_estimators=100, random_state=0) model.fit(X, y) # explain the model's predictions using SHAP values explainer = shap.TreeExplainer(model) return X, explainer, explainer.shap_values(X) @pytest.mark.parametrize( "cmap, exp_ctx", [ # Valid cmaps param("coolwarm", does_not_raise(), id="valid-str"), param(["#000000", "#ffffff"], does_not_raise(), id="valid-list[str]"), # Invalid cmaps param( 777, pytest.raises(TypeError, match="Plot color map must be string or list!"), id="invalid-dtype1", ), param( [], pytest.raises(ValueError, match="Color map must be at least two colors"), id="invalid-insufficient-colors1", ), param( ["#8834BB"], pytest.raises(ValueError, match="Color map must be at least two colors"), id="invalid-insufficient-colors2", ), param( ["#883488", "#Gg8888"], pytest.raises(ValueError, match=r"Invalid color .+ found in cmap"), id="invalid-hexcolor-in-list1", ), param( ["#883488", "#1111119"], pytest.raises(ValueError, match=r"Invalid color .+ found in cmap"), id="invalid-hexcolor-in-list2", ), ], ) def test_verify_valid_cmap(cmap, exp_ctx): from shap.plots._force import verify_valid_cmap with exp_ctx: verify_valid_cmap(cmap) def test_random_force_plot_mpl_with_data(data_explainer_shap_values): """Test if force plot with matplotlib works.""" X, explainer, shap_values = data_explainer_shap_values # visualize the first prediction's explanation shap.force_plot(explainer.expected_value, shap_values[0, :], X.iloc[0, :], matplotlib=True, show=False) with pytest.raises(TypeError, match="force plot now requires the base value as the first parameter"): shap.force_plot([1, 1], shap_values, X.iloc[0, :], show=False) plt.close("all") def test_random_force_plot_mpl_text_rotation_with_data(data_explainer_shap_values): """Test if force plot with matplotlib works when supplied with text_rotation.""" X, explainer, shap_values = data_explainer_shap_values # visualize the first prediction's explanation shap.force_plot( explainer.expected_value, shap_values[0, :], X.iloc[0, :], matplotlib=True, text_rotation=30, show=False ) plt.close("all") @pytest.mark.mpl_image_compare(tolerance=3) def test_force_plot_negative_sign(): np.random.seed(0) base = 100 contribution = np.r_[-np.random.rand(5)] names = [f"minus_{i}" for i in range(5)] shap.force_plot( base, contribution, names, matplotlib=True, show=False, ) return plt.gcf() @pytest.mark.mpl_image_compare(tolerance=3) def test_force_plot_positive_sign(): np.random.seed(0) base = 100 contribution = np.r_[np.random.rand(5)] names = [f"plus_{i}" for i in range(5)] shap.force_plot( base, contribution, names, matplotlib=True, show=False, ) return plt.gcf() def test_flipud_reverses_clust_order(): """Regression test for GH-4342: np.flipud(clustOrder) was a no-op.""" from shap.plots._force import AdditiveExplanation, AdditiveForceArrayVisualizer from shap.utils._legacy import DenseData, IdentityLink, Instance, Model feature_names = ["f0", "f1"] base_value = 0.0 link = IdentityLink() data = DenseData(np.zeros((1, 2)), feature_names) model = Model(lambda x: x, ["f(x)"]) def _make_exp(effects): effects = np.array(effects, dtype=float) out_value = base_value + effects.sum() instance = Instance(np.ones((1, len(feature_names))), np.zeros(len(feature_names))) return AdditiveExplanation(base_value, out_value, effects, None, instance, link, model, data) # Sample 0: low total (sum = 1.0) # Sample 1: high total (sum = 10.0) exp_low = _make_exp([0.5, 0.5]) exp_high = _make_exp([5.0, 5.0]) viz = AdditiveForceArrayVisualizer([exp_low, exp_high]) sim_low = viz.data["explanations"][0]["simIndex"] sim_high = viz.data["explanations"][1]["simIndex"] assert sim_high < sim_low, ( f"Higher-prediction sample should come first (lower simIndex), " f"got simIndex_high={sim_high}, simIndex_low={sim_low}" )