Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring Exceptions #2580

Merged
merged 11 commits into from Jun 15, 2022
7 changes: 4 additions & 3 deletions shap/_explanation.py
Expand Up @@ -10,6 +10,7 @@
from slicer import Slicer, Alias, Obj
# from ._order import Order
from .utils._general import OpChain
from .utils._exceptions import DimensionError

# slicer confuses pylint...
# pylint: disable=no-member
Expand Down Expand Up @@ -298,7 +299,7 @@ def cohorts(self, cohorts):
if isinstance(cohorts, (list, tuple, np.ndarray)):
cohorts = np.array(cohorts)
return Cohorts(**{name: self[cohorts == name] for name in np.unique(cohorts)})
raise Exception("The given set of cohort indicators is not recognized! Please give an array or int.")
raise TypeError("The given set of cohort indicators is not recognized! Please give an array or int.")

def __repr__(self):
""" Display some basic printable info, but not everything.
Expand Down Expand Up @@ -575,7 +576,7 @@ def sum(self, axis=None, grouping=None):
elif axis == 1 or len(self.shape) == 1:
return group_features(self, grouping)
else:
raise Exception("Only axis = 1 is supported for grouping right now...")
raise DimensionError("Only axis = 1 is supported for grouping right now...")

def hstack(self, other):
""" Stack two explanations column-wise.
Expand Down Expand Up @@ -638,7 +639,7 @@ def hclust(self, metric="sqeuclidean", axis=0):
values = self.values

if len(values.shape) != 2:
raise Exception("The hclust order only supports 2D arrays right now!")
raise DimensionError("The hclust order only supports 2D arrays right now!")

if axis == 1:
values = values.T
Expand Down
47 changes: 27 additions & 20 deletions shap/actions/_optimizer.py
Expand Up @@ -3,72 +3,79 @@
import warnings
import copy
from ._action import Action
from ..utils._exceptions import InvalidAction, ConvergenceError


class ActionOptimizer():
class ActionOptimizer:
def __init__(self, model, actions):
self.model = model
warnings.warn("Note that ActionOptimizer is still in an alpha state and is subjust to API changes.")
warnings.warn(
"Note that ActionOptimizer is still in an alpha state and is subjust to API changes."
)
# actions go into mutually exclusive groups
self.action_groups = []
for group in actions:

if issubclass(type(group), Action):
group._group_index = len(self.action_groups)
group._grouped_index = 0
self.action_groups.append([copy.copy(group)])
elif issubclass(type(group), list):
group = sorted([copy.copy(v) for v in group], key=lambda a: a.cost)
for i,v in enumerate(group):
for i, v in enumerate(group):
v._group_index = len(self.action_groups)
v._grouped_index = i
self.action_groups.append(group)
else:
raise Exception("A passed action was not an Action or list of actions!")

raise InvalidAction(
"A passed action was not an Action or list of actions!"
)

def __call__(self, *args, max_evals=10000):

# init our queue with all the least costly actions
q = queue.PriorityQueue()
for i in range(len(self.action_groups)):
group = self.action_groups[i]
q.put((group[0].cost, [group[0]]))

nevals = 0
while not q.empty():

# see if we have exceeded our runtime budget
nevals += 1
if nevals > max_evals:
raise Exception(f"Failed to find a solution with max_evals={max_evals}! Try reducing the number of actions or increasing max_evals.")

raise ConvergenceError(
f"Failed to find a solution with max_evals={max_evals}! Try reducing the number of actions or increasing max_evals."
)

# get the next cheapest set of actions we can do
cost, actions = q.get()

# apply those actions
args_tmp = copy.deepcopy(args)
for a in actions:
a(*args_tmp)

# if the model is now satisfied we are done!!
v = self.model(*args_tmp)
if v:
return actions

# if not then we add all possible follow-on actions to our queue
else:
for i in range(len(self.action_groups)):
group = self.action_groups[i]

# look to to see if we already have a action from this group, if so we need to
# move to a more expensive action in the same group
next_ind = 0
prev_in_group = -1
for j,a in enumerate(actions):
for j, a in enumerate(actions):
if a._group_index == i:
next_ind = max(next_ind, a._grouped_index+1)
next_ind = max(next_ind, a._grouped_index + 1)
prev_in_group = j

# we are adding a new action type
if prev_in_group == -1:
new_actions = actions + [group[next_ind]]
Expand All @@ -79,7 +86,7 @@ def __call__(self, *args, max_evals=10000):
# we don't have a more expensive action left in this group
else:
new_actions = None

# add the new option to our queue
if new_actions is not None:
q.put((sum([a.cost for a in new_actions]), new_actions))
q.put((sum([a.cost for a in new_actions]), new_actions))
2 changes: 1 addition & 1 deletion shap/benchmark/_explanation_error.py
Expand Up @@ -115,7 +115,7 @@ def __call__(self, explanation, name, step_fraction=0.01, indices=[], silent=Fal
elif callable(self.masker.clustering):
row_clustering = self.masker.clustering(*args)
else:
raise Exception("The masker passed has a .clustering attribute that is not yet supported by the ExplanationError benchmark!")
raise NotImplementedError("The masker passed has a .clustering attribute that is not yet supported by the ExplanationError benchmark!")

masked_model = MaskedModel(self.model, self.masker, self.link, self.linearize_link, *args)

Expand Down
2 changes: 1 addition & 1 deletion shap/benchmark/_sequential.py
Expand Up @@ -14,7 +14,7 @@ def __init__(self, mask_type, sort_order, masker, model, *model_args, batch_size

for arg in model_args:
if isinstance(arg, pd.DataFrame):
raise Exception("DataFrame arguments dont iterate correctly, pass numpy arrays instead!")
raise TypeError("DataFrame arguments dont iterate correctly, pass numpy arrays instead!")

# convert any DataFrames to numpy arrays
# self.model_arg_cols = []
Expand Down
2 changes: 1 addition & 1 deletion shap/benchmark/experiments.py
Expand Up @@ -413,7 +413,7 @@ def __run_remote_experiment(experiment, remote, cache_dir="/tmp", python_binary=
#print(cache_id.replace("__", " ") + " ...loaded from remote after %f seconds" % (time.time() - start))
return pickle.load(f)
else:
raise Exception("Remote benchmark call finished but no local file was found!")
raise FileNotFoundError("Remote benchmark call finished but no local file was found!")

def __gen_cache_id(experiment):
dataset_name, model_name, method_name, metric_name = experiment
Expand Down
4 changes: 2 additions & 2 deletions shap/explainers/_additive.py
Expand Up @@ -48,7 +48,7 @@ def __init__(self, model, masker, link=None, feature_names=None, linearize_link=
# self.model(np.zeros(num_features))
# self._zero_offset = self.model(np.zeros(num_features))#model.intercept_#outputs[0]
# self._input_offsets = np.zeros(num_features) #* self._zero_offset
raise Exception("Masker not given and we don't yet support pulling the distribution centering directly from the EBM model!")
raise NotImplementedError("Masker not given and we don't yet support pulling the distribution centering directly from the EBM model!")
return

# here we need to compute the offsets ourselves because we can't pull them directly from a model we know about
Expand Down Expand Up @@ -83,7 +83,7 @@ def supports_model_with_masker(model, masker):
"""
if safe_isinstance(model, "interpret.glassbox.ExplainableBoostingClassifier"):
if model.interactions is not 0:
raise Exception("Need to add support for interaction effects!")
raise NotImplementedError("Need to add support for interaction effects!")
return True

return False
Expand Down
3 changes: 2 additions & 1 deletion shap/explainers/_deep/deep_tf.py
Expand Up @@ -3,6 +3,7 @@
from .._explainer import Explainer
from packaging import version
from ..tf_utils import _get_session, _get_graph, _get_model_inputs, _get_model_output
from ...utils._exceptions import DimensionError
keras = None
tf = None
tf_ops = None
Expand Down Expand Up @@ -173,7 +174,7 @@ def __init__(self, model, data, session=None, learning_phase_flags=None):
if noutputs is not None:
self.phi_symbolics = [None for i in range(noutputs)]
else:
raise Exception("The model output tensor to be explained cannot have a static shape in dim 1 of None!")
raise DimensionError("The model output tensor to be explained cannot have a static shape in dim 1 of None!")

def _get_model_output(self, model):
if len(model.layers[-1]._inbound_nodes) == 0:
Expand Down
6 changes: 3 additions & 3 deletions shap/explainers/_exact.py
Expand Up @@ -92,7 +92,7 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si

# make sure we have enough evals
if max_evals is not None and max_evals != "auto" and max_evals < 2**len(inds):
raise Exception(
raise ValueError(
f"It takes {2**len(inds)} masked evaluations to run the Exact explainer on this instance, but max_evals={max_evals}!"
)

Expand Down Expand Up @@ -131,14 +131,14 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si
_compute_grey_code_row_values_st(row_values, mask, inds, outputs, coeff, extended_delta_indexes, MaskedModel.delta_mask_noop_value)

elif interactions > 2:
raise Exception("Currently the Exact explainer does not support interactions higher than order 2!")
raise NotImplementedError("Currently the Exact explainer does not support interactions higher than order 2!")

# do a partition tree constrained version of Shapley values
else:

# make sure we have enough evals
if max_evals is not None and max_evals != "auto" and max_evals < len(fm)**2:
raise Exception(
raise ValueError(
f"It takes {len(fm)**2} masked evaluations to run the Exact explainer on this instance, but max_evals={max_evals}!"
)

Expand Down
9 changes: 5 additions & 4 deletions shap/explainers/_explainer.py
@@ -1,4 +1,5 @@
import copy
from sqlite3 import NotSupportedError
alexisdrakopoulos marked this conversation as resolved.
Show resolved Hide resolved
import time
import numpy as np
import scipy as sp
Expand All @@ -13,7 +14,7 @@
from .._serializable import Serializable
from .. import explainers
from .._serializable import Serializer, Deserializer

from ..utils._exceptions import InvalidAlgorithmError


class Explainer(Serializable):
Expand Down Expand Up @@ -132,7 +133,7 @@ def __init__(self, model, masker=None, link=links.identity, algorithm="auto", ou
if callable(link):
self.link = link
else:
raise Exception("The passed link function needs to be callable!")
raise TypeError("The passed link function needs to be callable!")
self.linearize_link = linearize_link

# if we are called directly (as opposed to through super()) then we convert ourselves to the subclass
Expand Down Expand Up @@ -170,7 +171,7 @@ def __init__(self, model, masker=None, link=links.identity, algorithm="auto", ou

# if we get here then we don't know how to handle what was given to us
else:
raise Exception("The passed model is not callable and cannot be analyzed directly with the given masker! Model: " + str(model))
raise TypeError("The passed model is not callable and cannot be analyzed directly with the given masker! Model: " + str(model))

# build the right subclass
if algorithm == "exact":
Expand All @@ -195,7 +196,7 @@ def __init__(self, model, masker=None, link=links.identity, algorithm="auto", ou
self.__class__ = explainers.Deep
explainers.Deep.__init__(self, self.model, self.masker, link=self.link, feature_names=self.feature_names, linearize_link=linearize_link, **kwargs)
else:
raise Exception("Unknown algorithm type passed: %s!" % algorithm)
raise InvalidAlgorithmError("Unknown algorithm type passed: %s!" % algorithm)


def __call__(self, *args, max_evals="auto", main_effects=False, error_bounds=False, batch_size="auto",
Expand Down
18 changes: 10 additions & 8 deletions shap/explainers/_linear.py
Expand Up @@ -4,6 +4,7 @@
from tqdm.autonotebook import tqdm
from ._explainer import Explainer
from ..utils import safe_isinstance
from ..utils._exceptions import InvalidFeaturePerturbationError, InvalidModelError, DimensionError
from .. import maskers
from .. import links

Expand Down Expand Up @@ -92,7 +93,7 @@ def __init__(self, model, masker, link=links.identity, nsamples=1000, feature_pe
elif issubclass(type(self.masker), maskers.Impute):
self.feature_perturbation = "correlation_dependent"
else:
raise Exception("The Linear explainer only supports the Independent, Partition, and Impute maskers right now!")
raise NotImplementedError("The Linear explainer only supports the Independent, Partition, and Impute maskers right now!")
data = getattr(self.masker, "data", None)

# convert DataFrame's to numpy arrays
Expand Down Expand Up @@ -120,12 +121,12 @@ def __init__(self, model, masker, link=links.identity, nsamples=1000, feature_pe
if safe_isinstance(self.cov, "pandas.core.frame.DataFrame"):
self.cov = self.cov.values
elif data is None:
raise Exception("A background data distribution must be provided!")
raise ValueError("A background data distribution must be provided!")
else:
if sp.sparse.issparse(data):
self.mean = np.array(np.mean(data, 0))[0]
if self.feature_perturbation != "interventional":
raise Exception("Only feature_perturbation = 'interventional' is supported for sparse data")
raise NotImplementedError("Only feature_perturbation = 'interventional' is supported for sparse data")
else:
self.mean = np.array(np.mean(data, 0)).flatten() # assumes it is an array
if self.feature_perturbation == "correlation_dependent":
Expand Down Expand Up @@ -173,7 +174,7 @@ def __init__(self, model, masker, link=links.identity, nsamples=1000, feature_pe
if nsamples != 1000:
warnings.warn("Setting nsamples has no effect when feature_perturbation = 'interventional'!")
else:
raise Exception("Unknown type of feature_perturbation provided: " + self.feature_perturbation)
raise InvalidFeaturePerturbationError("Unknown type of feature_perturbation provided: " + self.feature_perturbation)

def _estimate_transforms(self, nsamples):
""" Uses block matrix inversion identities to quickly estimate transforms.
Expand Down Expand Up @@ -261,7 +262,7 @@ def _parse_model(model):
coef = model.coef_
intercept = model.intercept_
else:
raise Exception("An unknown model type was passed: " + str(type(model)))
raise InvalidModelError("An unknown model type was passed: " + str(type(model)))

return coef,intercept

Expand Down Expand Up @@ -296,11 +297,12 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si
X = X.values

#assert str(type(X)).endswith("'numpy.ndarray'>"), "Unknown instance type: " + str(type(X))
assert len(X.shape) == 1 or len(X.shape) == 2, "Instance must have 1 or 2 dimensions!"
if len(X.shape) not in (1, 2):
raise DimensionError("Instance must have 1 or 2 dimensions! Not: %s" %len(X.shape))

if self.feature_perturbation == "correlation_dependent":
if sp.sparse.issparse(X):
raise Exception("Only feature_perturbation = 'interventional' is supported for sparse data")
raise InvalidFeaturePerturbationError("Only feature_perturbation = 'interventional' is supported for sparse data")
phi = np.matmul(np.matmul(X[:,self.valid_inds], self.avg_proj.T), self.x_transform.T) - self.mean_transformed
phi = np.matmul(phi, self.avg_proj)

Expand Down Expand Up @@ -363,7 +365,7 @@ def shap_values(self, X):

if self.feature_perturbation == "correlation_dependent":
if sp.sparse.issparse(X):
raise Exception("Only feature_perturbation = 'interventional' is supported for sparse data")
raise InvalidFeaturePerturbationError("Only feature_perturbation = 'interventional' is supported for sparse data")
phi = np.matmul(np.matmul(X[:,self.valid_inds], self.avg_proj.T), self.x_transform.T) - self.mean_transformed
phi = np.matmul(phi, self.avg_proj)

Expand Down
2 changes: 1 addition & 1 deletion shap/explainers/_partition.py
Expand Up @@ -148,7 +148,7 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si
# else:
fixed_context = None
elif fixed_context not in [0, 1, None]:
raise Exception("Unknown fixed_context value passed (must be 0, 1 or None): %s" %fixed_context)
raise ValueError("Unknown fixed_context value passed (must be 0, 1 or None): %s" %fixed_context)

# build a masked version of the model for the current input sample
fm = MaskedModel(self.model, self.masker, self.link, self.linearize_link, *row_args)
Expand Down
4 changes: 2 additions & 2 deletions shap/explainers/_permutation.py
Expand Up @@ -103,7 +103,7 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si
elif callable(self.masker.clustering):
row_clustering = self.masker.clustering(*row_args)
else:
raise Exception("The masker passed has a .clustering attribute that is not yet supported by the Permutation explainer!")
raise NotImplementedError("The masker passed has a .clustering attribute that is not yet supported by the Permutation explainer!")

# loop over many permutations
inds = fm.varying_inputs()
Expand Down Expand Up @@ -161,7 +161,7 @@ def explain_row(self, *row_args, max_evals, main_effects, error_bounds, batch_si
history_pos += 1

if npermutations == 0:
raise Exception(f"max_evals={max_evals} is too low for the Permutation explainer, it must be at least 2 * num_features + 1 = {2 * len(inds) + 1}!")
raise ValueError(f"max_evals={max_evals} is too low for the Permutation explainer, it must be at least 2 * num_features + 1 = {2 * len(inds) + 1}!")

expected_value = outputs[0]

Expand Down