207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
from .. import Explanation
|
|
from ..utils import OpChain
|
|
from . import colors
|
|
from ._labels import labels
|
|
from ._utils import convert_ordering
|
|
|
|
|
|
def heatmap(
|
|
shap_values: Explanation,
|
|
instance_order=Explanation.hclust(), # type: ignore
|
|
feature_values=Explanation.abs.mean(0), # type: ignore
|
|
feature_order=None,
|
|
max_display=10,
|
|
cmap=colors.red_white_blue,
|
|
show=True,
|
|
plot_width=8,
|
|
ax=None,
|
|
):
|
|
"""Create a heatmap plot of a set of SHAP values.
|
|
|
|
This plot is designed to show the population substructure of a dataset using supervised
|
|
clustering and a heatmap.
|
|
Supervised clustering involves clustering data points not by their original
|
|
feature values but by their explanations.
|
|
By default, we cluster using :func:`shap.utils.hclust_ordering`,
|
|
but any clustering can be used to order the samples.
|
|
|
|
Parameters
|
|
----------
|
|
shap_values : shap.Explanation
|
|
A multi-row :class:`.Explanation` object that we want to visualize in a
|
|
cluster ordering.
|
|
|
|
instance_order : OpChain or numpy.ndarray
|
|
A function that returns a sort ordering given a matrix of SHAP values and an axis, or
|
|
a direct sample ordering given as an ``numpy.ndarray``.
|
|
|
|
feature_values : OpChain or numpy.ndarray
|
|
A function that returns a global summary value for each input feature, or an array of such values.
|
|
|
|
feature_order : None, OpChain, or numpy.ndarray
|
|
A function that returns a sort ordering given a matrix of SHAP values and an axis, or
|
|
a direct input feature ordering given as an ``numpy.ndarray``.
|
|
If ``None``, then we use ``feature_values.argsort``.
|
|
|
|
max_display : int
|
|
The maximum number of features to display (default is 10).
|
|
|
|
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.
|
|
|
|
plot_width : int, default 8
|
|
The width of the heatmap plot.
|
|
|
|
ax : matplotlib Axes
|
|
Axes object to draw the plot onto, otherwise uses the current Axes.
|
|
|
|
Returns
|
|
-------
|
|
ax: matplotlib Axes
|
|
Returns the :external+mpl:class:`~matplotlib.axes.Axes` object with the plot drawn onto it.
|
|
|
|
Examples
|
|
--------
|
|
See `heatmap plot examples <https://shap.readthedocs.io/en/latest/example_notebooks/api_examples/plots/heatmap.html>`_.
|
|
|
|
"""
|
|
# sort the SHAP values matrix by rows and columns
|
|
values = shap_values.values
|
|
if issubclass(type(feature_values), OpChain):
|
|
feature_values = feature_values.apply(Explanation(values))
|
|
if issubclass(type(feature_values), Explanation):
|
|
feature_values = feature_values.values
|
|
if feature_order is None:
|
|
feature_order = np.argsort(-feature_values)
|
|
elif issubclass(type(feature_order), OpChain):
|
|
feature_order = feature_order.apply(Explanation(values))
|
|
elif not hasattr(feature_order, "__len__"):
|
|
raise Exception(f"Unsupported feature_order: {str(feature_order)}!")
|
|
xlabel = "Instances"
|
|
instance_order = convert_ordering(instance_order, shap_values)
|
|
# if issubclass(type(instance_order), OpChain):
|
|
# #xlabel += " " + instance_order.summary_string("SHAP values")
|
|
# instance_order = instance_order.apply(Explanation(values))
|
|
# elif not hasattr(instance_order, "__len__"):
|
|
# raise Exception("Unsupported instance_order: %s!" % str(instance_order))
|
|
# else:
|
|
# instance_order_ops = None
|
|
|
|
feature_names = np.array(shap_values.feature_names)[feature_order]
|
|
values = shap_values.values[instance_order][:, feature_order]
|
|
feature_values = feature_values[feature_order]
|
|
|
|
# if we have more features than `max_display`, then group all the excess features
|
|
# into a single feature
|
|
if values.shape[1] > max_display:
|
|
new_values = np.zeros((values.shape[0], max_display))
|
|
new_values[:, :-1] = values[:, : max_display - 1]
|
|
new_values[:, -1] = values[:, max_display - 1 :].sum(1)
|
|
new_feature_values = np.zeros(max_display)
|
|
new_feature_values[:-1] = feature_values[: max_display - 1]
|
|
new_feature_values[-1] = feature_values[max_display - 1 :].sum()
|
|
feature_names = [
|
|
*feature_names[: max_display - 1],
|
|
f"Sum of {values.shape[1] - max_display + 1} other features",
|
|
]
|
|
values = new_values
|
|
feature_values = new_feature_values
|
|
|
|
# define the plot size based on how many features we are plotting
|
|
row_height = 0.5
|
|
if ax is None:
|
|
plt.gcf().set_size_inches(plot_width, values.shape[1] * row_height + 2.5)
|
|
ax = plt.gca()
|
|
|
|
# plot the matrix of SHAP values as a heat map
|
|
vmin, vmax = np.nanpercentile(values.flatten(), [1, 99])
|
|
ax.imshow(
|
|
values.T,
|
|
aspect=0.7 * values.shape[0] / values.shape[1],
|
|
interpolation="nearest",
|
|
vmin=min(vmin, -vmax),
|
|
vmax=max(-vmin, vmax),
|
|
cmap=cmap,
|
|
)
|
|
|
|
# adjust the axes ticks and spines for the heat map + f(x) line chart
|
|
ax.xaxis.set_ticks_position("bottom")
|
|
ax.yaxis.set_ticks_position("left")
|
|
ax.spines[["left", "right"]].set_visible(True)
|
|
ax.spines[["left", "right"]].set_bounds(values.shape[1] - row_height, -row_height)
|
|
ax.spines[["top", "bottom"]].set_visible(False)
|
|
ax.tick_params(axis="both", direction="out")
|
|
|
|
ax.set_ylim(values.shape[1] - row_height, -3)
|
|
heatmap_yticks_pos = np.arange(values.shape[1])
|
|
heatmap_yticks_labels = feature_names
|
|
ax.yaxis.set_ticks(
|
|
[-1.5, *heatmap_yticks_pos],
|
|
[r"$f(x)$", *heatmap_yticks_labels],
|
|
fontsize=13,
|
|
)
|
|
# remove the y-tick line for the f(x) label
|
|
ax.yaxis.get_ticklines()[0].set_visible(False)
|
|
|
|
ax.set_xlim(-0.5, values.shape[0] - 0.5)
|
|
ax.set_xlabel(xlabel)
|
|
|
|
# plot the f(x) line chart above the heat map
|
|
ax.axhline(-1.5, color="#aaaaaa", linestyle="--", linewidth=0.5)
|
|
fx = values.T.sum(0)
|
|
fx_max = np.abs(fx).max()
|
|
if fx_max > 0:
|
|
fx_normalized = fx / fx_max
|
|
else:
|
|
fx_normalized = fx # all zeros, flat line — no division needed
|
|
ax.plot(
|
|
-fx_normalized - 1.5,
|
|
color="#000000",
|
|
linewidth=1,
|
|
)
|
|
|
|
# plot the bar plot on the right spine of the heat map
|
|
fv_max = np.abs(feature_values).max()
|
|
bar_widths = (feature_values / fv_max) * values.shape[0] / 20 if fv_max > 0 else np.zeros_like(feature_values)
|
|
bar_container = ax.barh(
|
|
heatmap_yticks_pos,
|
|
bar_widths,
|
|
height=0.7,
|
|
align="center",
|
|
color="#000000",
|
|
left=values.shape[0] * 1.0 - 0.5,
|
|
)
|
|
for b in bar_container:
|
|
b.set_clip_on(False)
|
|
|
|
# draw the color bar
|
|
import matplotlib.cm as cm
|
|
|
|
m = cm.ScalarMappable(cmap=cmap)
|
|
m.set_array([min(vmin, -vmax), max(-vmin, vmax)])
|
|
cb = plt.colorbar(
|
|
m,
|
|
ticks=[min(vmin, -vmax), max(-vmin, vmax)],
|
|
ax=ax,
|
|
aspect=80,
|
|
fraction=0.01,
|
|
pad=0.10, # padding between the cb and the main axes
|
|
)
|
|
cb.set_label(labels["VALUE"], size=12, labelpad=-10)
|
|
cb.ax.tick_params(labelsize=11, length=0)
|
|
cb.set_alpha(1)
|
|
cb.outline.set_visible(False) # type: ignore
|
|
# bbox = cb.ax.get_window_extent().transformed(plt.gcf().dpi_scale_trans.inverted())
|
|
# cb.ax.set_aspect((bbox.height - 0.9) * 15)
|
|
# cb.draw_all()
|
|
|
|
if show:
|
|
plt.show()
|
|
|
|
return ax
|