import base64 import io import os import numpy as np import sklearn from matplotlib.colors import LinearSegmentedColormap from .. import __version__ from ..plots import colors from . import methods, metrics, models from .experiments import run_experiments try: import matplotlib import matplotlib.pyplot as pl from IPython.display import HTML except ImportError: pass metadata = { # "runtime": { # "title": "Runtime", # "sort_order": 1 # }, # "local_accuracy": { # "title": "Local Accuracy", # "sort_order": 2 # }, # "consistency_guarantees": { # "title": "Consistency Guarantees", # "sort_order": 3 # }, # "keep_positive_mask": { # "title": "Keep Positive (mask)", # "xlabel": "Max fraction of features kept", # "ylabel": "Mean model output", # "sort_order": 4 # }, # "keep_negative_mask": { # "title": "Keep Negative (mask)", # "xlabel": "Max fraction of features kept", # "ylabel": "Negative mean model output", # "sort_order": 5 # }, # "keep_absolute_mask__r2": { # "title": "Keep Absolute (mask)", # "xlabel": "Max fraction of features kept", # "ylabel": "R^2", # "sort_order": 6 # }, # "keep_absolute_mask__roc_auc": { # "title": "Keep Absolute (mask)", # "xlabel": "Max fraction of features kept", # "ylabel": "ROC AUC", # "sort_order": 6 # }, # "remove_positive_mask": { # "title": "Remove Positive (mask)", # "xlabel": "Max fraction of features removed", # "ylabel": "Negative mean model output", # "sort_order": 7 # }, # "remove_negative_mask": { # "title": "Remove Negative (mask)", # "xlabel": "Max fraction of features removed", # "ylabel": "Mean model output", # "sort_order": 8 # }, # "remove_absolute_mask__r2": { # "title": "Remove Absolute (mask)", # "xlabel": "Max fraction of features removed", # "ylabel": "1 - R^2", # "sort_order": 9 # }, # "remove_absolute_mask__roc_auc": { # "title": "Remove Absolute (mask)", # "xlabel": "Max fraction of features removed", # "ylabel": "1 - ROC AUC", # "sort_order": 9 # }, # "keep_positive_resample": { # "title": "Keep Positive (resample)", # "xlabel": "Max fraction of features kept", # "ylabel": "Mean model output", # "sort_order": 10 # }, # "keep_negative_resample": { # "title": "Keep Negative (resample)", # "xlabel": "Max fraction of features kept", # "ylabel": "Negative mean model output", # "sort_order": 11 # }, # "keep_absolute_resample__r2": { # "title": "Keep Absolute (resample)", # "xlabel": "Max fraction of features kept", # "ylabel": "R^2", # "sort_order": 12 # }, # "keep_absolute_resample__roc_auc": { # "title": "Keep Absolute (resample)", # "xlabel": "Max fraction of features kept", # "ylabel": "ROC AUC", # "sort_order": 12 # }, # "remove_positive_resample": { # "title": "Remove Positive (resample)", # "xlabel": "Max fraction of features removed", # "ylabel": "Negative mean model output", # "sort_order": 13 # }, # "remove_negative_resample": { # "title": "Remove Negative (resample)", # "xlabel": "Max fraction of features removed", # "ylabel": "Mean model output", # "sort_order": 14 # }, # "remove_absolute_resample__r2": { # "title": "Remove Absolute (resample)", # "xlabel": "Max fraction of features removed", # "ylabel": "1 - R^2", # "sort_order": 15 # }, # "remove_absolute_resample__roc_auc": { # "title": "Remove Absolute (resample)", # "xlabel": "Max fraction of features removed", # "ylabel": "1 - ROC AUC", # "sort_order": 15 # }, # "remove_positive_retrain": { # "title": "Remove Positive (retrain)", # "xlabel": "Max fraction of features removed", # "ylabel": "Negative mean model output", # "sort_order": 11 # }, # "remove_negative_retrain": { # "title": "Remove Negative (retrain)", # "xlabel": "Max fraction of features removed", # "ylabel": "Mean model output", # "sort_order": 12 # }, # "keep_positive_retrain": { # "title": "Keep Positive (retrain)", # "xlabel": "Max fraction of features kept", # "ylabel": "Mean model output", # "sort_order": 6 # }, # "keep_negative_retrain": { # "title": "Keep Negative (retrain)", # "xlabel": "Max fraction of features kept", # "ylabel": "Negative mean model output", # "sort_order": 7 # }, # "batch_remove_absolute__r2": { # "title": "Batch Remove Absolute", # "xlabel": "Fraction of features removed", # "ylabel": "1 - R^2", # "sort_order": 13 # }, # "batch_keep_absolute__r2": { # "title": "Batch Keep Absolute", # "xlabel": "Fraction of features kept", # "ylabel": "R^2", # "sort_order": 8 # }, # "batch_remove_absolute__roc_auc": { # "title": "Batch Remove Absolute", # "xlabel": "Fraction of features removed", # "ylabel": "1 - ROC AUC", # "sort_order": 13 # }, # "batch_keep_absolute__roc_auc": { # "title": "Batch Keep Absolute", # "xlabel": "Fraction of features kept", # "ylabel": "ROC AUC", # "sort_order": 8 # }, # "linear_shap_corr": { # "title": "Linear SHAP (corr)" # }, # "linear_shap_ind": { # "title": "Linear SHAP (ind)" # }, # "coef": { # "title": "Coefficients" # }, # "random": { # "title": "Random" # }, # "kernel_shap_1000_meanref": { # "title": "Kernel SHAP 1000 mean ref." # }, # "sampling_shap_1000": { # "title": "Sampling SHAP 1000" # }, # "tree_shap_tree_path_dependent": { # "title": "Tree SHAP" # }, # "saabas": { # "title": "Saabas" # }, # "tree_gain": { # "title": "Gain/Gini Importance" # }, # "mean_abs_tree_shap": { # "title": "mean(|Tree SHAP|)" # }, # "lasso_regression": { # "title": "Lasso Regression" # }, # "ridge_regression": { # "title": "Ridge Regression" # }, # "gbm_regression": { # "title": "Gradient Boosting Regression" # } } benchmark_color_map = { "tree_shap": "#1E88E5", "deep_shap": "#1E88E5", "linear_shap_corr": "#1E88E5", "linear_shap_ind": "#ff0d57", "coef": "#13B755", "random": "#999999", "const_random": "#666666", "kernel_shap_1000_meanref": "#7C52FF", } # negated_metrics = [ # "runtime", # "remove_positive_retrain", # "remove_positive_mask", # "remove_positive_resample", # "keep_negative_retrain", # "keep_negative_mask", # "keep_negative_resample" # ] # one_minus_metrics = [ # "remove_absolute_mask__r2", # "remove_absolute_mask__roc_auc", # "remove_absolute_resample__r2", # "remove_absolute_resample__roc_auc" # ] def get_method_color(method): for line in getattr(methods, method).__doc__.split("\n"): line = line.strip() if line.startswith("color = "): v = line.split("=")[1].strip() if v.startswith("red_blue_circle("): return colors.red_blue_circle(float(v[16:-1])) else: return v return "#000000" def get_method_linestyle(method): for line in getattr(methods, method).__doc__.split("\n"): line = line.strip() if line.startswith("linestyle = "): return line.split("=")[1].strip() return "solid" def get_metric_attr(metric, attr): for line in getattr(metrics, metric).__doc__.split("\n"): line = line.strip() # string prefix = attr + ' = "' suffix = '"' if line.startswith(prefix) and line.endswith(suffix): return line[len(prefix) : -len(suffix)] # number prefix = attr + " = " if line.startswith(prefix): return float(line[len(prefix) :]) return "" def plot_curve(dataset, model, metric, cmap=benchmark_color_map): experiments = run_experiments(dataset=dataset, model=model, metric=metric) pl.figure() method_arr = [] for name, (fcounts, scores) in experiments: _, _, method, _ = name transform = get_metric_attr(metric, "transform") if transform == "negate": scores = -scores elif transform == "one_minus": scores = 1 - scores auc = sklearn.metrics.auc(fcounts, scores) / fcounts[-1] method_arr.append((auc, method, scores)) for auc, method, scores in sorted(method_arr): method_title = getattr(methods, method).__doc__.split("\n")[0].strip() label = f"{auc:6.3f} - " + method_title pl.plot( fcounts / fcounts[-1], scores, label=label, color=get_method_color(method), linewidth=2, linestyle=get_method_linestyle(method), ) metric_title = getattr(metrics, metric).__doc__.split("\n")[0].strip() pl.xlabel(get_metric_attr(metric, "xlabel")) pl.ylabel(get_metric_attr(metric, "ylabel")) model_title = getattr(models, dataset + "__" + model).__doc__.split("\n")[0].strip() pl.title(metric_title + " - " + model_title) pl.gca().xaxis.set_ticks_position("bottom") pl.gca().yaxis.set_ticks_position("left") pl.gca().spines["right"].set_visible(False) pl.gca().spines["top"].set_visible(False) ahandles, alabels = pl.gca().get_legend_handles_labels() pl.legend(reversed(ahandles), reversed(alabels)) return pl.gcf() def plot_human(dataset, model, metric, cmap=benchmark_color_map): experiments = run_experiments(dataset=dataset, model=model, metric=metric) pl.figure() method_arr = [] for name, (fcounts, scores) in experiments: _, _, method, _ = name diff_sum = np.sum(np.abs(scores[1] - scores[0])) method_arr.append((diff_sum, method, scores[0], scores[1])) inds = np.arange(3) # the x locations for the groups inc_width = (1.0 / len(method_arr)) * 0.8 width = inc_width * 0.9 pl.bar(inds, method_arr[0][2], width, label="Human Consensus", color="black", edgecolor="white") i = 1 line_style_to_hatch = {"dashed": "///", "dotted": "..."} for diff_sum, method, _, methods_attrs in sorted(method_arr): method_title = getattr(methods, method).__doc__.split("\n")[0].strip() label = f"{diff_sum:.2f} - " + method_title pl.bar( inds + inc_width * i, methods_attrs.flatten(), width, label=label, edgecolor="white", color=get_method_color(method), hatch=line_style_to_hatch.get(get_method_linestyle(method), None), ) i += 1 metric_title = getattr(metrics, metric).__doc__.split("\n")[0].strip() pl.xlabel("Features in the model") pl.ylabel("Feature attribution value") model_title = getattr(models, dataset + "__" + model).__doc__.split("\n")[0].strip() pl.title(metric_title + " - " + model_title) pl.gca().xaxis.set_ticks_position("bottom") pl.gca().yaxis.set_ticks_position("left") pl.gca().spines["right"].set_visible(False) pl.gca().spines["top"].set_visible(False) ahandles, alabels = pl.gca().get_legend_handles_labels() # pl.legend(ahandles, alabels) pl.xticks(np.array([0, 1, 2, 3]) - (inc_width + width) / 2, ["", "", "", ""]) pl.gca().xaxis.set_minor_locator(matplotlib.ticker.FixedLocator([0.4, 1.4, 2.4])) pl.gca().xaxis.set_minor_formatter(matplotlib.ticker.FixedFormatter(["Fever", "Cough", "Headache"])) pl.gca().tick_params(which="minor", length=0) pl.axhline(0, color="#aaaaaa", linewidth=0.5) box = pl.gca().get_position() pl.gca().set_position([box.x0, box.y0 + box.height * 0.3, box.width, box.height * 0.7]) # Put a legend below current axis pl.gca().legend(ahandles, alabels, loc="upper center", bbox_to_anchor=(0.5, -0.15), ncol=2) return pl.gcf() def _human_score_map(human_consensus, methods_attrs): """Converts human agreement differences to numerical scores for coloring.""" v = 1 - min(np.sum(np.abs(methods_attrs - human_consensus)) / (np.abs(human_consensus).sum() + 1), 1.0) return v def make_grid(scores, dataset, model, normalize=True, transform=True): color_vals = {} metric_sort_order = {} for (_, _, method, metric), (fcounts, score) in filter(lambda x: x[0][0] == dataset and x[0][1] == model, scores): metric_sort_order[metric] = get_metric_attr(metric, "sort_order") if metric not in color_vals: color_vals[metric] = {} if transform: transform_type = get_metric_attr(metric, "transform") if transform_type == "negate": score = -score elif transform_type == "one_minus": score = 1 - score elif transform_type == "negate_log": score = -np.log10(score) if fcounts is None: color_vals[metric][method] = score elif fcounts == "human": color_vals[metric][method] = _human_score_map(*score) else: auc = sklearn.metrics.auc(fcounts, score) / fcounts[-1] color_vals[metric][method] = auc # print(metric_sort_order) # col_keys = sorted(list(color_vals.keys()), key=lambda v: metric_sort_order[v]) # print(col_keys) col_keys = list(color_vals.keys()) row_keys = list({v for k in col_keys for v in color_vals[k].keys()}) data = -28567 * np.ones((len(row_keys), len(col_keys))) for i in range(len(row_keys)): for j in range(len(col_keys)): data[i, j] = color_vals[col_keys[j]][row_keys[i]] assert np.sum(data == -28567) == 0, "There are missing data values!" if normalize: data = (data - data.min(0)) / (data.max(0) - data.min(0) + 1e-8) # sort by performans inds = np.argsort(-data.mean(1)) row_keys = [row_keys[i] for i in inds] data = data[inds, :] return row_keys, col_keys, data red_blue_solid = LinearSegmentedColormap( "red_blue_solid", { "red": ((0.0, 198.0 / 255, 198.0 / 255), (1.0, 5.0 / 255, 5.0 / 255)), "green": ((0.0, 34.0 / 255, 34.0 / 255), (1.0, 198.0 / 255, 198.0 / 255)), "blue": ((0.0, 5.0 / 255, 5.0 / 255), (1.0, 24.0 / 255, 24.0 / 255)), "alpha": ((0.0, 1, 1), (1.0, 1, 1)), }, ) def plot_grids(dataset, model_names, out_dir=None): if out_dir is not None: os.mkdir(out_dir) scores = [] for model in model_names: scores.extend(run_experiments(dataset=dataset, model=model)) prefix = "" out = "" # background: rgb(30, 136, 229) # out += "
SHAP Benchmark
\n" # out += "
\n" # out += "
" out += "
\n" # box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); out += "
\n" for ind, model in enumerate(model_names): row_keys, col_keys, data = make_grid(scores, dataset, model) # print(data) # print(colors.red_blue_solid(0.)) # print(colors.red_blue_solid(1.)) # return for metric in col_keys: save_plot = False if metric.startswith("human_"): plot_human(dataset, model, metric) save_plot = True elif metric not in ["local_accuracy", "runtime", "consistency_guarantees"]: plot_curve(dataset, model, metric) save_plot = True if save_plot: buf = io.BytesIO() pl.gcf().set_size_inches(1200.0 / 175, 1000.0 / 175) pl.savefig(buf, format="png", dpi=175) if out_dir is not None: pl.savefig(f"{out_dir}/plot_{dataset}_{model}_{metric}.pdf", format="pdf") pl.close() buf.seek(0) data_uri = base64.b64encode(buf.read()).decode("utf-8").replace("\n", "") plot_id = "plot__" + dataset + "__" + model + "__" + metric prefix += f"" model_title = getattr(models, dataset + "__" + model).__doc__.split("\n")[0].strip() if ind == 0: out += "" for j in range(data.shape[1]): metric_title = getattr(metrics, col_keys[j]).__doc__.split("\n")[0].strip() out += ( "" ) out += "\n" out += "
" + metric_title + "
\n" out += "\n" out += f"\n" for i in range(data.shape[0]): out += "" # if i == 0: # out += "" % (data.shape[0], model_name) method_title = getattr(methods, row_keys[i]).__doc__.split("\n")[0].strip() out += ( "\n" ) for j in range(data.shape[1]): plot_id = "plot__" + dataset + "__" + model + "__" + col_keys[j] out += f"\n" out += "\n" # out += f"" out += "
{model_title}
%s
" + method_title + "" # out += "
" out += ( "
" ) # out += "
" out += "
" out += "
\n" out += ( "
SHAP Benchmark v" + __version__ + "
\n" ) # select { # margin: 50px; # width: 150px; # padding: 5px 35px 5px 5px; # font-size: 16px; # border: 1px solid #ccc; # height: 34px; # -webkit-appearance: none; # -moz-appearance: none; # appearance: none; # background: url(http://www.stackoverflow.com/favicon.ico) 96% / 15% no-repeat #eee; # } # out += "
Dataset:
\n" out += "\n" # out += "" # out += "
CRIC
\n" out += "
\n" # output the legend out += "\n" out += "\n" legend_size = 21 for i in range(legend_size - 9): out += "" out += "" out += "\n" # out += "\n" out += "
Higher score
" val = (legend_size - i - 1) / (legend_size - 1) out += ( "
" ) out += "
Lower score
\n" if out_dir is not None: with open(out_dir + "/index.html", "w") as f: f.write( "
" ) f.write(prefix) f.write(out) f.write("
") else: return HTML(prefix + out)