Files
shap--shap/tests/maskers/test_output_composite.py
2026-07-13 13:22:52 +08:00

226 lines
6.5 KiB
Python

"""This file contains tests for the OutputComposite masker."""
import tempfile
import numpy as np
import shap
def test_output_composite_init():
"""Test OutputComposite masker initialization."""
masker = shap.maskers.Fixed()
def simple_model(x):
return np.sum(x)
output_composite = shap.maskers.OutputComposite(masker, simple_model)
assert output_composite.masker is masker
assert output_composite.model is simple_model
def test_output_composite_attribute_propagation():
"""Test that attributes from the underlying masker are propagated."""
masker = shap.maskers.Fixed()
def simple_model(x):
return x
output_composite = shap.maskers.OutputComposite(masker, simple_model)
# Check that shape is propagated
assert hasattr(output_composite, "shape")
assert output_composite.shape == masker.shape
def test_output_composite_call_with_tuple_output():
"""Test OutputComposite __call__ when model returns a tuple."""
masker = shap.maskers.Fixed()
def model_with_tuple_output(x):
return (np.sum(x), np.mean(x))
output_composite = shap.maskers.OutputComposite(masker, model_with_tuple_output)
test_input = np.array([1, 2, 3, 4, 5])
mask = np.array([], dtype=bool)
result = output_composite(mask, test_input)
# Result should be masked input + model output
assert isinstance(result, tuple)
# Fixed masker returns 1 element, model returns 2 elements
assert len(result) == 3
def test_output_composite_call_with_scalar_output():
"""Test OutputComposite __call__ when model returns a scalar."""
masker = shap.maskers.Fixed()
def model_with_scalar_output(x):
return np.sum(x)
output_composite = shap.maskers.OutputComposite(masker, model_with_scalar_output)
test_input = np.array([1, 2, 3, 4, 5])
mask = np.array([], dtype=bool)
result = output_composite(mask, test_input)
# Result should be masked input + model output (wrapped in tuple)
assert isinstance(result, tuple)
# Fixed masker returns 1 element, model returns 1 element (wrapped)
assert len(result) == 2
def test_output_composite_call_with_array_output():
"""Test OutputComposite __call__ when model returns an array."""
masker = shap.maskers.Fixed()
def model_with_array_output(x):
return x * 2
output_composite = shap.maskers.OutputComposite(masker, model_with_array_output)
test_input = np.array([1, 2, 3])
mask = np.array([], dtype=bool)
result = output_composite(mask, test_input)
assert isinstance(result, tuple)
assert len(result) == 2
# Check model output is correct
np.testing.assert_array_equal(result[1], test_input * 2)
def test_output_composite_serialization():
"""Test OutputComposite serialization and deserialization."""
masker = shap.maskers.Fixed()
def simple_model(x):
return np.sum(x)
original_composite = shap.maskers.OutputComposite(masker, simple_model)
with tempfile.TemporaryFile() as temp_file:
# Serialize
original_composite.save(temp_file)
temp_file.seek(0)
# Deserialize
loaded_composite = shap.maskers.OutputComposite.load(temp_file)
# Verify the loaded masker works
test_input = np.array([1, 2, 3])
mask = np.array([], dtype=bool)
original_result = original_composite(mask, test_input)
loaded_result = loaded_composite(mask, test_input)
# Check results are the same
assert len(original_result) == len(loaded_result)
def test_output_composite_with_multiple_args():
"""Test OutputComposite with a model that takes multiple arguments."""
# Create a custom masker that takes 2 arguments
class TwoArgMasker(shap.maskers.Masker):
def __init__(self):
self.shape = (None, 0)
def __call__(self, mask, *args): # type: ignore[override]
x, y = args
return ([x], [y])
masker = TwoArgMasker()
def model_two_args(x, y):
return np.sum(x) + np.sum(y)
output_composite = shap.maskers.OutputComposite(masker, model_two_args)
test_input1 = np.array([1, 2, 3])
test_input2 = np.array([4, 5, 6])
mask = np.array([], dtype=bool)
result = output_composite(mask, test_input1, test_input2)
assert isinstance(result, tuple)
# Masker returns 2 elements, model returns 1 (wrapped)
assert len(result) == 3
assert result[2] == np.sum(test_input1) + np.sum(test_input2)
def test_output_composite_attribute_none_handling():
"""Test that OutputComposite handles None attributes correctly."""
# Create a minimal masker without all optional attributes
class MinimalMasker(shap.maskers.Masker):
def __init__(self):
self.shape = (None, 0)
# Don't set optional attributes
def __call__(self, mask, *args): # type: ignore[override]
x = args[0]
return ([x],)
masker = MinimalMasker()
def simple_model(x):
return x
output_composite = shap.maskers.OutputComposite(masker, simple_model)
# Should have shape but not necessarily other attributes
assert hasattr(output_composite, "shape")
# These attributes shouldn't be set if not present in masker
assert not hasattr(output_composite, "invariants") or output_composite.invariants is None
def test_output_composite_text_data_flag():
"""Test that text_data flag propagates from underlying masker."""
class TextMasker(shap.maskers.Masker):
def __init__(self):
self.shape = (None, 0)
self.text_data = True
def __call__(self, mask, *args): # type: ignore[override]
x = args[0]
return ([x],)
masker = TextMasker()
def simple_model(x):
return x
output_composite = shap.maskers.OutputComposite(masker, simple_model)
assert hasattr(output_composite, "text_data")
assert output_composite.text_data is True
def test_output_composite_image_data_flag():
"""Test that image_data flag propagates from underlying masker."""
class ImageMasker(shap.maskers.Masker):
def __init__(self):
self.shape = (None, 0)
self.image_data = True
def __call__(self, mask, *args): # type: ignore[override]
x = args[0]
return ([x],)
masker = ImageMasker()
def simple_model(x):
return x
output_composite = shap.maskers.OutputComposite(masker, simple_model)
assert hasattr(output_composite, "image_data")
assert output_composite.image_data is True