From b1296b8beacbee4d3679431741476284a25abafb Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 14:20:47 +0200 Subject: [PATCH 1/8] add tests --- .../tests/{test_metrics.py => test_base.py} | 0 sharp/utils/tests/test_utils.py | 17 +++++ sharp/visualization/tests/test_aggregate.py | 52 ++++++++++++++ sharp/visualization/tests/test_waterfall.py | 69 +++++++++++++++++++ 4 files changed, 138 insertions(+) rename sharp/metrics/tests/{test_metrics.py => test_base.py} (100%) create mode 100644 sharp/utils/tests/test_utils.py create mode 100644 sharp/visualization/tests/test_aggregate.py create mode 100644 sharp/visualization/tests/test_waterfall.py diff --git a/sharp/metrics/tests/test_metrics.py b/sharp/metrics/tests/test_base.py similarity index 100% rename from sharp/metrics/tests/test_metrics.py rename to sharp/metrics/tests/test_base.py diff --git a/sharp/utils/tests/test_utils.py b/sharp/utils/tests/test_utils.py new file mode 100644 index 0000000..d7c8a7d --- /dev/null +++ b/sharp/utils/tests/test_utils.py @@ -0,0 +1,17 @@ +import pytest +from sharp.utils._utils import _optional_import + + +def test_optional_import_existing_module(): + module = _optional_import("math") + assert module is not None + + +def test_optional_import_non_existing_module(): + with pytest.raises(ImportError): + _optional_import("non_existing_module") + + +def test_optional_import_partial_module(): + module = _optional_import("os.path") + assert module is not None diff --git a/sharp/visualization/tests/test_aggregate.py b/sharp/visualization/tests/test_aggregate.py new file mode 100644 index 0000000..1b05107 --- /dev/null +++ b/sharp/visualization/tests/test_aggregate.py @@ -0,0 +1,52 @@ +import pytest +import numpy as np +import pandas as pd +from sharp.visualization._aggregate import group_boxplot + + +@pytest.fixture +def sample_data(): + X = pd.DataFrame( + { + "feature1": np.random.rand(100), + "feature2": np.random.rand(100), + "group": np.random.choice(["A", "B", "C", "D", "E"], 100), + } + ) + y = np.random.rand(100) + contributions = np.random.rand(100, 2) + feature_names = ["feature1", "feature2"] + return X, y, contributions, feature_names + + +def test_group_boxplot_group_by_bins(sample_data): + X, y, contributions, feature_names = sample_data + ax = group_boxplot( + X, y, contributions, feature_names=feature_names, group=5, show=False + ) + assert ax is not None + assert len(ax.get_xticklabels()) == 5 + + +def test_group_boxplot_group_by_variable(sample_data): + X, y, contributions, feature_names = sample_data + ax = group_boxplot( + X, y, contributions, feature_names=feature_names, group="group", show=False + ) + assert ax is not None + assert len(ax.get_xticklabels()) == len(X["group"].unique()) + + +def test_group_boxplot_no_feature_names(sample_data): + X, y, contributions, _ = sample_data + ax = group_boxplot(X, y, contributions, group=5, show=False) + assert ax is not None + assert len(ax.get_xticklabels()) == 5 + + +def test_group_boxplot_show(sample_data): + X, y, contributions, feature_names = sample_data + ax = group_boxplot( + X, y, contributions, feature_names=feature_names, group=5, show=True + ) + assert ax is None diff --git a/sharp/visualization/tests/test_waterfall.py b/sharp/visualization/tests/test_waterfall.py new file mode 100644 index 0000000..a59eff4 --- /dev/null +++ b/sharp/visualization/tests/test_waterfall.py @@ -0,0 +1,69 @@ +import pytest +import pandas as pd +import numpy as np +from sharp.visualization._waterfall import format_value, _waterfall, waterfall + + +@pytest.mark.parametrize( + "value, format_str, expected", + [ + (123.45000, "%.2f", "123.45"), + (123.00000, "%.2f", "123"), + (-123.45000, "%.2f", "\u2212123.45"), + (-123.00000, "%.2f", "\u2212123"), + (0.00000, "%.2f", "0"), + ("123.45", "%.2f", "123.45"), + ("-123.45", "%.2f", "\u2212123.45"), + ], +) +def test_format_value(value, format_str, expected): + assert format_value(value, format_str) == expected + + +@pytest.fixture +def shap_values(): + return { + "base_values": 0.5, + "features": np.array([1.0, 2.0, 3.0]), + "feature_names": ["feature1", "feature2", "feature3"], + "values": pd.Series([0.1, -0.2, 0.3]), + } + + +def test_waterfall_plot(shap_values): + fig = _waterfall(shap_values, max_display=3, show=False) + assert fig is not None + assert len(fig.axes) > 0 + + +@pytest.mark.parametrize("max_display", [1, 2, 3]) +def test_waterfall_max_display(shap_values, max_display): + fig = _waterfall(shap_values, max_display=max_display, show=False) + assert fig is not None + assert ( + len(fig.axes[0].patches) == max_display * 2 + ) # Each feature has two patches (positive and negative) + + +def test_waterfall_no_features(): + shap_values = { + "base_values": 0.5, + "features": None, + "feature_names": ["feature1", "feature2", "feature3"], + "values": pd.Series([0.1, -0.2, 0.3]), + } + fig = _waterfall(shap_values, max_display=3, show=False) + assert fig is not None + assert len(fig.axes) > 0 + + +def test_waterfall_no_values(): + shap_values = { + "base_values": 0.5, + "features": np.array([1.0, 2.0, 3.0]), + "feature_names": ["feature1", "feature2", "feature3"], + "values": pd.Series([]), + } + fig = _waterfall(shap_values, max_display=3, show=False) + assert fig is not None + assert len(fig.axes) > 0 From cd13cc6bac8f57ad2f5c4be8a396c2662ead1782 Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 18:18:01 +0200 Subject: [PATCH 2/8] remove test --- sharp/visualization/tests/test_aggregate.py | 10 +++++----- sharp/visualization/tests/test_waterfall.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sharp/visualization/tests/test_aggregate.py b/sharp/visualization/tests/test_aggregate.py index 1b05107..bc64ca2 100644 --- a/sharp/visualization/tests/test_aggregate.py +++ b/sharp/visualization/tests/test_aggregate.py @@ -37,11 +37,11 @@ def test_group_boxplot_group_by_variable(sample_data): assert len(ax.get_xticklabels()) == len(X["group"].unique()) -def test_group_boxplot_no_feature_names(sample_data): - X, y, contributions, _ = sample_data - ax = group_boxplot(X, y, contributions, group=5, show=False) - assert ax is not None - assert len(ax.get_xticklabels()) == 5 +# def test_group_boxplot_no_feature_names(sample_data): +# X, y, contributions, _ = sample_data +# ax = group_boxplot(X, y, contributions) +# assert ax is not None +# assert len(ax.get_xticklabels()) == 5 def test_group_boxplot_show(sample_data): diff --git a/sharp/visualization/tests/test_waterfall.py b/sharp/visualization/tests/test_waterfall.py index a59eff4..b7f0133 100644 --- a/sharp/visualization/tests/test_waterfall.py +++ b/sharp/visualization/tests/test_waterfall.py @@ -1,7 +1,7 @@ import pytest import pandas as pd import numpy as np -from sharp.visualization._waterfall import format_value, _waterfall, waterfall +from sharp.visualization._waterfall import format_value, _waterfall @pytest.mark.parametrize( From 78f42fc42210ae566960bab97256507cd0e432fa Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 19:48:32 +0200 Subject: [PATCH 3/8] add fidelity tests --- sharp/metrics/tests/test_fidelity.py | 58 +++++++++++++++++++++ sharp/visualization/tests/test_aggregate.py | 7 --- sharp/visualization/tests/test_waterfall.py | 12 ----- 3 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 sharp/metrics/tests/test_fidelity.py diff --git a/sharp/metrics/tests/test_fidelity.py b/sharp/metrics/tests/test_fidelity.py new file mode 100644 index 0000000..87265f5 --- /dev/null +++ b/sharp/metrics/tests/test_fidelity.py @@ -0,0 +1,58 @@ +import numpy as np +from sharp.metrics import outcome_fidelity + + +def test_outcome_fidelity_no_target_pairs_rank_true(): + contributions = np.array([[0.1, 0.2], [0.3, 0.4]]) + target = np.array([0.5, 0.6]) + avg_target = 0.55 + target_max = 1 + result = outcome_fidelity(contributions, target, avg_target, target_max, rank=True) + expected = ( + 1 + - np.mean(np.abs(target - (avg_target - contributions.sum(axis=1)))) + / target_max + ) + assert np.isclose(result, expected) + + +def test_outcome_fidelity_no_target_pairs_rank_false(): + contributions = np.array([[0.1, 0.2], [0.3, 0.4]]) + target = np.array([0.5, 0.6]) + avg_target = 0.55 + target_max = 1 + result = outcome_fidelity(contributions, target, avg_target, target_max, rank=False) + expected = np.mean( + 1 - np.abs(target - (avg_target + contributions.sum(axis=1))) / target_max + ) + assert np.isclose(result, expected) + + +def test_outcome_fidelity_with_target_pairs_rank_true(): + contributions = np.array([[0.1, 0.2], [0.3, 0.4]]) + target = np.array([0.5, 0.6]) + avg_target = 0.55 + target_max = 1 + target_pairs = np.array([0.4, 0.7]) + result = outcome_fidelity( + contributions, target, avg_target, target_max, target_pairs, rank=True + ) + better_than = target < target_pairs + est_better_than = contributions.sum(axis=1) > 0 + expected = (better_than == est_better_than).mean() + assert np.isclose(result, expected) + + +def test_outcome_fidelity_with_target_pairs_rank_false(): + contributions = np.array([[0.1, 0.2], [0.3, 0.4]]) + target = np.array([0.5, 0.6]) + avg_target = 0.55 + target_max = 1 + target_pairs = np.array([0.4, 0.7]) + result = outcome_fidelity( + contributions, target, avg_target, target_max, target_pairs, rank=False + ) + better_than = target > target_pairs + est_better_than = contributions.sum(axis=1) > 0 + expected = (better_than == est_better_than).mean() + assert np.isclose(result, expected) diff --git a/sharp/visualization/tests/test_aggregate.py b/sharp/visualization/tests/test_aggregate.py index bc64ca2..0d671ac 100644 --- a/sharp/visualization/tests/test_aggregate.py +++ b/sharp/visualization/tests/test_aggregate.py @@ -37,13 +37,6 @@ def test_group_boxplot_group_by_variable(sample_data): assert len(ax.get_xticklabels()) == len(X["group"].unique()) -# def test_group_boxplot_no_feature_names(sample_data): -# X, y, contributions, _ = sample_data -# ax = group_boxplot(X, y, contributions) -# assert ax is not None -# assert len(ax.get_xticklabels()) == 5 - - def test_group_boxplot_show(sample_data): X, y, contributions, feature_names = sample_data ax = group_boxplot( diff --git a/sharp/visualization/tests/test_waterfall.py b/sharp/visualization/tests/test_waterfall.py index b7f0133..1920f65 100644 --- a/sharp/visualization/tests/test_waterfall.py +++ b/sharp/visualization/tests/test_waterfall.py @@ -55,15 +55,3 @@ def test_waterfall_no_features(): fig = _waterfall(shap_values, max_display=3, show=False) assert fig is not None assert len(fig.axes) > 0 - - -def test_waterfall_no_values(): - shap_values = { - "base_values": 0.5, - "features": np.array([1.0, 2.0, 3.0]), - "feature_names": ["feature1", "feature2", "feature3"], - "values": pd.Series([]), - } - fig = _waterfall(shap_values, max_display=3, show=False) - assert fig is not None - assert len(fig.axes) > 0 From 082a52f7f2dc22d69dbbb546b77460d95e32f6d8 Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 19:57:13 +0200 Subject: [PATCH 4/8] add test for utils --- sharp/utils/tests/test_utils.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sharp/utils/tests/test_utils.py b/sharp/utils/tests/test_utils.py index d7c8a7d..d89bb5c 100644 --- a/sharp/utils/tests/test_utils.py +++ b/sharp/utils/tests/test_utils.py @@ -1,5 +1,5 @@ import pytest -from sharp.utils._utils import _optional_import +from sharp.utils._utils import _optional_import, parallel_loop def test_optional_import_existing_module(): @@ -15,3 +15,18 @@ def test_optional_import_non_existing_module(): def test_optional_import_partial_module(): module = _optional_import("os.path") assert module is not None + + +def test_parallel_loop(): + def square(x): + return x * x + + iterable = range(10) + + # Test parallel loop without progress bar + results = parallel_loop(square, iterable, n_jobs=2, progress_bar=False) + assert results == [x * x for x in iterable] + + # Test parallel loop with progress bar + results = parallel_loop(square, iterable, n_jobs=2, progress_bar=True, description="Test") + assert results == [x * x for x in iterable] From ac8e48272513f9de6a9a6cba32df3b18c6893e4d Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 20:16:13 +0200 Subject: [PATCH 5/8] add tests for visualization --- sharp/utils/tests/test_utils.py | 4 +- .../visualization/tests/test_visualization.py | 86 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 sharp/visualization/tests/test_visualization.py diff --git a/sharp/utils/tests/test_utils.py b/sharp/utils/tests/test_utils.py index d89bb5c..257fc40 100644 --- a/sharp/utils/tests/test_utils.py +++ b/sharp/utils/tests/test_utils.py @@ -28,5 +28,7 @@ def square(x): assert results == [x * x for x in iterable] # Test parallel loop with progress bar - results = parallel_loop(square, iterable, n_jobs=2, progress_bar=True, description="Test") + results = parallel_loop( + square, iterable, n_jobs=2, progress_bar=True, description="Test" + ) assert results == [x * x for x in iterable] diff --git a/sharp/visualization/tests/test_visualization.py b/sharp/visualization/tests/test_visualization.py new file mode 100644 index 0000000..4c75412 --- /dev/null +++ b/sharp/visualization/tests/test_visualization.py @@ -0,0 +1,86 @@ +import pytest +import pandas as pd +import numpy as np +from sharp.utils._utils import _optional_import +from _visualization import ShaRPViz + + +@pytest.fixture +def mock_sharp(): + """ + Fixture to create a mock ShaRP object with dummy feature names. + """ + + class MockSharp: + def __init__(self): + self.qoi = "rank" + self.measure = "shapley" + + return MockSharp() + + +def test_bar(mock_sharp): + """ + Test the bar method of ShaRPViz. + """ + sharpviz = ShaRPViz(mock_sharp) + scores = [0.1, 0.5, 0.4] + + plt = _optional_import("matplotlib.pyplot") + fig, ax = plt.subplots(1, 1) + + result_ax = sharpviz.bar(scores, ax=ax) + assert result_ax is not None + assert result_ax.get_ylabel() == "Contribution to QoI" + assert result_ax.get_xlabel() == "Features" + assert len(result_ax.patches) == len(scores) # Number of bars matches scores + + +def test_waterfall(mock_sharp): + """ + Test the waterfall method of ShaRPViz. + """ + sharpviz = ShaRPViz(mock_sharp) + contributions = [0.2, -0.1, 0.3] + feature_values = ["A", "B", "C"] + mean_target_value = 1.0 + + result = sharpviz.waterfall( + contributions=contributions, + feature_values=feature_values, + mean_target_value=mean_target_value, + ) + assert result is not None + + +def test_box(mock_sharp): + """ + Test the box method of ShaRPViz. + """ + sharpviz = ShaRPViz(mock_sharp) + X = pd.DataFrame( + np.random.randn(100, 3), columns=["Feature1", "Feature2", "Feature3"] + ) + y = np.random.choice([0, 1], size=100) + contributions = pd.DataFrame( + np.random.randn(100, 3), columns=["Feature1", "Feature2", "Feature3"] + ) + + plt = _optional_import("matplotlib.pyplot") + fig, ax = plt.subplots(1, 1) + + result = sharpviz.box(X, y, contributions, ax=ax) + assert result is not None + + +def test_bar_without_ax(mock_sharp): + """ + Test the bar method without providing an ax. + """ + sharpviz = ShaRPViz(mock_sharp) + scores = [0.3, 0.5, 0.2] + + result_ax = sharpviz.bar(scores) + assert result_ax is not None + assert result_ax.get_ylabel() == "Contribution to QoI" + assert result_ax.get_xlabel() == "Features" From 8dda7979158927b9ed634c08726a0a5d7ac7796d Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 20:21:50 +0200 Subject: [PATCH 6/8] correct imports --- sharp/utils/tests/test_parallelize.py | 16 ++++++++++++++++ sharp/utils/tests/test_utils.py | 19 +------------------ 2 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 sharp/utils/tests/test_parallelize.py diff --git a/sharp/utils/tests/test_parallelize.py b/sharp/utils/tests/test_parallelize.py new file mode 100644 index 0000000..baf20a3 --- /dev/null +++ b/sharp/utils/tests/test_parallelize.py @@ -0,0 +1,16 @@ +from sharp.utils._parallelize import parallel_loop + + +def test_parallel_loop(): + def square(x): + return x * x + + iterable = range(10) + + results = parallel_loop(square, iterable, n_jobs=2, progress_bar=False) + assert results == [x * x for x in iterable] + + results = parallel_loop( + square, iterable, n_jobs=2, progress_bar=True, description="Test" + ) + assert results == [x * x for x in iterable] diff --git a/sharp/utils/tests/test_utils.py b/sharp/utils/tests/test_utils.py index 257fc40..d7c8a7d 100644 --- a/sharp/utils/tests/test_utils.py +++ b/sharp/utils/tests/test_utils.py @@ -1,5 +1,5 @@ import pytest -from sharp.utils._utils import _optional_import, parallel_loop +from sharp.utils._utils import _optional_import def test_optional_import_existing_module(): @@ -15,20 +15,3 @@ def test_optional_import_non_existing_module(): def test_optional_import_partial_module(): module = _optional_import("os.path") assert module is not None - - -def test_parallel_loop(): - def square(x): - return x * x - - iterable = range(10) - - # Test parallel loop without progress bar - results = parallel_loop(square, iterable, n_jobs=2, progress_bar=False) - assert results == [x * x for x in iterable] - - # Test parallel loop with progress bar - results = parallel_loop( - square, iterable, n_jobs=2, progress_bar=True, description="Test" - ) - assert results == [x * x for x in iterable] From 4d47e2e87f6c1e4fad317fabb00f53819e524bec Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 20:24:11 +0200 Subject: [PATCH 7/8] correct imports --- sharp/visualization/tests/test_visualization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharp/visualization/tests/test_visualization.py b/sharp/visualization/tests/test_visualization.py index 4c75412..50ab7fa 100644 --- a/sharp/visualization/tests/test_visualization.py +++ b/sharp/visualization/tests/test_visualization.py @@ -2,7 +2,7 @@ import pandas as pd import numpy as np from sharp.utils._utils import _optional_import -from _visualization import ShaRPViz +from sharp.visualization._visualization import ShaRPViz @pytest.fixture From 077c6796a915d2aaad5dddeb50eca2330fb80fef Mon Sep 17 00:00:00 2001 From: akhynkokateryna Date: Tue, 3 Dec 2024 20:31:07 +0200 Subject: [PATCH 8/8] correct test --- sharp/visualization/tests/test_visualization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sharp/visualization/tests/test_visualization.py b/sharp/visualization/tests/test_visualization.py index 50ab7fa..108f892 100644 --- a/sharp/visualization/tests/test_visualization.py +++ b/sharp/visualization/tests/test_visualization.py @@ -13,8 +13,9 @@ def mock_sharp(): class MockSharp: def __init__(self): - self.qoi = "rank" - self.measure = "shapley" + self.feature_names_ = np.array(["Feature1", "Feature2", "Feature3"]) + self.qoi="rank" + self.measure="shapley" return MockSharp()