627 lines
26 KiB
Python
627 lines
26 KiB
Python
"""Visualize cumulative SHAP values."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import matplotlib.cm as cm
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from ..utils import hclust_ordering
|
|
from ..utils._legacy import LogitLink, convert_to_link
|
|
from . import colors
|
|
from ._labels import labels
|
|
|
|
|
|
def __change_shap_base_value(base_value, new_base_value, shap_values) -> np.ndarray:
|
|
"""Shift SHAP base value to a new value. This function assumes that `base_value` and `new_base_value` are scalars
|
|
and that `shap_values` is a two or three dimensional array.
|
|
"""
|
|
# matrix of shap_values
|
|
if shap_values.ndim == 2:
|
|
return shap_values + (base_value - new_base_value) / shap_values.shape[1]
|
|
|
|
# cube of shap_interaction_values
|
|
main_effects = shap_values.shape[1]
|
|
all_effects = main_effects * (main_effects + 1) // 2
|
|
temp = (base_value - new_base_value) / all_effects / 2 # divided by 2 because interaction effects are halved
|
|
shap_values = shap_values + temp
|
|
# Add the other half to the main effects on the diagonal
|
|
idx = np.diag_indices_from(shap_values[0])
|
|
shap_values[:, idx[0], idx[1]] += temp
|
|
return shap_values
|
|
|
|
|
|
def __decision_plot_matplotlib(
|
|
base_value,
|
|
cumsum,
|
|
ascending,
|
|
feature_display_count,
|
|
features,
|
|
feature_names,
|
|
highlight,
|
|
plot_color,
|
|
axis_color,
|
|
y_demarc_color,
|
|
xlim,
|
|
alpha,
|
|
color_bar,
|
|
auto_size_plot,
|
|
title,
|
|
show,
|
|
legend_labels,
|
|
legend_location,
|
|
):
|
|
"""Matplotlib rendering for decision_plot()"""
|
|
# image size
|
|
row_height = 0.4
|
|
if auto_size_plot:
|
|
plt.gcf().set_size_inches(8, feature_display_count * row_height + 1.5)
|
|
|
|
# draw vertical line indicating center
|
|
plt.axvline(x=base_value, color="#999999", zorder=-1)
|
|
|
|
# draw horizontal dashed lines for each feature contribution
|
|
for i in range(1, feature_display_count):
|
|
plt.axhline(y=i, color=y_demarc_color, lw=0.5, dashes=(1, 5), zorder=-1)
|
|
|
|
# initialize highlighting
|
|
linestyle = np.array("-", dtype=object)
|
|
linestyle = np.repeat(linestyle, cumsum.shape[0])
|
|
linewidth = np.repeat(1, cumsum.shape[0])
|
|
if highlight is not None:
|
|
linestyle[highlight] = "-."
|
|
linewidth[highlight] = 2
|
|
|
|
# plot each observation's cumulative SHAP values.
|
|
ax = plt.gca()
|
|
ax.set_xlim(xlim)
|
|
m = cm.ScalarMappable(cmap=plot_color)
|
|
m.set_clim(xlim)
|
|
y_pos = np.arange(0, feature_display_count + 1)
|
|
lines = []
|
|
for i in range(cumsum.shape[0]):
|
|
o = plt.plot(
|
|
cumsum[i, :], y_pos, color=m.to_rgba(cumsum[i, -1], alpha), linewidth=linewidth[i], linestyle=linestyle[i]
|
|
)
|
|
lines.append(o[0])
|
|
|
|
# determine font size. if ' *\n' character sequence is found (as in interaction labels), use a smaller
|
|
# font. we don't shrink the font for all interaction plots because if an interaction term is not
|
|
# in the display window there is no need to shrink the font.
|
|
s = next((s for s in feature_names if " *\n" in s), None)
|
|
fontsize = 13 if s is None else 9
|
|
|
|
# if there is a single observation and feature values are supplied, print them.
|
|
if (cumsum.shape[0] == 1) and (features is not None):
|
|
renderer = plt.gcf().canvas.get_renderer() # type: ignore
|
|
inverter = plt.gca().transData.inverted()
|
|
y_pos = y_pos + 0.5
|
|
for i in range(feature_display_count):
|
|
v = features[0, i]
|
|
if isinstance(v, str):
|
|
v = f"({str(v).strip()})"
|
|
else:
|
|
v = "({})".format(f"{v:,.3f}".rstrip("0").rstrip("."))
|
|
t = ax.text(
|
|
np.max(cumsum[0, i : (i + 2)]),
|
|
y_pos[i],
|
|
" " + v,
|
|
fontsize=fontsize,
|
|
horizontalalignment="left",
|
|
verticalalignment="center_baseline",
|
|
color="#666666",
|
|
)
|
|
bb = inverter.transform_bbox(t.get_window_extent(renderer=renderer))
|
|
if bb.xmax > xlim[1]:
|
|
t.set_text(v + " ")
|
|
t.set_x(np.min(cumsum[0, i : (i + 2)]))
|
|
t.set_horizontalalignment("right")
|
|
bb = inverter.transform_bbox(t.get_window_extent(renderer=renderer))
|
|
if bb.xmin < xlim[0]:
|
|
t.set_text(v)
|
|
t.set_x(xlim[0])
|
|
t.set_horizontalalignment("left")
|
|
|
|
# style axes
|
|
ax.xaxis.set_ticks_position("both")
|
|
ax.yaxis.set_ticks_position("none")
|
|
ax.spines["right"].set_visible(False)
|
|
ax.spines["left"].set_visible(False)
|
|
ax.tick_params(color=axis_color, labelcolor=axis_color, labeltop=True)
|
|
plt.yticks(np.arange(feature_display_count) + 0.5, feature_names, fontsize=fontsize)
|
|
ax.tick_params("x", labelsize=11)
|
|
plt.ylim(0, feature_display_count)
|
|
plt.xlabel(labels["MODEL_OUTPUT"], fontsize=13)
|
|
|
|
# draw the color bar - must come after axes styling
|
|
if color_bar:
|
|
m = cm.ScalarMappable(cmap=plot_color)
|
|
m.set_array(np.array([0, 1]))
|
|
|
|
# place the colorbar
|
|
plt.ylim(0, feature_display_count + 0.25)
|
|
ax_cb = ax.inset_axes((xlim[0], feature_display_count, xlim[1] - xlim[0], 0.25), transform=ax.transData)
|
|
cb = plt.colorbar(m, ticks=[0, 1], orientation="horizontal", cax=ax_cb)
|
|
cb.set_ticklabels([])
|
|
cb.ax.tick_params(labelsize=11, length=0)
|
|
cb.set_alpha(alpha)
|
|
cb.outline.set_visible(False) # type: ignore
|
|
|
|
# re-activate the main axis for drawing.
|
|
plt.sca(ax)
|
|
|
|
if title:
|
|
# TODO decide on style/size
|
|
plt.title(title)
|
|
|
|
if ascending:
|
|
plt.gca().invert_yaxis()
|
|
|
|
if legend_labels is not None:
|
|
ax.legend(handles=lines, labels=legend_labels, loc=legend_location)
|
|
|
|
if show:
|
|
plt.show()
|
|
|
|
|
|
class DecisionPlotResult:
|
|
"""The optional return value of decision_plot.
|
|
|
|
The class attributes can be used to apply the same scale and feature ordering to other decision plots.
|
|
"""
|
|
|
|
def __init__(self, base_value, shap_values, feature_names, feature_idx, xlim):
|
|
"""Example
|
|
-------
|
|
Plot two decision plots using the same feature order and x-axis.
|
|
>>> range1, range2 = range(20), range(20, 40)
|
|
>>> r = decision_plot(base, shap_values[range1], features[range1], return_objects=True)
|
|
>>> decision_plot(base, shap_values[range2], features[range2], feature_order=r.feature_idx, xlim=r.xlim)
|
|
|
|
Parameters
|
|
----------
|
|
base_value : float
|
|
The base value used in the plot. For multioutput models,
|
|
this will be the mean of the base values. This will inherit `new_base_value` if specified.
|
|
|
|
shap_values : numpy.ndarray
|
|
The `shap_values` passed to decision_plot re-ordered based on `feature_order`. If SHAP interaction values
|
|
are passed to decision_plot, `shap_values` is a 2D (matrix) representation of the interactions. See
|
|
`feature_names` to locate the feature positions. If `new_base_value` is specified, the SHAP values are
|
|
relative to the new base value.
|
|
|
|
feature_names : list of str
|
|
The feature names used in the plot in the order specified in the decision_plot parameter `feature_order`.
|
|
|
|
feature_idx : numpy.ndarray
|
|
The index used to order `shap_values` based on `feature_order`. This attribute can be used to specify
|
|
identical feature ordering in multiple decision plots.
|
|
|
|
xlim : tuple[float, float]
|
|
The x-axis limits. This attributed can be used to specify the same x-axis in multiple decision plots.
|
|
|
|
"""
|
|
self.base_value = base_value
|
|
self.shap_values = shap_values
|
|
self.feature_names = feature_names
|
|
self.feature_idx = feature_idx
|
|
self.xlim = xlim
|
|
|
|
|
|
def decision(
|
|
base_value: float | np.ndarray,
|
|
shap_values: np.ndarray,
|
|
features: np.ndarray | pd.Series | pd.DataFrame | list | None = None,
|
|
feature_names=None,
|
|
feature_order="importance",
|
|
feature_display_range=None,
|
|
highlight=None,
|
|
link="identity",
|
|
plot_color=None,
|
|
axis_color="#333333",
|
|
y_demarc_color="#333333",
|
|
alpha=None,
|
|
color_bar=True,
|
|
auto_size_plot=True,
|
|
title=None,
|
|
xlim=None,
|
|
show=True,
|
|
return_objects=False,
|
|
ignore_warnings=False,
|
|
new_base_value=None,
|
|
legend_labels=None,
|
|
legend_location="best",
|
|
) -> DecisionPlotResult | None:
|
|
"""Visualize model decisions using cumulative SHAP values.
|
|
|
|
Each plotted line explains a single model prediction. If a single prediction is plotted, feature values will be
|
|
printed in the plot (if supplied). If multiple predictions are plotted together, feature values will not be printed.
|
|
Plotting too many predictions together will make the plot unintelligible.
|
|
|
|
Parameters
|
|
----------
|
|
base_value : float or numpy.ndarray
|
|
This is the reference value that the feature contributions start from. Usually, this is
|
|
``explainer.expected_value``.
|
|
|
|
shap_values : numpy.ndarray
|
|
Matrix of SHAP values (# features) or (# samples x # features) from
|
|
``explainer.shap_values()``. Or cube of SHAP interaction values (# samples x
|
|
# features x # features) from ``explainer.shap_interaction_values()``.
|
|
|
|
features : numpy.array or pandas.Series or pandas.DataFrame or numpy.ndarray or list
|
|
Matrix of feature values (# features) or (# samples x # features). This provides the values of all the
|
|
features and, optionally, the feature names.
|
|
|
|
feature_names : list or numpy.ndarray
|
|
List of feature names (# features). If ``None``, names may be derived from the
|
|
``features`` argument if a Pandas object is provided. Otherwise, numeric feature
|
|
names will be generated.
|
|
|
|
feature_order : str or None or list or numpy.ndarray
|
|
Any of "importance" (the default), "hclust" (hierarchical clustering), ``None``,
|
|
or a list/array of indices.
|
|
|
|
feature_display_range: slice or range
|
|
The slice or range of features to plot after ordering features by ``feature_order``. A step of 1 or ``None``
|
|
will display the features in ascending order. A step of -1 will display the features in descending order. If
|
|
``feature_display_range=None``, ``slice(-1, -21, -1)`` is used (i.e. show the last 20 features in descending order).
|
|
If ``shap_values`` contains interaction values, the number of features is automatically expanded to include all
|
|
possible interactions: N(N + 1)/2 where N = ``shap_values.shape[1]``.
|
|
|
|
highlight : Any
|
|
Specify which observations to draw in a different line style. All numpy indexing methods are supported. For
|
|
example, list of integer indices, or a bool array.
|
|
|
|
link : str
|
|
Use "identity" or "logit" to specify the transformation used for the x-axis. The "logit" link transforms
|
|
log-odds into probabilities.
|
|
|
|
plot_color : str or matplotlib.colors.ColorMap
|
|
Color spectrum used to draw the plot lines. If ``str``, a registered matplotlib color name is assumed.
|
|
|
|
axis_color : str or int
|
|
Color used to draw plot axes.
|
|
|
|
y_demarc_color : str or int
|
|
Color used to draw feature demarcation lines on the y-axis.
|
|
|
|
alpha : float
|
|
Alpha blending value in [0, 1] used to draw plot lines.
|
|
|
|
color_bar : bool
|
|
Whether to draw the color bar (legend).
|
|
|
|
auto_size_plot : bool
|
|
Whether to automatically size the matplotlib plot to fit the number of features
|
|
displayed. If ``False``, specify the plot size using matplotlib before calling
|
|
this function.
|
|
|
|
title : str
|
|
Title of the plot.
|
|
|
|
xlim: tuple[float, float]
|
|
The extents of the x-axis (e.g. ``(-1.0, 1.0)``). If not specified, the limits
|
|
are determined by the maximum/minimum predictions centered around base_value
|
|
when ``link="identity"``. When ``link="logit"``, the x-axis extents are ``(0,
|
|
1)`` centered at 0.5. ``xlim`` values are not transformed by the ``link``
|
|
function. This argument is provided to simplify producing multiple plots on the
|
|
same scale for comparison.
|
|
|
|
show : bool
|
|
Whether :external+mpl:func:`matplotlib.pyplot.show()` is called before returning.
|
|
Setting this to ``False`` allows the plot
|
|
to be customized further after it has been created.
|
|
|
|
return_objects : bool
|
|
Whether to return a :obj:`DecisionPlotResult` object containing various plotting
|
|
features. This can be used to generate multiple decision plots using the same
|
|
feature ordering and scale.
|
|
|
|
ignore_warnings : bool
|
|
Plotting many data points or too many features at a time may be slow, or may create very large plots. Set
|
|
this argument to ``True`` to override hard-coded limits that prevent plotting large amounts of data.
|
|
|
|
new_base_value : float
|
|
SHAP values are relative to a base value. By default, this base value is the
|
|
expected value of the model's raw predictions. Use ``new_base_value`` to shift
|
|
the base value to an arbitrary value (e.g. the cutoff point for a binary
|
|
classification task).
|
|
|
|
legend_labels : list of str
|
|
List of legend labels. If ``None``, legend will not be shown.
|
|
|
|
legend_location : str
|
|
Legend location. Any of "best", "upper right", "upper left", "lower left", "lower right", "right",
|
|
"center left", "center right", "lower center", "upper center", "center".
|
|
|
|
Returns
|
|
-------
|
|
DecisionPlotResult or None
|
|
Returns a :obj:`DecisionPlotResult` object if ``return_objects=True``. Returns ``None`` otherwise (the default).
|
|
|
|
Examples
|
|
--------
|
|
Plot two decision plots using the same feature order and x-axis.
|
|
|
|
>>> range1, range2 = range(20), range(20, 40)
|
|
>>> r = decision_plot(base, shap_values[range1], features[range1], return_objects=True)
|
|
>>> decision_plot(base, shap_values[range2], features[range2], feature_order=r.feature_idx, xlim=r.xlim)
|
|
|
|
See more `decision plot examples here <https://shap.readthedocs.io/en/latest/example_notebooks/api_examples/plots/decision_plot.html>`_.
|
|
|
|
"""
|
|
# code taken from force_plot. auto unwrap the base_value
|
|
if isinstance(base_value, np.ndarray) and len(base_value) == 1:
|
|
base_value = base_value[0]
|
|
|
|
if isinstance(base_value, list) or isinstance(shap_values, list):
|
|
raise TypeError(
|
|
"Looks like multi output. Try base_value[i] and shap_values[i], or use shap.multioutput_decision_plot()."
|
|
)
|
|
|
|
# validate shap_values
|
|
if not isinstance(shap_values, np.ndarray):
|
|
raise TypeError("The shap_values arg is the wrong type. Try explainer.shap_values().")
|
|
|
|
# calculate the various dimensions involved (observations, features, interactions, display, etc.
|
|
if shap_values.ndim == 1:
|
|
shap_values = shap_values.reshape(1, -1)
|
|
observation_count = shap_values.shape[0]
|
|
feature_count = shap_values.shape[1]
|
|
|
|
# code taken from force_plot. convert features from other types.
|
|
if isinstance(features, pd.DataFrame):
|
|
if feature_names is None:
|
|
feature_names = features.columns.to_list()
|
|
features = features.values
|
|
elif isinstance(features, pd.Series):
|
|
if feature_names is None:
|
|
feature_names = features.index.to_list()
|
|
features = features.values
|
|
elif isinstance(features, list):
|
|
if feature_names is None:
|
|
feature_names = features
|
|
features = None
|
|
elif features is not None and features.ndim == 1 and feature_names is None:
|
|
feature_names = features.tolist()
|
|
features = None
|
|
|
|
# the above code converts features to either None or np.ndarray. if features is something else at this point,
|
|
# there's a problem.
|
|
if not isinstance(features, (np.ndarray, type(None))):
|
|
raise TypeError("The features arg uses an unsupported type.")
|
|
if (features is not None) and (features.ndim == 1):
|
|
features = features.reshape(1, -1)
|
|
|
|
# validate/generate feature_names. at this point, feature_names does not include interactions.
|
|
if feature_names is None:
|
|
feature_names = [labels["FEATURE"] % str(i) for i in range(feature_count)]
|
|
elif len(feature_names) != feature_count:
|
|
raise ValueError("The feature_names arg must include all features represented in shap_values.")
|
|
elif not isinstance(feature_names, (list, np.ndarray)):
|
|
raise TypeError("The feature_names arg requires a list or numpy array.")
|
|
|
|
# transform interactions cube to a matrix and generate interaction names.
|
|
if shap_values.ndim == 3:
|
|
# flatten
|
|
triu_count = feature_count * (feature_count - 1) // 2
|
|
idx_diag = np.diag_indices_from(shap_values[0])
|
|
idx_triu = np.triu_indices_from(shap_values[0], 1)
|
|
a: np.ndarray = np.ndarray((observation_count, feature_count + triu_count), shap_values.dtype)
|
|
a[:, :feature_count] = shap_values[:, idx_diag[0], idx_diag[1]]
|
|
a[:, feature_count:] = shap_values[:, idx_triu[0], idx_triu[1]] * 2
|
|
shap_values = a
|
|
# names
|
|
b: list[str | None] = [None] * shap_values.shape[1]
|
|
b[:feature_count] = feature_names
|
|
for i, row, col in zip(range(feature_count, shap_values.shape[1]), idx_triu[0], idx_triu[1]):
|
|
b[i] = f"{feature_names[row]} *\n{feature_names[col]}"
|
|
feature_names = b
|
|
feature_count = shap_values.shape[1]
|
|
features = None # Can't use feature values for interactions...
|
|
|
|
# determine feature order
|
|
if isinstance(feature_order, list):
|
|
feature_idx = np.array(feature_order)
|
|
elif isinstance(feature_order, np.ndarray):
|
|
feature_idx = feature_order
|
|
elif (feature_order is None) or (feature_order.lower() == "none"):
|
|
feature_idx = np.arange(feature_count)
|
|
elif feature_order == "importance":
|
|
feature_idx = np.argsort(np.sum(np.abs(shap_values), axis=0))
|
|
elif feature_order == "hclust":
|
|
feature_idx = np.array(hclust_ordering(shap_values.transpose()))
|
|
else:
|
|
raise ValueError(
|
|
"The feature_order arg requires 'importance', 'hclust', 'none', or an integer list/array "
|
|
"of feature indices."
|
|
)
|
|
|
|
if (feature_idx.shape != (feature_count,)) or (not np.issubdtype(feature_idx.dtype, np.integer)):
|
|
raise ValueError(
|
|
"A list or array has been specified for the feature_order arg. The length must match the "
|
|
"feature count and the data type must be integer."
|
|
)
|
|
|
|
# validate and convert feature_display_range to a slice. prevents out of range errors later.
|
|
if feature_display_range is None:
|
|
feature_display_range = slice(-1, -21, -1) # show last 20 features in descending order.
|
|
elif not isinstance(feature_display_range, (slice, range)):
|
|
raise TypeError("The feature_display_range arg requires a slice or a range.")
|
|
elif feature_display_range.step not in (-1, 1, None):
|
|
raise ValueError("The feature_display_range arg supports a step of 1, -1, or None.")
|
|
elif isinstance(feature_display_range, range):
|
|
# Negative values in a range are not the same as negs in a slice. Consider range(2, -1, -1) == [2, 1, 0],
|
|
# but slice(2, -1, -1) == [] when len(features) > 2. However, range(2, -1, -1) == slice(2, -inf, -1) after
|
|
# clipping.
|
|
c = np.iinfo(np.integer).min
|
|
feature_display_range = slice(
|
|
feature_display_range.start if feature_display_range.start >= 0 else c, # should never happen, but...
|
|
feature_display_range.stop if feature_display_range.stop >= 0 else c,
|
|
feature_display_range.step,
|
|
)
|
|
|
|
# apply new_base_value
|
|
if new_base_value is not None:
|
|
shap_values = __change_shap_base_value(base_value, new_base_value, shap_values)
|
|
base_value = new_base_value
|
|
|
|
# use feature_display_range to determine which features will be plotted. convert feature_display_range to
|
|
# ascending indices and expand by one in the negative direction. why? we are plotting the change in prediction
|
|
# for every feature. this requires that we include the value previous to the first displayed feature
|
|
# (i.e. i_0 - 1 to i_n).
|
|
d = feature_display_range.indices(feature_count)
|
|
ascending = True
|
|
if d[2] == -1: # The step
|
|
ascending = False
|
|
d = (d[1] + 1, d[0] + 1, 1)
|
|
feature_display_count = d[1] - d[0]
|
|
shap_values = shap_values[:, feature_idx]
|
|
if d[0] == 0:
|
|
cumsum: np.ndarray = np.ndarray((observation_count, feature_display_count + 1), shap_values.dtype)
|
|
cumsum[:, 0] = base_value
|
|
cumsum[:, 1:] = base_value + np.nancumsum(shap_values[:, 0 : d[1]], axis=1)
|
|
else:
|
|
cumsum = base_value + np.nancumsum(shap_values, axis=1)[:, (d[0] - 1) : d[1]]
|
|
|
|
# Select and sort feature names and features according to the range selected above
|
|
feature_names = np.array(feature_names)
|
|
feature_names_display = feature_names[feature_idx[d[0] : d[1]]].tolist()
|
|
feature_names = feature_names[feature_idx].tolist()
|
|
features_display = None if features is None else features[:, feature_idx[d[0] : d[1]]]
|
|
|
|
# throw large data errors
|
|
if not ignore_warnings:
|
|
if observation_count > 2000:
|
|
raise RuntimeError(
|
|
f"Plotting {observation_count} observations may be slow. Consider subsampling or set "
|
|
"ignore_warnings=True to ignore this message."
|
|
)
|
|
if feature_display_count > 200:
|
|
raise RuntimeError(
|
|
f"Plotting {feature_display_count} features may create a very large plot. Set "
|
|
"ignore_warnings=True to ignore this "
|
|
"message."
|
|
)
|
|
if feature_count * observation_count > 100000000:
|
|
raise RuntimeError(
|
|
f"Processing SHAP values for {feature_count} features over {observation_count} observations may be slow. Set "
|
|
"ignore_warnings=True to ignore this "
|
|
"message."
|
|
)
|
|
|
|
# convert values based on link and update x-axis extents
|
|
create_xlim = xlim is None
|
|
link = convert_to_link(link)
|
|
base_value_saved = base_value
|
|
if isinstance(link, LogitLink):
|
|
base_value = link.finv(base_value)
|
|
cumsum = link.finv(cumsum)
|
|
if create_xlim:
|
|
# Expand [0, 1] limits a little for a visual margin
|
|
xlim = (-0.02, 1.02)
|
|
elif create_xlim:
|
|
xmin: float = min((cumsum.min(), base_value))
|
|
xmax: float = max((cumsum.max(), base_value))
|
|
# create a symmetric axis around base_value
|
|
n, m = (base_value - xmin), (xmax - base_value)
|
|
if n > m:
|
|
xlim = (base_value - n, base_value + m)
|
|
else:
|
|
xlim = (base_value - m, base_value + m)
|
|
# Adjust xlim to include a little visual margin.
|
|
e = (xlim[1] - xlim[0]) * 0.02
|
|
xlim = (xlim[0] - e, xlim[1] + e)
|
|
|
|
# Initialize style arguments
|
|
if alpha is None:
|
|
alpha = 1.0
|
|
|
|
if plot_color is None:
|
|
plot_color = colors.red_blue
|
|
|
|
__decision_plot_matplotlib(
|
|
base_value,
|
|
cumsum,
|
|
ascending,
|
|
feature_display_count,
|
|
features_display,
|
|
feature_names_display,
|
|
highlight,
|
|
plot_color,
|
|
axis_color,
|
|
y_demarc_color,
|
|
xlim,
|
|
alpha,
|
|
color_bar,
|
|
auto_size_plot,
|
|
title,
|
|
show,
|
|
legend_labels,
|
|
legend_location,
|
|
)
|
|
|
|
if not return_objects:
|
|
return None
|
|
|
|
return DecisionPlotResult(base_value_saved, shap_values, feature_names, feature_idx, xlim)
|
|
|
|
|
|
def multioutput_decision(base_values, shap_values, row_index, **kwargs) -> DecisionPlotResult | None:
|
|
"""Decision plot for multioutput models.
|
|
|
|
Plots all outputs for a single observation. By default, the plotted base value will be the mean of base_values
|
|
unless new_base_value is specified. Supports both SHAP values and SHAP interaction values.
|
|
|
|
Parameters
|
|
----------
|
|
base_values : list of float
|
|
This is the reference value that the feature contributions start from. Use explainer.expected_value.
|
|
|
|
shap_values : list of numpy.ndarray
|
|
A multioutput list of SHAP matrices or SHAP cubes from explainer.shap_values() or
|
|
explainer.shap_interaction_values(), respectively.
|
|
|
|
row_index : int
|
|
The integer index of the row to plot.
|
|
|
|
**kwargs : Any
|
|
Arguments to be passed on to decision_plot().
|
|
|
|
Returns
|
|
-------
|
|
DecisionPlotResult or None
|
|
Returns a DecisionPlotResult object if `return_objects=True`. Returns `None` otherwise (the default).
|
|
|
|
"""
|
|
# todo: adjust to breaking changes made in #3318
|
|
if not (isinstance(base_values, list) and isinstance(shap_values, list)):
|
|
raise ValueError("The base_values and shap_values args expect lists.")
|
|
|
|
# convert arguments to arrays for simpler handling
|
|
base_values = np.array(base_values)
|
|
if not ((base_values.ndim == 1) or (np.issubdtype(base_values.dtype, np.number))):
|
|
raise ValueError("The base_values arg should be a list of scalars.")
|
|
shap_values = np.array(shap_values)
|
|
if shap_values.ndim not in [3, 4]:
|
|
raise ValueError("The shap_values arg should be a list of two or three dimensional SHAP arrays.")
|
|
if shap_values.shape[0] != base_values.shape[0]:
|
|
raise ValueError("The base_values output length is different than shap_values.")
|
|
|
|
# shift shap base values to mean of base values
|
|
base_values_mean = base_values.mean()
|
|
for i in range(shap_values.shape[0]):
|
|
shap_values[i] = __change_shap_base_value(base_values[i], base_values_mean, shap_values[i])
|
|
|
|
# select the feature row corresponding to row_index
|
|
if (kwargs is not None) and ("features" in kwargs):
|
|
features = kwargs["features"]
|
|
if isinstance(features, np.ndarray) and (features.ndim == 2):
|
|
kwargs["features"] = features[[row_index]]
|
|
elif isinstance(features, pd.DataFrame):
|
|
kwargs["features"] = features.iloc[row_index]
|
|
|
|
return decision(base_values_mean, shap_values[:, row_index, :], **kwargs)
|