Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion python/interpret-core/interpret/core/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def data(self, key=None):
return None
return self._internal_obj["specific"][key]

def visualize(self, key=None, title=None):
def visualize(self, key=None, title=None, plot_type="bar", **kwargs):
"""Provides interactive visualizations.

Args:
Expand All @@ -82,6 +82,7 @@ def visualize(self, key=None, title=None):
plot_horizontal_bar,
plot_line,
plot_pairwise_heatmap,
plot_waterfall,
sort_take,
)

Expand All @@ -98,6 +99,10 @@ def visualize(self, key=None, title=None):

# Handle local instance graphs
if self.explanation_type == "local":
if plot_type == "waterfall":
title_str = "" if title is None else title
return plot_waterfall(data_dict, title=title_str, **kwargs)

if isinstance(key, tuple) and len(key) == 2:
provider, key = key
# TODO: MLI should handle multiclass at a future date.
Expand Down
53 changes: 43 additions & 10 deletions python/interpret-core/interpret/glassbox/_ebm.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,13 @@ class EBMExplanation(FeatureValueExplanation):

explanation_type = None

def visualize(self, key: int | None = None, title: str | None = None) -> Any:
def visualize(
self,
key: int | None = None,
title: str | None = None,
plot_type: str = "bar",
**kwargs: Any,
) -> Any:
"""Provide interactive visualizations.

Args:
Expand Down Expand Up @@ -387,11 +393,23 @@ def visualize(self, key: int | None = None, title: str | None = None) -> Any:

# Local explanation graph
if self.explanation_type == "local":
figure = super().visualize(key)
figure.update_layout(
title="Local Explanation (" + figure.layout.title.text + ")",
xaxis_title="Contribution to Prediction",
)
figure = super().visualize(key, title=title, plot_type=plot_type, **kwargs)
if figure is None: # pragma: no cover
return None
orig_title = figure.layout.title.text if figure.layout.title else ""
if plot_type == "waterfall":
figure.update_layout(
title="Local Explanation (" + orig_title + ")"
if orig_title
else "Local Explanation",
)
else:
figure.update_layout(
title="Local Explanation (" + orig_title + ")"
if orig_title
else "Local Explanation",
xaxis_title="Contribution to Prediction",
)
figure._interpret_help_text = (
"A local explanation shows the breakdown of how much "
"each term contributed to the prediction for a single sample. The intercept "
Expand Down Expand Up @@ -2604,19 +2622,34 @@ def explain_local(
perf = None if perf_dicts is None else perf_dicts[row_idx]
perf_list.append(perf)

for data, sample_scores, perf in zip(X_unified, explanations, perf_list):
for row_idx, (data, sample_scores, perf) in enumerate(
zip(X_unified, explanations, perf_list, strict=False)
):
values = [
data[tfs[0]] if len(tfs) == 1 else "" for tfs in self.term_features_
]
extra_names = ["Intercept"]
extra_scores = [intercept]
extra_values = [1]
if init_score is not None:
sample_init = init_score[row_idx]
if (classes is None or len(classes) <= 2) and isinstance(
sample_init, (list, np.ndarray)
):
sample_init = sample_init[0]
extra_names.append("Init score")
extra_scores.append(sample_init)
extra_values.append(1)

data_dict = {
"type": "univariate",
"names": list(self.term_names_),
"scores": list(sample_scores),
"values": values,
"extra": {
"names": ["Intercept"],
"scores": [intercept],
"values": [1],
"names": extra_names,
"scores": extra_scores,
"values": extra_values,
},
"perf": perf,
}
Expand Down
4 changes: 2 additions & 2 deletions python/interpret-core/interpret/glassbox/_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ class LinearExplanation(FeatureValueExplanation):

explanation_type = None

def visualize(self, key=None):
def visualize(self, key=None, title=None, plot_type="bar", **kwargs):
"""Provides interactive visualizations.

Args:
Expand Down Expand Up @@ -428,7 +428,7 @@ def visualize(self, key=None):
data_dict, title="Overall Importance:<br>Coefficients"
)

return super().visualize(key)
return super().visualize(key, title=title, plot_type=plot_type, **kwargs)


class LinearRegression(_SKRegressorMixin, BaseLinear):
Expand Down
13 changes: 10 additions & 3 deletions python/interpret-core/interpret/provider/_visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@ def render(self, explanation, key=-1, **kwargs):
class PreserveProvider(VisualizeProvider):
def render(self, explanation, key=-1, **kwargs):
file_name = kwargs.pop("file_name", None)
plot_type = kwargs.pop("plot_type", None)

# NOTE: Preserve didn't support returning everything. If key is -1 default to key is None.
# This is for backward-compatibility. All of this will be deprecated shortly anyway.
if key == -1:
key = None

# Get visual object
visual = explanation.visualize(key=key)
if plot_type is not None and plot_type != "bar":
visual = explanation.visualize(key=key, plot_type=plot_type)
else:
visual = explanation.visualize(key=key)

# Output to front-end/file

Expand Down Expand Up @@ -201,12 +205,14 @@ def link(self, explanation, **kwargs):

return self.app_runner.display_link(explanation)

def render(self, explanation, **kwargs):
def render(self, explanation, key=-1, **kwargs):
self.idempotent_start()

# Register
share_tables = kwargs.pop("share_tables", None)
self.app_runner.register(explanation, share_tables=share_tables)
self.app_runner.register(
explanation, share_tables=share_tables, key=key, **kwargs
)

# Display
open_link = isinstance(explanation, list)
Expand Down Expand Up @@ -234,4 +240,5 @@ def render(self, explanation, key=-1, **kwargs):
default_key=key,
detected_envs=self.detected_envs,
js_url=self.js_url,
**kwargs,
)
17 changes: 12 additions & 5 deletions python/interpret-core/interpret/visual/_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ def _build_viz_err_obj(err_msg):
}


def _build_viz_obj(explanation, detected_envs):
overall = _build_viz_figure(explanation.visualize(), detected_envs)
def _build_viz_obj(explanation, detected_envs, **kwargs):
overall = _build_viz_figure(explanation.visualize(**kwargs), detected_envs)
if explanation.selector is None:
# NOTE: Unsure if this should be a list or None in the long term.
specific = []
selector_obj = {"columns": [], "data": []}
else:
specific = [
_build_viz_figure(explanation.visualize(i), detected_envs)
_build_viz_figure(explanation.visualize(i, **kwargs), detected_envs)
for i in range(len(explanation.selector["data"]))
]
selector_obj = explanation.selector
Expand Down Expand Up @@ -218,7 +218,14 @@ def _render_databricks(js): # pragma: no cover
_databricks_displayHTML(js)


def render(explanation, id_str=None, default_key=-1, detected_envs=None, js_url=None):
def render(
explanation,
id_str=None,
default_key=-1,
detected_envs=None,
js_url=None,
**kwargs,
):
from IPython.display import HTML, display

global _jupyter_initialized
Expand All @@ -227,7 +234,7 @@ def render(explanation, id_str=None, default_key=-1, detected_envs=None, js_url=
msg = "Dashboard not yet supported in cloud environments."
viz_obj = _build_viz_err_obj(msg)
else:
viz_obj = _build_viz_obj(explanation, detected_envs)
viz_obj = _build_viz_obj(explanation, detected_envs, **kwargs)

init_js, body_js = _build_javascript(
viz_obj, id_str, default_key=default_key, js_url=js_url
Expand Down
60 changes: 50 additions & 10 deletions python/interpret-core/interpret/visual/_udash.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(self, *args, **kwargs):
# Dash app for a single explanation only
def generate_app_mini(
ctx_item,
options=None,
url_base_pathname=None,
requests_pathname_prefix=None,
routes_pathname_prefix=None,
Expand All @@ -99,12 +100,14 @@ def generate_app_mini(
requests_pathname_prefix=requests_pathname_prefix,
routes_pathname_prefix=routes_pathname_prefix,
)
app.options = options or {}
app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
app.config["suppress_callback_exceptions"] = True
app.ctx_item = ctx_item
app.logger.handlers = []
app.title = "InterpretML"
app.options = options or {}
server = app.server

# Items in drop-down.
Expand Down Expand Up @@ -145,7 +148,12 @@ def generate_app_mini(
id="component-drop",
options=data_options,
multi=False,
value=-1,
value=(
(options or {}).get("key", -1)
if (options or {}).get("key", -1) is not None
and (options or {}).get("key", -1) != -1
else -1
),
)
],
className="card-body",
Expand Down Expand Up @@ -179,10 +187,15 @@ def update_viz_container(value):
return None

explanation, selector = app.ctx_item
viz_kwargs = {
k: v
for k, v in (app.options or {}).items()
if k not in ("share_tables", "key")
}
if value == -1:
output_div = gen_overall_plot(explanation, int(value))
output_div = gen_overall_plot(explanation, int(value), **viz_kwargs)
else:
output_div = gen_plot(explanation, int(value), 0, 0)
output_div = gen_plot(explanation, int(value), 0, 0, **viz_kwargs)

return output_div

Expand All @@ -195,8 +208,14 @@ def handle_error(e): # pragma: no cover
return app


def gen_overall_plot(exp, model_idx):
figure = exp.visualize(key=None)
def gen_overall_plot(exp, model_idx, **kwargs):
if kwargs:
try:
figure = exp.visualize(key=None, **kwargs)
except TypeError:
figure = exp.visualize(key=None)
else:
figure = exp.visualize(key=None)
if figure is None:
_log.info(f"No overall plot to display: {model_idx}|{exp.name}")
# Provide default 'no overall' graph
Expand Down Expand Up @@ -272,8 +291,14 @@ def gen_overall_plot(exp, model_idx):
)


def gen_plot(exp, picker, model_idx, counter):
figure = exp.visualize(key=picker)
def gen_plot(exp, picker, model_idx, counter, **kwargs):
if kwargs:
try:
figure = exp.visualize(key=picker, **kwargs)
except TypeError:
figure = exp.visualize(key=picker)
else:
figure = exp.visualize(key=picker)
if isinstance(figure, _NDFrame):
records = figure.to_dict("records")
columns = [
Expand Down Expand Up @@ -330,7 +355,10 @@ def gen_plot(exp, picker, model_idx, counter):
# Dash app code
# TODO: Consider reducing complexity of this function.
def generate_app_full(
url_base_pathname=None, requests_pathname_prefix=None, routes_pathname_prefix=None
options=None,
url_base_pathname=None,
requests_pathname_prefix=None,
routes_pathname_prefix=None,
):
"""Generates the Dash application including callbacks.

Expand Down Expand Up @@ -786,8 +814,13 @@ def gen_plots_container(model_idx, picker_idx):

output = []
counter = 0
viz_kwargs = {
k: v
for k, v in (app.options or {}).items()
if k not in ("share_tables", "key")
}
for picker in reversed(picker_idx):
output_div = gen_plot(exp, picker, model_idx, counter)
output_div = gen_plot(exp, picker, model_idx, counter, **viz_kwargs)
counter += 1
output.append(output_div)
return html.Div(output)
Expand All @@ -798,7 +831,12 @@ def gen_overall_plot_container(model_idx):
ctx = app.ctx
exp = ctx[model_idx][0]

output_div = gen_overall_plot(exp, model_idx)
viz_kwargs = {
k: v
for k, v in (app.options or {}).items()
if k not in ("share_tables", "key")
}
output_div = gen_overall_plot(exp, model_idx, **viz_kwargs)

return html.Div(output_div)

Expand Down Expand Up @@ -888,12 +926,14 @@ def generate_app(
new_item = _expand_ctx_item(ctx)
return generate_app_mini(
new_item,
options=options,
url_base_pathname=url_base_pathname,
requests_pathname_prefix=requests_pathname_prefix,
routes_pathname_prefix=routes_pathname_prefix,
)

app = generate_app_full(
options=options,
requests_pathname_prefix=requests_pathname_prefix,
routes_pathname_prefix=routes_pathname_prefix,
)
Expand Down
6 changes: 3 additions & 3 deletions python/interpret-core/interpret/visual/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ def __init__(self, base_url=None, use_relative_links=False):
def obj_id(self, obj):
return str(id(obj))

def register(self, ctx, share_tables=None):
def register(self, ctx, share_tables=None, **kwargs):
ctx_id = self.obj_id(ctx)
if ctx_id not in self.pool:
if ctx_id not in self.pool or kwargs:
_log.info(f"Creating App Entry: {ctx_id}")
ctx_path = (
f"/{ctx_id}/"
Expand All @@ -220,7 +220,7 @@ def register(self, ctx, share_tables=None):
)
app = _udash.generate_app(
ctx,
{"share_tables": share_tables},
{"share_tables": share_tables, **kwargs},
# url_base_pathname=ctx_path,
requests_pathname_prefix=ctx_path,
routes_pathname_prefix=ctx_path,
Expand Down
Loading
Loading