Files
2026-07-13 13:22:52 +08:00

738 lines
28 KiB
Python

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from .. import Explanation
from ..utils import format_value
from ._labels import labels
from ._style import get_style
# TODO: If we make a JS version of this plot then we could let users click on a bar and then see the dependence
# plot that is associated with that feature get overlaid on the plot...it would quickly allow users to answer
# why a feature is pushing down or up. Perhaps the best way to do this would be with an ICE plot hanging off
# of the bar...
def waterfall(shap_values, max_display=10, show=True):
"""Plots an explanation of a single prediction as a waterfall plot.
The SHAP value of a feature represents the impact of the evidence provided by that feature on the model's
output. The waterfall plot is designed to visually display how the SHAP values (evidence) of each feature
move the model output from our prior expectation under the background data distribution, to the final model
prediction given the evidence of all the features.
Features are sorted by the magnitude of their SHAP values with the smallest
magnitude features grouped together at the bottom of the plot when the number of
features in the models exceeds the ``max_display`` parameter.
Parameters
----------
shap_values : Explanation
A one-dimensional :class:`.Explanation` object that contains the feature values and SHAP values to plot.
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, returning the current axis via plt.gca().
Examples
--------
See `waterfall plot examples <https://shap.readthedocs.io/en/latest/example_notebooks/api_examples/plots/waterfall.html>`_.
"""
style = get_style()
# Turn off interactive plot
if show is False:
plt.ioff()
# make sure the input is an Explanation object
if not isinstance(shap_values, Explanation):
emsg = "The waterfall plot requires an `Explanation` object as the `shap_values` argument."
raise TypeError(emsg)
# make sure we only have a single explanation to plot
sv_shape = shap_values.shape
if len(sv_shape) != 1:
emsg = (
"The waterfall plot can currently only plot a single explanation, but a "
f"matrix of explanations (shape {sv_shape}) was passed! Perhaps try "
"`shap.plots.waterfall(shap_values[0])` or for multi-output models, "
"try `shap.plots.waterfall(shap_values[0, 0])`."
)
raise ValueError(emsg)
base_values = float(shap_values.base_values)
features = shap_values.display_data if shap_values.display_data is not None else shap_values.data
feature_names = shap_values.feature_names
lower_bounds = getattr(shap_values, "lower_bounds", None)
upper_bounds = getattr(shap_values, "upper_bounds", None)
values = shap_values.values
# unwrap pandas series
if isinstance(features, pd.Series):
if feature_names is None:
feature_names = list(features.index)
features = features.values
# fallback feature names
if feature_names is None:
feature_names = np.array([labels["FEATURE"] % str(i) for i in range(len(values))])
# init variables we use for tracking the plot locations
num_features = min(max_display, len(values))
row_height = 0.5
rng = range(num_features - 1, -1, -1)
order = np.argsort(-np.abs(values))
pos_lefts = []
pos_inds = []
pos_widths = []
pos_low = []
pos_high = []
neg_lefts = []
neg_inds = []
neg_widths = []
neg_low = []
neg_high = []
loc = base_values + values.sum()
yticklabels = ["" for _ in range(num_features + 1)]
# size the plot based on how many features we are plotting
plt.gcf().set_size_inches(8, num_features * row_height + 1.5)
# see how many individual (vs. grouped at the end) features we are plotting
if num_features == len(values):
num_individual = num_features
else:
num_individual = num_features - 1
# compute the locations of the individual features and plot the dashed connecting lines
for i in range(num_individual):
sval = values[order[i]]
loc -= sval
if sval >= 0:
pos_inds.append(rng[i])
pos_widths.append(sval)
if lower_bounds is not None:
pos_low.append(lower_bounds[order[i]])
pos_high.append(upper_bounds[order[i]])
pos_lefts.append(loc)
else:
neg_inds.append(rng[i])
neg_widths.append(sval)
if lower_bounds is not None:
neg_low.append(lower_bounds[order[i]])
neg_high.append(upper_bounds[order[i]])
neg_lefts.append(loc)
if num_individual != num_features or i + 4 < num_individual:
plt.plot(
[loc, loc],
[rng[i] - 1 - 0.4, rng[i] + 0.4],
color=style.vlines_color,
linestyle="--",
linewidth=0.5,
zorder=-1,
)
if features is None:
yticklabels[rng[i]] = feature_names[order[i]]
else:
if np.issubdtype(type(features[order[i]]), np.number):
yticklabels[rng[i]] = (
format_value(float(features[order[i]]), "%0.03f") + " = " + str(feature_names[order[i]])
)
else:
yticklabels[rng[i]] = str(features[order[i]]) + " = " + str(feature_names[order[i]])
# add a last grouped feature to represent the impact of all the features we didn't show
if num_features < len(values):
yticklabels[0] = f"{len(shap_values) - num_features + 1} other features"
remaining_impact = base_values - loc
if remaining_impact < 0:
pos_inds.append(0)
pos_widths.append(-remaining_impact)
pos_lefts.append(loc + remaining_impact)
else:
neg_inds.append(0)
neg_widths.append(-remaining_impact)
neg_lefts.append(loc + remaining_impact)
points = (
pos_lefts
+ list(np.array(pos_lefts) + np.array(pos_widths))
+ neg_lefts
+ list(np.array(neg_lefts) + np.array(neg_widths))
)
dataw = np.max(points) - np.min(points)
# draw invisible bars just for sizing the axes
label_padding = np.array([0.1 * dataw if w < 1 else 0 for w in pos_widths])
plt.barh(
pos_inds,
np.array(pos_widths) + label_padding + 0.02 * dataw,
left=np.array(pos_lefts) - 0.01 * dataw,
color=style.primary_color_positive,
alpha=0,
)
label_padding = np.array([-0.1 * dataw if -w < 1 else 0 for w in neg_widths])
plt.barh(
neg_inds,
np.array(neg_widths) + label_padding - 0.02 * dataw,
left=np.array(neg_lefts) + 0.01 * dataw,
color=style.primary_color_negative,
alpha=0,
)
# define variable we need for plotting the arrows
head_length = 0.08
bar_width = 0.8
xlen = plt.xlim()[1] - plt.xlim()[0]
fig = plt.gcf()
ax = plt.gca()
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width = bbox.width
bbox_to_xscale = xlen / width
hl_scaled = bbox_to_xscale * head_length
renderer = fig.canvas.get_renderer()
# draw the positive arrows
for i in range(len(pos_inds)):
dist = pos_widths[i]
arrow_obj = plt.arrow(
pos_lefts[i],
pos_inds[i],
dist - hl_scaled,
0,
head_length=min(dist, hl_scaled),
color=style.primary_color_positive,
width=bar_width,
head_width=bar_width,
)
if pos_low is not None and i < len(pos_low):
plt.errorbar(
pos_lefts[i] + pos_widths[i],
pos_inds[i],
xerr=np.array([[pos_widths[i] - pos_low[i]], [pos_high[i] - pos_widths[i]]]),
ecolor=style.secondary_color_positive,
)
txt_obj = plt.text(
pos_lefts[i] + 0.5 * dist,
pos_inds[i],
format_value(pos_widths[i], "%+0.02f"),
horizontalalignment="center",
verticalalignment="center",
color=style.text_color,
fontsize=12,
)
text_bbox = txt_obj.get_window_extent(renderer=renderer)
arrow_bbox = arrow_obj.get_window_extent(renderer=renderer)
# if the text overflows the arrow then draw it after the arrow
if text_bbox.width > arrow_bbox.width:
txt_obj.remove()
txt_obj = plt.text(
pos_lefts[i] + (5 / 72) * bbox_to_xscale + dist,
pos_inds[i],
format_value(pos_widths[i], "%+0.02f"),
horizontalalignment="left",
verticalalignment="center",
color=style.primary_color_positive,
fontsize=12,
)
# draw the negative arrows
for i in range(len(neg_inds)):
dist = neg_widths[i]
arrow_obj = plt.arrow(
neg_lefts[i],
neg_inds[i],
-(-dist - hl_scaled),
0,
head_length=min(-dist, hl_scaled),
color=style.primary_color_negative,
width=bar_width,
head_width=bar_width,
)
if neg_low is not None and i < len(neg_low):
plt.errorbar(
neg_lefts[i] + neg_widths[i],
neg_inds[i],
xerr=np.array([[neg_widths[i] - neg_low[i]], [neg_high[i] - neg_widths[i]]]),
ecolor=style.secondary_color_negative,
)
txt_obj = plt.text(
neg_lefts[i] + 0.5 * dist,
neg_inds[i],
format_value(neg_widths[i], "%+0.02f"),
horizontalalignment="center",
verticalalignment="center",
color=style.text_color,
fontsize=12,
)
text_bbox = txt_obj.get_window_extent(renderer=renderer)
arrow_bbox = arrow_obj.get_window_extent(renderer=renderer)
# if the text overflows the arrow then draw it after the arrow
if text_bbox.width > arrow_bbox.width:
txt_obj.remove()
txt_obj = plt.text(
neg_lefts[i] - (5 / 72) * bbox_to_xscale + dist,
neg_inds[i],
format_value(neg_widths[i], "%+0.02f"),
horizontalalignment="right",
verticalalignment="center",
color=style.primary_color_negative,
fontsize=12,
)
# draw the y-ticks twice, once in gray and then again with just the feature names in black
# The 1e-8 is so matplotlib 3.3 doesn't try and collapse the ticks
ytick_pos = list(range(num_features)) + list(np.arange(num_features) + 1e-8)
plt.yticks(ytick_pos, yticklabels[:-1] + [label.split("=")[-1] for label in yticklabels[:-1]], fontsize=13)
# put horizontal lines for each feature row
for i in range(num_features):
plt.axhline(i, color=style.hlines_color, lw=0.5, dashes=(1, 5), zorder=-1)
# mark the prior expected value and the model prediction
plt.axvline(base_values, 0, 1 / num_features, color=style.vlines_color, linestyle="--", linewidth=0.5, zorder=-1)
fx = base_values + values.sum()
plt.axvline(fx, 0, 1, color=style.vlines_color, linestyle="--", linewidth=0.5, zorder=-1)
# clean up the main axis
plt.gca().xaxis.set_ticks_position("bottom")
plt.gca().yaxis.set_ticks_position("none")
plt.gca().spines["right"].set_visible(False)
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["left"].set_visible(False)
ax.tick_params(labelsize=13)
# plt.xlabel("\nModel output", fontsize=12)
# draw the E[f(X)] tick mark
xmin, xmax = ax.get_xlim()
ax2 = ax.twiny()
ax2.set_xlim(xmin, xmax)
ax2.set_xticks(
[base_values, base_values + min(1e-8, xmax * 1e-10)]
) # The 1e-8 is so matplotlib 3.3 doesn't try and collapse the ticks
# However, for very small values, 1e-8 is disruptively large, so xmax * 1e-10 is used instead
ax2.set_xticklabels(["\n$E[f(X)]$", "\n$ = " + format_value(base_values, "%0.03f") + "$"], fontsize=12, ha="left")
ax2.spines["right"].set_visible(False)
ax2.spines["top"].set_visible(False)
ax2.spines["left"].set_visible(False)
# draw the f(x) tick mark
ax3 = ax2.twiny()
ax3.set_xlim(xmin, xmax)
ax3.set_xticks(
[base_values + values.sum(), base_values + values.sum() + min(1e-8, xmax * 1e-10)]
) # The 1e-8 is so matplotlib 3.3 doesn't try and collapse the ticks
# However, for very small values, 1e-8 is disruptively large, so xmax * 1e-10 is used instead
ax3.set_xticklabels(["$f(x)$", "$ = " + format_value(fx, "%0.03f") + "$"], fontsize=12, ha="left")
tick_labels = ax3.xaxis.get_majorticklabels()
tick_labels[0].set_transform(
tick_labels[0].get_transform() + matplotlib.transforms.ScaledTranslation(-10 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_transform(
tick_labels[1].get_transform() + matplotlib.transforms.ScaledTranslation(12 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_color(style.tick_labels_color)
ax3.spines["right"].set_visible(False)
ax3.spines["top"].set_visible(False)
ax3.spines["left"].set_visible(False)
# adjust the position of the E[f(X)] = x.xx label
tick_labels = ax2.xaxis.get_majorticklabels()
tick_labels[0].set_transform(
tick_labels[0].get_transform() + matplotlib.transforms.ScaledTranslation(-20 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_transform(
tick_labels[1].get_transform()
+ matplotlib.transforms.ScaledTranslation(22 / 72.0, -1 / 72.0, fig.dpi_scale_trans)
)
tick_labels[1].set_color(style.tick_labels_color)
# color the y tick labels that have the feature values as gray
# (these fall behind the black ones with just the feature name)
tick_labels = ax.yaxis.get_majorticklabels()
for i in range(num_features):
tick_labels[i].set_color(style.tick_labels_color)
if show:
plt.show()
else:
return plt.gca()
def waterfall_legacy(expected_value, shap_values=None, features=None, feature_names=None, max_display=10, show=True):
"""Plots an explanation of a single prediction as a waterfall plot.
The SHAP value of a feature represents the impact of the evidence provided by that feature on the model's
output. The waterfall plot is designed to visually display how the SHAP values (evidence) of each feature
move the model output from our prior expectation under the background data distribution, to the final model
prediction given the evidence of all the features. Features are sorted by the magnitude of their SHAP values
with the smallest magnitude features grouped together at the bottom of the plot when the number of features
in the models exceeds the max_display parameter.
Parameters
----------
expected_value : float
This is the reference value that the feature contributions start from. For SHAP values it should
be the value of explainer.expected_value.
shap_values : numpy.array
One dimensional array of SHAP values.
features : numpy.array
One dimensional array of feature values. This provides the values of all the
features, and should be the same shape as the shap_values argument.
feature_names : list
List of feature names (# features).
max_display : int
The maximum number of features to display (default is 10).
show : bool
Whether matplotlib.pyplot.show() is called before returning. Setting this to False allows the plot
to be customized further after it has been created.
"""
style = get_style()
# Turn off interactive plot when not calling plt.show
if show is False:
plt.ioff()
# support passing an explanation object
upper_bounds = None
lower_bounds = None
if str(type(expected_value)).endswith("Explanation'>"):
shap_exp = expected_value
expected_value = shap_exp.expected_value
shap_values = shap_exp.values
features = shap_exp.data
feature_names = shap_exp.feature_names
lower_bounds = getattr(shap_exp, "lower_bounds", None)
upper_bounds = getattr(shap_exp, "upper_bounds", None)
# make sure we only have a single output to explain
if (isinstance(expected_value, np.ndarray) and len(expected_value) > 0) or isinstance(expected_value, list):
raise Exception(
"waterfall_plot requires a scalar expected_value of the model output as the first "
"parameter, but you have passed an array as the first parameter! "
"Try shap.waterfall_plot(explainer.expected_value[0], shap_values[0], X[0]) or "
"for multi-output models try "
"shap.waterfall_plot(explainer.expected_value[0], shap_values[0][0], X[0])."
)
# make sure we only have a single explanation to plot
if len(shap_values.shape) == 2:
raise Exception(
"The waterfall_plot can currently only plot a single explanation but a matrix of explanations was passed!"
)
# unwrap pandas series
if isinstance(features, pd.Series):
if feature_names is None:
feature_names = list(features.index)
features = features.values
# fallback feature names
if feature_names is None:
feature_names = np.array([labels["FEATURE"] % str(i) for i in range(len(shap_values))])
# init variables we use for tracking the plot locations
num_features = min(max_display, len(shap_values))
row_height = 0.5
rng = range(num_features - 1, -1, -1)
order = np.argsort(-np.abs(shap_values))
pos_lefts = []
pos_inds = []
pos_widths = []
pos_low = []
pos_high = []
neg_lefts = []
neg_inds = []
neg_widths = []
neg_low = []
neg_high = []
loc = expected_value + shap_values.sum()
yticklabels = ["" for i in range(num_features + 1)]
# size the plot based on how many features we are plotting
plt.gcf().set_size_inches(8, num_features * row_height + 1.5)
# see how many individual (vs. grouped at the end) features we are plotting
if num_features == len(shap_values):
num_individual = num_features
else:
num_individual = num_features - 1
# compute the locations of the individual features and plot the dashed connecting lines
for i in range(num_individual):
sval = shap_values[order[i]]
loc -= sval
if sval >= 0:
pos_inds.append(rng[i])
pos_widths.append(sval)
if lower_bounds is not None:
pos_low.append(lower_bounds[order[i]])
pos_high.append(upper_bounds[order[i]])
pos_lefts.append(loc)
else:
neg_inds.append(rng[i])
neg_widths.append(sval)
if lower_bounds is not None:
neg_low.append(lower_bounds[order[i]])
neg_high.append(upper_bounds[order[i]])
neg_lefts.append(loc)
if num_individual != num_features or i + 4 < num_individual:
plt.plot(
[loc, loc], [rng[i] - 1 - 0.4, rng[i] + 0.4], color="#bbbbbb", linestyle="--", linewidth=0.5, zorder=-1
)
if features is None:
yticklabels[rng[i]] = feature_names[order[i]]
else:
yticklabels[rng[i]] = format_value(features[order[i]], "%0.03f") + " = " + feature_names[order[i]]
# add a last grouped feature to represent the impact of all the features we didn't show
if num_features < len(shap_values):
yticklabels[0] = f"{len(shap_values) - num_features + 1} other features"
remaining_impact = expected_value - loc
if remaining_impact < 0:
pos_inds.append(0)
pos_widths.append(-remaining_impact)
pos_lefts.append(loc + remaining_impact)
else:
neg_inds.append(0)
neg_widths.append(-remaining_impact)
neg_lefts.append(loc + remaining_impact)
points = (
pos_lefts
+ list(np.array(pos_lefts) + np.array(pos_widths))
+ neg_lefts
+ list(np.array(neg_lefts) + np.array(neg_widths))
)
dataw = np.max(points) - np.min(points)
# draw invisible bars just for sizing the axes
label_padding = np.array([0.1 * dataw if w < 1 else 0 for w in pos_widths])
plt.barh(
pos_inds,
np.array(pos_widths) + label_padding + 0.02 * dataw,
left=np.array(pos_lefts) - 0.01 * dataw,
color=style.primary_color_positive,
alpha=0,
)
label_padding = np.array([-0.1 * dataw if -w < 1 else 0 for w in neg_widths])
plt.barh(
neg_inds,
np.array(neg_widths) + label_padding - 0.02 * dataw,
left=np.array(neg_lefts) + 0.01 * dataw,
color=style.primary_color_negative,
alpha=0,
)
# define variable we need for plotting the arrows
head_length = 0.08
bar_width = 0.8
xlen = plt.xlim()[1] - plt.xlim()[0]
fig = plt.gcf()
ax = plt.gca()
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width = bbox.width
bbox_to_xscale = xlen / width
hl_scaled = bbox_to_xscale * head_length
renderer = fig.canvas.get_renderer()
# draw the positive arrows
for i in range(len(pos_inds)):
dist = pos_widths[i]
arrow_obj = plt.arrow(
pos_lefts[i],
pos_inds[i],
max(dist - hl_scaled, 0.000001),
0,
head_length=min(dist, hl_scaled),
color=style.primary_color_positive,
width=bar_width,
head_width=bar_width,
)
if pos_low is not None and i < len(pos_low):
plt.errorbar(
pos_lefts[i] + pos_widths[i],
pos_inds[i],
xerr=np.array([[pos_widths[i] - pos_low[i]], [pos_high[i] - pos_widths[i]]]),
ecolor=style.secondary_color_positive,
)
txt_obj = plt.text(
pos_lefts[i] + 0.5 * dist,
pos_inds[i],
format_value(pos_widths[i], "%+0.02f"),
horizontalalignment="center",
verticalalignment="center",
color=style.text_color,
fontsize=12,
)
text_bbox = txt_obj.get_window_extent(renderer=renderer)
arrow_bbox = arrow_obj.get_window_extent(renderer=renderer)
# if the text overflows the arrow then draw it after the arrow
if text_bbox.width > arrow_bbox.width:
txt_obj.remove()
txt_obj = plt.text(
pos_lefts[i] + (5 / 72) * bbox_to_xscale + dist,
pos_inds[i],
format_value(pos_widths[i], "%+0.02f"),
horizontalalignment="left",
verticalalignment="center",
color=style.primary_color_positive,
fontsize=12,
)
# draw the negative arrows
for i in range(len(neg_inds)):
dist = neg_widths[i]
arrow_obj = plt.arrow(
neg_lefts[i],
neg_inds[i],
-max(-dist - hl_scaled, 0.000001),
0,
head_length=min(-dist, hl_scaled),
color=style.primary_color_negative,
width=bar_width,
head_width=bar_width,
)
if neg_low is not None and i < len(neg_low):
plt.errorbar(
neg_lefts[i] + neg_widths[i],
neg_inds[i],
xerr=np.array([[neg_widths[i] - neg_low[i]], [neg_high[i] - neg_widths[i]]]),
ecolor=style.secondary_color_negative,
)
txt_obj = plt.text(
neg_lefts[i] + 0.5 * dist,
neg_inds[i],
format_value(neg_widths[i], "%+0.02f"),
horizontalalignment="center",
verticalalignment="center",
color=style.text_color,
fontsize=12,
)
text_bbox = txt_obj.get_window_extent(renderer=renderer)
arrow_bbox = arrow_obj.get_window_extent(renderer=renderer)
# if the text overflows the arrow then draw it after the arrow
if text_bbox.width > arrow_bbox.width:
txt_obj.remove()
txt_obj = plt.text(
neg_lefts[i] - (5 / 72) * bbox_to_xscale + dist,
neg_inds[i],
format_value(neg_widths[i], "%+0.02f"),
horizontalalignment="right",
verticalalignment="center",
color=style.primary_color_negative,
fontsize=12,
)
# draw the y-ticks twice, once in gray and then again with just the feature names in black
plt.yticks(
list(range(num_features)) * 2,
yticklabels[:-1] + [label.split("=")[-1] for label in yticklabels[:-1]],
fontsize=13,
)
# put horizontal lines for each feature row
for i in range(num_features):
plt.axhline(i, color=style.hlines_color, lw=0.5, dashes=(1, 5), zorder=-1)
# mark the prior expected value and the model prediction
plt.axvline(expected_value, 0, 1 / num_features, color=style.vlines_color, linestyle="--", linewidth=0.5, zorder=-1)
fx = expected_value + shap_values.sum()
plt.axvline(fx, 0, 1, color=style.vlines_color, linestyle="--", linewidth=0.5, zorder=-1)
# clean up the main axis
plt.gca().xaxis.set_ticks_position("bottom")
plt.gca().yaxis.set_ticks_position("none")
plt.gca().spines["right"].set_visible(False)
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["left"].set_visible(False)
ax.tick_params(labelsize=13)
# plt.xlabel("\nModel output", fontsize=12)
# draw the E[f(X)] tick mark
xmin, xmax = ax.get_xlim()
ax2 = ax.twiny()
ax2.set_xlim(xmin, xmax)
ax2.set_xticks(
[expected_value, expected_value + 1e-8]
) # The 1e-8 is so matplotlib 3.3 doesn't try and collapse the ticks
ax2.set_xticklabels(
["\n$E[f(X)]$", "\n$ = " + format_value(expected_value, "%0.03f") + "$"], fontsize=12, ha="left"
)
ax2.spines["right"].set_visible(False)
ax2.spines["top"].set_visible(False)
ax2.spines["left"].set_visible(False)
# draw the f(x) tick mark
ax3 = ax2.twiny()
ax3.set_xlim(xmin, xmax)
# The 1e-8 is so matplotlib 3.3 doesn't try and collapse the ticks
ax3.set_xticks(
[
expected_value + shap_values.sum(),
expected_value + shap_values.sum() + 1e-8,
]
)
ax3.set_xticklabels(["$f(x)$", "$ = " + format_value(fx, "%0.03f") + "$"], fontsize=12, ha="left")
tick_labels = ax3.xaxis.get_majorticklabels()
tick_labels[0].set_transform(
tick_labels[0].get_transform() + matplotlib.transforms.ScaledTranslation(-10 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_transform(
tick_labels[1].get_transform() + matplotlib.transforms.ScaledTranslation(12 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_color(style.tick_labels_color)
ax3.spines["right"].set_visible(False)
ax3.spines["top"].set_visible(False)
ax3.spines["left"].set_visible(False)
# adjust the position of the E[f(X)] = x.xx label
tick_labels = ax2.xaxis.get_majorticklabels()
tick_labels[0].set_transform(
tick_labels[0].get_transform() + matplotlib.transforms.ScaledTranslation(-20 / 72.0, 0, fig.dpi_scale_trans)
)
tick_labels[1].set_transform(
tick_labels[1].get_transform()
+ matplotlib.transforms.ScaledTranslation(22 / 72.0, -1 / 72.0, fig.dpi_scale_trans)
)
tick_labels[1].set_color(style.tick_labels_color)
# color the y tick labels that have the feature values as gray
# (these fall behind the black ones with just the feature name)
tick_labels = ax.yaxis.get_majorticklabels()
for i in range(num_features):
tick_labels[i].set_color(style.tick_labels_color)
if show:
plt.show()
else:
return plt.gcf()