Tuana commited on
Commit
41e795b
·
1 Parent(s): 0e50607

Fix HF Space graph rendering

Browse files
Files changed (2) hide show
  1. app.py +8 -7
  2. interpret.py +87 -62
app.py CHANGED
@@ -277,7 +277,8 @@ FEATURES_HEADING = "#### Extracted Features"
277
  PROBS_HEADING = "#### Class Probabilities"
278
  SHAPLEY_HEADING = (
279
  "#### Shapley Attribution\n\n"
280
- "*Waterfall: each bar is a feature's Shapley contribution toward the predicted class.*"
 
281
  )
282
  MEANS_HEADING = "#### What This Means"
283
  NOT_READY_MSG = (
@@ -432,7 +433,7 @@ def handle_own_wine(
432
  )
433
 
434
  progress(0.55, desc="Computing Shapley attributions (shapiq)…")
435
- waterfall_fig, attributions, shapley_meta = safe_explain(features, pred_idx)
436
 
437
  progress(0.75, desc="Running what-if probes…")
438
  counterfactuals = run_counterfactuals(clf, features)
@@ -465,7 +466,7 @@ def handle_own_wine(
465
  SHOW,
466
  gr.update(value=probs_to_df(probabilities), visible=True),
467
  SHOW,
468
- gr.update(value=waterfall_fig, visible=waterfall_fig is not None),
469
  gr.update(value=attributions, visible=True),
470
  SHOW,
471
  gr.update(value=explanation, visible=True),
@@ -544,7 +545,7 @@ def handle_sample_predict(
544
  )
545
 
546
  progress(0.5, desc="Computing Shapley attributions (shapiq)…")
547
- waterfall_fig, attributions, shapley_meta = safe_explain(feat, pred_idx)
548
 
549
  progress(0.85, desc="Writing plain-English explanation…")
550
  from agent import generate_layman_explanation
@@ -565,7 +566,7 @@ def handle_sample_predict(
565
  gr.update(value=probs_to_df(probabilities), visible=True),
566
  gr.update(value=result_md, visible=True),
567
  SHOW,
568
- gr.update(value=waterfall_fig, visible=waterfall_fig is not None),
569
  gr.update(value=attributions, visible=True),
570
  SHOW,
571
  gr.update(value=explanation, visible=True),
@@ -670,7 +671,7 @@ Free-tier usage limits apply on Prior Labs; see their docs for current quotas.
670
  with gr.Column():
671
  sample_verdict_md = gr.Markdown(visible=False)
672
  sample_shapley_heading = gr.Markdown(SHAPLEY_HEADING, visible=False)
673
- sample_attributions_plot = gr.Plot(visible=False)
674
  sample_attributions_table = gr.DataFrame(visible=False)
675
  sample_means_heading = gr.Markdown(MEANS_HEADING, visible=False)
676
  sample_explanation_md = gr.Markdown(visible=False)
@@ -719,7 +720,7 @@ Free-tier usage limits apply on Prior Labs; see their docs for current quotas.
719
  own_probs_heading = gr.Markdown(PROBS_HEADING, visible=False)
720
  own_probs_table = gr.DataFrame(visible=False)
721
  own_shapley_heading = gr.Markdown(SHAPLEY_HEADING, visible=False)
722
- own_attributions_plot = gr.Plot(visible=False)
723
  own_attributions_table = gr.DataFrame(visible=False)
724
  own_means_heading = gr.Markdown(MEANS_HEADING, visible=False)
725
  own_explanation_md = gr.Markdown(visible=False)
 
277
  PROBS_HEADING = "#### Class Probabilities"
278
  SHAPLEY_HEADING = (
279
  "#### Shapley Attribution\n\n"
280
+ "*Each bar is a feature's Shapley contribution toward the predicted class "
281
+ "(positive = supports, negative = works against).*"
282
  )
283
  MEANS_HEADING = "#### What This Means"
284
  NOT_READY_MSG = (
 
433
  )
434
 
435
  progress(0.55, desc="Computing Shapley attributions (shapiq)…")
436
+ attribution_img, attributions, shapley_meta = safe_explain(features, pred_idx)
437
 
438
  progress(0.75, desc="Running what-if probes…")
439
  counterfactuals = run_counterfactuals(clf, features)
 
466
  SHOW,
467
  gr.update(value=probs_to_df(probabilities), visible=True),
468
  SHOW,
469
+ gr.update(value=attribution_img, visible=attribution_img is not None),
470
  gr.update(value=attributions, visible=True),
471
  SHOW,
472
  gr.update(value=explanation, visible=True),
 
545
  )
546
 
547
  progress(0.5, desc="Computing Shapley attributions (shapiq)…")
548
+ attribution_img, attributions, shapley_meta = safe_explain(feat, pred_idx)
549
 
550
  progress(0.85, desc="Writing plain-English explanation…")
551
  from agent import generate_layman_explanation
 
566
  gr.update(value=probs_to_df(probabilities), visible=True),
567
  gr.update(value=result_md, visible=True),
568
  SHOW,
569
+ gr.update(value=attribution_img, visible=attribution_img is not None),
570
  gr.update(value=attributions, visible=True),
571
  SHOW,
572
  gr.update(value=explanation, visible=True),
 
671
  with gr.Column():
672
  sample_verdict_md = gr.Markdown(visible=False)
673
  sample_shapley_heading = gr.Markdown(SHAPLEY_HEADING, visible=False)
674
+ sample_attributions_plot = gr.Image(visible=False, type="pil")
675
  sample_attributions_table = gr.DataFrame(visible=False)
676
  sample_means_heading = gr.Markdown(MEANS_HEADING, visible=False)
677
  sample_explanation_md = gr.Markdown(visible=False)
 
720
  own_probs_heading = gr.Markdown(PROBS_HEADING, visible=False)
721
  own_probs_table = gr.DataFrame(visible=False)
722
  own_shapley_heading = gr.Markdown(SHAPLEY_HEADING, visible=False)
723
+ own_attributions_plot = gr.Image(visible=False, type="pil")
724
  own_attributions_table = gr.DataFrame(visible=False)
725
  own_means_heading = gr.Markdown(MEANS_HEADING, visible=False)
726
  own_explanation_md = gr.Markdown(visible=False)
interpret.py CHANGED
@@ -2,13 +2,17 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import warnings
6
- from typing import Any, Dict, Optional, Tuple
7
 
 
 
 
8
  import matplotlib.pyplot as plt
9
  import numpy as np
10
  import pandas as pd
11
- from matplotlib.figure import Figure
12
  from tabpfn_client import TabPFNClassifier
13
  from tabpfn_extensions.interpretability.shapiq import get_tabpfn_imputation_explainer
14
 
@@ -19,6 +23,12 @@ _explainer_cache: Dict[int, object] = {}
19
  _BACKGROUND_ROWS = 256
20
  _SHAPIQ_BUDGET = 64
21
 
 
 
 
 
 
 
22
 
23
  def clear_explainer_cache() -> None:
24
  """Drop cached explainers after reconnecting with a new TabPFN token."""
@@ -33,8 +43,6 @@ def _get_explainer(
33
  if class_index not in _explainer_cache:
34
  n = min(_BACKGROUND_ROWS, len(train_df))
35
  background = train_df[FEATURES].sample(n=n, random_state=42).values
36
- # Permutation sampling avoids KernelSHAP's regression solve, which can emit
37
- # divide-by-zero / overflow warnings with near-flat TabPFN coalitions.
38
  _explainer_cache[class_index] = get_tabpfn_imputation_explainer(
39
  clf,
40
  background,
@@ -48,49 +56,76 @@ def _get_explainer(
48
  return _explainer_cache[class_index]
49
 
50
 
51
- def _style_waterfall(fig: Figure) -> Figure:
52
- """Dark-theme styling and extra margins so labels are not clipped in Gradio."""
53
- fig.patch.set_facecolor("#1e0f14")
54
- primary_ax = fig.axes[0] if fig.axes else None
55
-
56
- for ax in fig.axes:
57
- ax.set_facecolor("#1e0f14")
58
- ax.tick_params(colors="#f5e6d3", labelcolor="#f5e6d3")
59
- for spine in ax.spines.values():
60
- spine.set_color("#722f37")
61
- if ax.title:
62
- ax.title.set_color("#f5e6d3")
63
- if ax.xaxis.label:
64
- ax.xaxis.label.set_color("#c9a87c")
65
- for label in ax.get_yticklabels() + ax.get_xticklabels():
66
- label.set_color("#f5e6d3")
67
- xmin, xmax = ax.get_xlim()
68
- pad = (xmax - xmin) * 0.14
69
- ax.set_xlim(xmin - pad, xmax + pad)
70
-
71
- if primary_ax is not None:
72
- w, h = fig.get_size_inches()
73
- fig.set_size_inches(max(w, 12.0), max(h, 6.5))
74
- primary_ax.tick_params(axis="y", pad=10)
75
- # Wide left margin for full feature names; padding on x for arrow heads.
76
- fig.subplots_adjust(left=0.42, right=0.90, bottom=0.16, top=0.86)
77
-
78
- return fig
79
-
80
-
81
- def _shapley_table(sv) -> pd.DataFrame:
82
- """First-order Shapley values as a readable table."""
83
  rows = []
84
  for interaction, value in sv.dict_values.items():
85
  if len(interaction) != 1:
86
  continue
87
- idx = interaction[0]
88
- key = FEATURES[idx]
89
- magnitude = abs(float(value))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  bar_len = max(1, round(magnitude * 40)) if magnitude > 0 else 0
91
  rows.append({
92
- "Feature": FEATURE_DISPLAY_NAMES[key],
93
- "Shapley": round(float(value), 4),
94
  "Effect": (
95
  "supports predicted class" if value > 0
96
  else "works against predicted class" if value < 0
@@ -98,10 +133,7 @@ def _shapley_table(sv) -> pd.DataFrame:
98
  ),
99
  "Bar": "█" * bar_len,
100
  })
101
- df = pd.DataFrame(rows)
102
- if df.empty:
103
- return df
104
- return df.reindex(df["Shapley"].abs().sort_values(ascending=False).index)
105
 
106
 
107
  def explain_prediction(
@@ -109,8 +141,8 @@ def explain_prediction(
109
  train_df: pd.DataFrame,
110
  features: dict,
111
  class_index: int,
112
- ) -> Tuple[Optional[Figure], pd.DataFrame, Dict[str, Any]]:
113
- """Run shapiq SV attribution and return waterfall figure, table, and metadata."""
114
  explainer = _get_explainer(clf, train_df, class_index)
115
  x = np.array([[features[f] for f in FEATURES]], dtype=float)
116
  with warnings.catch_warnings():
@@ -121,23 +153,16 @@ def explain_prediction(
121
  )
122
  sv = explainer.explain(x, budget=_SHAPIQ_BUDGET)
123
 
124
- display_names = np.array([FEATURE_DISPLAY_NAMES[f] for f in FEATURES])
125
- ax = sv.plot_waterfall(
126
- feature_names=display_names,
127
- show=False,
128
- max_display=len(FEATURES),
129
- abbreviate=False,
130
- )
131
- fig = ax.figure if ax is not None else plt.gcf()
132
- _style_waterfall(fig)
133
 
134
- shapley_sum = sum(
135
- float(v) for k, v in sv.dict_values.items() if len(k) == 1
136
- )
137
  meta = {
138
- "baseline_probability": round(float(sv.baseline_value), 4),
139
- "final_probability": round(float(sv.baseline_value) + shapley_sum, 4),
140
  "predicted_class_index": class_index,
141
  "attribution_method": "shapiq SV via permutation sampling (first-order)",
142
  }
143
- return fig, _shapley_table(sv), meta
 
2
 
3
  from __future__ import annotations
4
 
5
+ import io
6
  import warnings
7
+ from typing import Any, Dict, List, Optional, Tuple
8
 
9
+ import matplotlib
10
+
11
+ matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
13
  import numpy as np
14
  import pandas as pd
15
+ from PIL import Image
16
  from tabpfn_client import TabPFNClassifier
17
  from tabpfn_extensions.interpretability.shapiq import get_tabpfn_imputation_explainer
18
 
 
23
  _BACKGROUND_ROWS = 256
24
  _SHAPIQ_BUDGET = 64
25
 
26
+ _POS_COLOR = "#e85d75"
27
+ _NEG_COLOR = "#4a9eff"
28
+ _BG_COLOR = "#1e0f14"
29
+ _TEXT_COLOR = "#f5e6d3"
30
+ _GRID_COLOR = "#722f37"
31
+
32
 
33
  def clear_explainer_cache() -> None:
34
  """Drop cached explainers after reconnecting with a new TabPFN token."""
 
43
  if class_index not in _explainer_cache:
44
  n = min(_BACKGROUND_ROWS, len(train_df))
45
  background = train_df[FEATURES].sample(n=n, random_state=42).values
 
 
46
  _explainer_cache[class_index] = get_tabpfn_imputation_explainer(
47
  clf,
48
  background,
 
56
  return _explainer_cache[class_index]
57
 
58
 
59
+ def _shapley_pairs(sv) -> List[Tuple[str, float]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  rows = []
61
  for interaction, value in sv.dict_values.items():
62
  if len(interaction) != 1:
63
  continue
64
+ key = FEATURES[interaction[0]]
65
+ rows.append((FEATURE_DISPLAY_NAMES[key], float(value)))
66
+ rows.sort(key=lambda item: abs(item[1]), reverse=True)
67
+ return rows
68
+
69
+
70
+ def _shapley_bar_image(
71
+ pairs: List[Tuple[str, float]],
72
+ baseline: float,
73
+ final: float,
74
+ ) -> Image.Image:
75
+ """Diverging bar chart — rasterized for reliable display in Gradio on HF."""
76
+ if not pairs:
77
+ fig, ax = plt.subplots(figsize=(10, 2), facecolor=_BG_COLOR)
78
+ ax.set_facecolor(_BG_COLOR)
79
+ ax.text(0.5, 0.5, "No attributions available", ha="center", va="center", color=_TEXT_COLOR)
80
+ ax.axis("off")
81
+ else:
82
+ names = [name for name, _ in pairs]
83
+ values = [value for _, value in pairs]
84
+ height = max(5.0, len(names) * 0.42)
85
+ fig, ax = plt.subplots(figsize=(10, height), facecolor=_BG_COLOR)
86
+ ax.set_facecolor(_BG_COLOR)
87
+ colors = [_POS_COLOR if v > 0 else _NEG_COLOR for v in values]
88
+ y_pos = np.arange(len(names))
89
+ ax.barh(y_pos, values, color=colors, height=0.72)
90
+ ax.set_yticks(y_pos)
91
+ ax.set_yticklabels(names, color=_TEXT_COLOR, fontsize=10)
92
+ ax.axvline(0, color=_GRID_COLOR, linewidth=1)
93
+ ax.tick_params(axis="x", colors=_TEXT_COLOR, labelcolor=_TEXT_COLOR)
94
+ ax.set_xlabel("Shapley contribution (predicted-class probability)", color="#c9a87c")
95
+ ax.set_title(
96
+ f"Baseline {baseline:.3f} → prediction {final:.3f}",
97
+ color=_TEXT_COLOR,
98
+ fontsize=11,
99
+ pad=10,
100
+ )
101
+ for spine in ax.spines.values():
102
+ spine.set_color(_GRID_COLOR)
103
+ ax.grid(axis="x", color=_GRID_COLOR, alpha=0.35, linestyle="--", linewidth=0.6)
104
+ ax.invert_yaxis()
105
+ fig.tight_layout()
106
+
107
+ buf = io.BytesIO()
108
+ fig.savefig(
109
+ buf,
110
+ format="png",
111
+ dpi=150,
112
+ bbox_inches="tight",
113
+ facecolor=_BG_COLOR,
114
+ edgecolor="none",
115
+ )
116
+ plt.close(fig)
117
+ buf.seek(0)
118
+ return Image.open(buf)
119
+
120
+
121
+ def _shapley_table(pairs: List[Tuple[str, float]]) -> pd.DataFrame:
122
+ rows = []
123
+ for name, value in pairs:
124
+ magnitude = abs(value)
125
  bar_len = max(1, round(magnitude * 40)) if magnitude > 0 else 0
126
  rows.append({
127
+ "Feature": name,
128
+ "Shapley": round(value, 4),
129
  "Effect": (
130
  "supports predicted class" if value > 0
131
  else "works against predicted class" if value < 0
 
133
  ),
134
  "Bar": "█" * bar_len,
135
  })
136
+ return pd.DataFrame(rows)
 
 
 
137
 
138
 
139
  def explain_prediction(
 
141
  train_df: pd.DataFrame,
142
  features: dict,
143
  class_index: int,
144
+ ) -> Tuple[Optional[Image.Image], pd.DataFrame, Dict[str, Any]]:
145
+ """Run shapiq SV attribution and return a bar-chart image, table, and metadata."""
146
  explainer = _get_explainer(clf, train_df, class_index)
147
  x = np.array([[features[f] for f in FEATURES]], dtype=float)
148
  with warnings.catch_warnings():
 
153
  )
154
  sv = explainer.explain(x, budget=_SHAPIQ_BUDGET)
155
 
156
+ pairs = _shapley_pairs(sv)
157
+ shapley_sum = sum(value for _, value in pairs)
158
+ baseline = float(sv.baseline_value)
159
+ final = baseline + shapley_sum
 
 
 
 
 
160
 
161
+ image = _shapley_bar_image(pairs, baseline, final)
 
 
162
  meta = {
163
+ "baseline_probability": round(baseline, 4),
164
+ "final_probability": round(final, 4),
165
  "predicted_class_index": class_index,
166
  "attribution_method": "shapiq SV via permutation sampling (first-order)",
167
  }
168
+ return image, _shapley_table(pairs), meta