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
10 changes: 7 additions & 3 deletions shapash/backend/shap_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ def __init__(self, model, preprocessing=None, masker=None, explainer_args=None,
self.explainer_args = explainer_args if explainer_args else {}
self.explainer_compute_args = explainer_compute_args if explainer_compute_args else {}

shap_parameters = {"model": model, "masker": self.masker, **self.explainer_args}

if self.explainer_args:
if "explainer" in self.explainer_args.keys():
shap_parameters = {k: v for k, v in self.explainer_args.items() if k != "explainer"}
self.explainer = self.explainer_args["explainer"](**shap_parameters)
# For explicit explainer classes, keep user-provided kwargs only.
# Some SHAP explainers (e.g. TreeExplainer) do not accept `masker`.
explainer_args = {k: v for k, v in self.explainer_args.items() if k != "explainer"}
self.explainer = self.explainer_args["explainer"](**explainer_args)
else:
self.explainer = shap.Explainer(**self.explainer_args)
self.explainer = shap.Explainer(**shap_parameters)
else:
if shap.explainers.Linear.supports_model_with_masker(model, self.masker):
self.explainer = shap.Explainer(model=model, masker=self.masker)
Expand Down
6 changes: 6 additions & 0 deletions shapash/explainer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Explainer package public exports."""

from .explainer import Explainer
from .smart_explainer import SmartExplainer

__all__ = ["Explainer", "SmartExplainer"]
824 changes: 824 additions & 0 deletions shapash/explainer/explainer.py

Large diffs are not rendered by default.

1,103 changes: 155 additions & 948 deletions shapash/explainer/smart_explainer.py

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions shapash/explainer/smart_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class SmartPlotter:
just use the following syntax
Attributes :
explainer: object
SmartExplainer instance to point to.
Explainer compute object (delegating to SmartExplainer state).
Example
--------
>>> xpl.plot.my_plot_method(param=value)
Expand Down Expand Up @@ -174,9 +174,15 @@ def _check_masked_contributions(self, line, var_dict, x_val, contrib, label=None
"""
if hasattr(self._explainer, "masked_contributions"):
if isinstance(self._explainer.masked_contributions, list):
ext_contrib = self._explainer.masked_contributions[label].loc[line[0], :].values
masked_contrib = self._explainer.masked_contributions[label]
else:
ext_contrib = self._explainer.masked_contributions.loc[line[0], :].values
masked_contrib = self._explainer.masked_contributions

# No hidden contributions are available until a filter computation fills this structure.
if masked_contrib.empty or line[0] not in masked_contrib.index:
return var_dict, x_val, contrib

ext_contrib = masked_contrib.loc[line[0], :].values

ext_var_dict = ["Hidden Negative Contributions", "Hidden Positive Contributions"]
ext_x = ["", ""]
Expand Down
2 changes: 1 addition & 1 deletion shapash/report/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def execute_report(
explainer: "SmartExplainer",
project_info_file: str,
x_train: pd.DataFrame | None = None,
y_train: pd.DataFrame | None = None,
y_train: pd.Series | pd.DataFrame | None = None,
y_test: pd.Series | pd.DataFrame | None = None,
config: dict | None = None,
notebook_path: str | None = None,
Expand Down
63 changes: 32 additions & 31 deletions shapash/report/project_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,32 @@ def __init__(
explainer: SmartExplainer,
project_info_file: str,
x_train: pd.DataFrame | None = None,
y_train: pd.DataFrame | None = None,
y_test: pd.DataFrame | None = None,
y_train: pd.Series | pd.DataFrame | None = None,
y_test: pd.Series | pd.DataFrame | None = None,
config: dict | None = None,
):
self.explainer = explainer
self.compute = explainer.explainer
self.metadata = load_yml(path=project_info_file)
self.x_train_init = x_train
if x_train is not None:
x_train_pre = inverse_transform(x_train, self.explainer.preprocessing)
x_train_pre = inverse_transform(x_train, self.compute.preprocessing)
self.x_train_pre = handle_categorical_missing(x_train_pre)

if self.explainer.postprocessing:
self.x_train_pre = apply_postprocessing(self.x_train_pre, self.explainer.postprocessing)
if self.compute.postprocessing:
self.x_train_pre = apply_postprocessing(self.x_train_pre, self.compute.postprocessing)
else:
self.x_train_pre = None
self.x_init = self.explainer.x_init
self.x_init = self.compute.x_init
self.config = config if config is not None else dict()
self.col_names = list(self.explainer.columns_dict.values())
self.col_names = list(self.compute.columns_dict.values())
# x_init is always set on a compiled explainer, so `test` is never None here and
# `_create_train_test_df` cannot return None.
self.df_train_test = cast(pd.DataFrame, self._create_train_test_df(test=self.x_init, train=self.x_train_pre))
if self.explainer.y_pred is not None:
self.y_pred = np.array(self.explainer.y_pred.T)[0]
if self.compute.y_pred is not None:
self.y_pred = np.array(self.compute.y_pred.T)[0]
else:
self.y_pred = self.explainer.model.predict(self.explainer.x_encoded)
self.y_pred = self.compute.model.predict(self.compute.x_encoded)
self.y_test, target_name_test = self._get_values_and_name(y_test, "target")
self.y_train, target_name_train = self._get_values_and_name(y_train, "target")
self.target_name = target_name_train or target_name_test
Expand Down Expand Up @@ -233,16 +234,16 @@ def display_model_analysis(self):
Displays information about the model used : class name, library name, library version,
model parameters, ...
"""
print_md(f"**Model used :** {self.explainer.model.__class__.__name__}")
print_md(f"**Model used :** {self.compute.model.__class__.__name__}")

print_md(f"**Library :** {self.explainer.model.__class__.__module__}")
print_md(f"**Library :** {self.compute.model.__class__.__module__}")

for _, module in sorted(sys.modules.items()):
if not hasattr(module, "__name__"):
continue

module_name = module.__name__.split(".")[0]
expected_name = self.explainer.model.__class__.__module__.split(".")[0]
expected_name = self.compute.model.__class__.__module__.split(".")[0]

if expected_name == module_name:
try:
Expand All @@ -254,7 +255,7 @@ def display_model_analysis(self):
break

print_md("**Model parameters :** ")
model_params = self.explainer.model.__dict__
model_params = self.compute.model.__dict__
table_template = template_env.get_template("double_table.html")
print_html(
table_template.render(
Expand Down Expand Up @@ -359,7 +360,7 @@ def _perform_and_display_analysis_univariate(
):
col_types = compute_col_types(df)
n_splits = df[col_splitter].nunique()
inv_columns_dict = {v: k for k, v in self.explainer.columns_dict.items()}
inv_columns_dict = {v: k for k, v in self.compute.columns_dict.items()}
test_stats_univariate = perform_univariate_dataframe_analysis(
df.loc[df[col_splitter] == split_values[0]], col_types=col_types
)
Expand All @@ -371,10 +372,10 @@ def _perform_and_display_analysis_univariate(
univariate_template = template_env.get_template("univariate.html")
univariate_features_desc = list()
list_cols_labels = [
self.explainer.features_dict.get(col, col) for col in df.drop(col_splitter, axis=1).columns.to_list()
self.compute.features_dict.get(col, col) for col in df.drop(col_splitter, axis=1).columns.to_list()
]
for col_label in sorted(list_cols_labels):
col = self.explainer.inv_features_dict.get(col_label, col_label)
col = self.compute.inv_features_dict.get(col_label, col_label)
fig = plot_distribution(
df_all=df,
col=col,
Expand Down Expand Up @@ -416,21 +417,21 @@ def display_model_explainability(self):
"""
print_md("*Note : the explainability graphs were generated using the test set only.*")
explainability_template = template_env.get_template("explainability.html")
inv_columns_dict = {v: k for k, v in self.explainer.columns_dict.items()}
inv_columns_dict = {v: k for k, v in self.compute.columns_dict.items()}
explain_data = list()
multiclass = True if (self.explainer._classes and len(self.explainer._classes) > 2) else False
c_list = self.explainer._classes if multiclass else [1] # list just used for multiclass
multiclass = True if (self.compute._classes and len(self.compute._classes) > 2) else False
c_list = self.compute._classes if multiclass else [1] # list just used for multiclass
for index_label, label in enumerate(c_list): # Iterating over all labels in multiclass case
label_value = self.explainer.check_label_name(label)[2] if multiclass else ""
label_value = self.compute.check_label_name(label)[2] if multiclass else ""

# Feature Importance
fig_features_importance = self.explainer.plot.features_importance(label=label)

# Contribution Plot
explain_contrib_data = list()
list_cols_labels = [self.explainer.features_dict.get(col, col) for col in self.col_names]
list_cols_labels = [self.compute.features_dict.get(col, col) for col in self.col_names]
for feature_label in sorted(list_cols_labels):
feature = self.explainer.inv_features_dict.get(feature_label, feature_label)
feature = self.compute.inv_features_dict.get(feature_label, feature_label)
fig = self.explainer.plot.contribution_plot(feature, label=label, max_points=self.max_points)
# Apparently matkers are not supported during conversion into html
for el in fig.data:
Expand All @@ -440,7 +441,7 @@ def display_model_explainability(self):
{
"feature_index": int(inv_columns_dict[feature]),
"name": feature,
"description": self.explainer.features_dict[feature],
"description": self.compute.features_dict[feature],
"plot": plotly.io.to_html(fig, include_plotlyjs=False, full_html=False),
}
)
Expand All @@ -451,26 +452,26 @@ def display_model_explainability(self):
list_ind, _ = self.explainer.plot._select_indices_interactions_plot(
selection=None, max_points=self.max_points
)
interaction_values = self.explainer.get_interaction_values(selection=list_ind)
interaction_values = self.compute.get_interaction_values(selection=list_ind)
sorted_top_features_indices = compute_sorted_variables_interactions_list_indices(interaction_values)
indices_to_plot = sorted_top_features_indices[: self.nb_top_interactions]

for i, ids in enumerate(indices_to_plot):
id0, id1 = ids

fig_one_interaction = self.explainer.plot.interactions_plot(
col1=self.explainer.columns_dict[id0],
col2=self.explainer.columns_dict[id1],
col1=self.compute.columns_dict[id0],
col2=self.compute.columns_dict[id1],
max_points=self.max_points,
)

explain_contrib_data_interaction.append(
{
"feature_index": i,
"name": self.explainer.columns_dict[id0] + " / " + self.explainer.columns_dict[id1],
"description": self.explainer.features_dict[self.explainer.columns_dict[id0]]
"name": self.compute.columns_dict[id0] + " / " + self.compute.columns_dict[id1],
"description": self.compute.features_dict[self.compute.columns_dict[id0]]
+ " / "
+ self.explainer.features_dict[self.explainer.columns_dict[id1]],
+ self.compute.features_dict[self.compute.columns_dict[id1]],
"plot": plotly.io.to_html(fig_one_interaction, include_plotlyjs=False, full_html=False),
}
)
Expand Down Expand Up @@ -560,7 +561,7 @@ def display_model_performance(self):
metric_fn = get_callable(path=metric["path"])
# Look if we should use proba values instead of predicted values
if "use_proba_values" in metric.keys() and metric["use_proba_values"] is True:
y_pred = self.explainer.proba_values
y_pred = self.compute.proba_values
else:
y_pred = self.y_pred
res = metric_fn(self.y_test, y_pred)
Expand Down
15 changes: 8 additions & 7 deletions shapash/webapp/smart_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ class SmartApp:
Attributes
----------
explainer: object
SmartExplainer instance to point to.
Explainer instance to point to.
"""

def __init__(self, explainer, settings: dict | None = None):
def __init__(self, explainer, settings: dict | None = None, title_story: str | None = None):
"""
Init on class instantiation, everything to be able to run the app on server.
Parameters
----------
explainer : SmartExplainer
SmartExplainer object
explainer : Explainer
Explainer object
settings : dict
A dict describing the default webapp settings values to be used
Possible settings (dict keys) are 'rows', 'points', 'violin', 'features', 'toggle_group'
Expand All @@ -89,8 +89,9 @@ def __init__(self, explainer, settings: dict | None = None):
external_stylesheets=[dbc.themes.BOOTSTRAP],
)
self.app.title = "Shapash Monitor"
if explainer.title_story:
self.app.title += " - " + explainer.title_story
self.title_story = title_story if title_story is not None else ""
if self.title_story:
self.app.title += " - " + self.title_story
self.explainer = explainer

# SETTINGS
Expand Down Expand Up @@ -831,7 +832,7 @@ def make_skeleton(self):
dbc.Row(
[
html.H3(
truncate_str(self.explainer.title_story, maxlen=40),
truncate_str(self.title_story, maxlen=40),
id="shapash_title_story",
style={"text-align": "center"},
)
Expand Down
18 changes: 13 additions & 5 deletions shapash/webapp/utils/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from shapash.explainer.smart_explainer import SmartExplainer
from shapash.explainer.explainer import Explainer

import dash_bootstrap_components as dbc
import numpy as np
Expand Down Expand Up @@ -827,13 +827,13 @@ def handle_group_display_logic(


def determine_total_pages_and_display(
explainer: "SmartExplainer", features: int, bool_group: bool, group_name: str, page: int
explainer: "Explainer", features: int, bool_group: bool, group_name: str, page: int
) -> tuple[int, dict[str, str], int]:
"""
Determine the total number of pages and the display properties.

Args:
explainer (SmartExplainer): The explainer object.
explainer (Explainer): The explainer object.
features (int): Number of features to display per page.
bool_group (bool): Whether to display groups.
group_name (str): Name of the feature group.
Expand All @@ -844,9 +844,17 @@ def determine_total_pages_and_display(
"""
display_groups = explainer.features_groups is not None and bool_group
if explainer._case == "classification":
nb_features = len(explainer.features_imp_groups[0]) if display_groups else len(explainer.features_imp[0])
features_imp = explainer.features_imp_groups if display_groups else explainer.features_imp
if not isinstance(features_imp, list) or len(features_imp) == 0:
raise ValueError("Feature importances are missing for classification case.")
nb_features = len(features_imp[0])
elif explainer._case == "regression":
nb_features = len(explainer.features_imp_groups) if display_groups else len(explainer.features_imp)
features_imp = explainer.features_imp_groups if display_groups else explainer.features_imp
if features_imp is None:
raise ValueError("Feature importances are missing for regression case.")
nb_features = len(features_imp)
else:
raise ValueError("Unknown explainer case.")

total_pages = (nb_features - 1) // features + 1
if (total_pages == 1) or (group_name):
Expand Down
Loading