Spaces:
Paused
Paused
Download explainability/shap_explainer/visualization.py from priya-ts/varuna-backend: direct link, hf CLI and curl.
- Browser
- Download file 7.04 kB
-
https://huggingface.co/spaces/priya-ts/varuna-backend/resolve/main/explainability/shap_explainer/visualization.py
- Command line
-
hf download hf://spaces/priya-ts/varuna-backend/explainability/shap_explainer/visualization.py
-
curl -L -o visualization.py https://huggingface.co/spaces/priya-ts/varuna-backend/resolve/main/explainability/shap_explainer/visualization.py
7.04 kB
| """ | |
| SHAP visualizations for the hybrid model. | |
| Three figures, all rendered from measured attributions: | |
| - **bar** β ranked feature importance for one prediction | |
| - **waterfall** β how the baseline risk becomes this prediction, step by step | |
| - **summary** β overall model behaviour across a sample of the record | |
| Written to `reports/shap/`. Colour is used consistently throughout: red | |
| raises risk, blue lowers it. Rendering uses the Agg backend so the module | |
| is safe to import from a headless process. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| from explainability.shap_explainer.config import ShapConfig | |
| from explainability.shap_explainer.explainer import FusionShapExplainer, ShapExplanation | |
| logger = logging.getLogger(__name__) | |
| RAISES = "#c0392b" # red β pushes risk up | |
| LOWERS = "#2471a3" # blue β pushes risk down | |
| def _colour(value: float) -> str: | |
| return RAISES if value > 0 else LOWERS | |
| def plot_feature_importance( | |
| explanation: ShapExplanation, config: ShapConfig, *, filename: str = "shap_bar.png" | |
| ) -> Path: | |
| """Ranked bar chart of the features driving one prediction.""" | |
| config.ensure_output_dirs() | |
| items = explanation.top(config.top_k)[::-1] | |
| if not items: | |
| raise ValueError("No attributions to plot") | |
| labels = [f"{c.label}\n({c.value:g})" if c.value is not None else c.label for c in items] | |
| values = [c.contribution for c in items] | |
| fig, ax = plt.subplots(figsize=(9, 0.62 * len(items) + 2.2)) | |
| ax.barh(labels, values, color=[_colour(v) for v in values]) | |
| ax.axvline(0, color="#444", linewidth=0.8) | |
| ax.set_xlabel("contribution to fused risk (probability units)") | |
| ax.set_title("Why this risk score β ranked feature contributions") | |
| span = max(abs(v) for v in values) or 1.0 | |
| for y, item in enumerate(items): | |
| offset = span * 0.02 | |
| ax.text( | |
| item.contribution + (offset if item.contribution >= 0 else -offset), | |
| y, | |
| f"{item.share * 100:+.1f}% {item.impact}", | |
| va="center", | |
| ha="left" if item.contribution >= 0 else "right", | |
| fontsize=8, | |
| ) | |
| ax.set_xlim(-span * 1.45, span * 1.45) | |
| fig.tight_layout() | |
| path = config.reports_dir / filename | |
| fig.savefig(path, dpi=150) | |
| plt.close(fig) | |
| logger.info("SHAP bar chart -> %s", path) | |
| return path | |
| def plot_waterfall( | |
| explanation: ShapExplanation, config: ShapConfig, *, filename: str = "shap_waterfall.png" | |
| ) -> Path: | |
| """Baseline β prediction, one cumulative step per feature. | |
| Features outside the top-k are pooled into a single "other features" | |
| step so the bars still sum to the explained risk rather than silently | |
| dropping mass. | |
| """ | |
| config.ensure_output_dirs() | |
| top = explanation.top(config.top_k) | |
| remainder = sum( | |
| c.contribution for c in explanation.contributions if c not in top | |
| ) | |
| steps = [(c.label, c.contribution) for c in top] | |
| if abs(remainder) > 1e-9: | |
| steps.append((f"other features ({len(explanation.contributions) - len(top)})", remainder)) | |
| fig, ax = plt.subplots(figsize=(10, 0.62 * len(steps) + 3.0)) | |
| cumulative = explanation.base_value | |
| for index, (label, value) in enumerate(steps): | |
| ax.barh(index, value, left=cumulative, color=_colour(value), height=0.62) | |
| ax.text( | |
| cumulative + value / 2, index, f"{value:+.3f}", | |
| ha="center", va="center", fontsize=8, | |
| color="white" if abs(value) > 0.02 else "#222", | |
| ) | |
| cumulative += value | |
| ax.set_yticks(range(len(steps)), [label for label, _ in steps]) | |
| ax.invert_yaxis() | |
| ax.axvline(explanation.base_value, color="#555", linestyle="--", linewidth=1, | |
| label=f"baseline {explanation.base_value:.3f}") | |
| ax.axvline(explanation.predicted_risk, color="#111", linewidth=1.2, | |
| label=f"explained risk {explanation.predicted_risk:.3f}") | |
| ax.set_xlabel("fused risk (probability units)") | |
| ax.set_title("From baseline risk to this prediction") | |
| ax.legend(fontsize=8, loc="lower right") | |
| fig.tight_layout() | |
| path = config.reports_dir / filename | |
| fig.savefig(path, dpi=150) | |
| plt.close(fig) | |
| logger.info("SHAP waterfall -> %s", path) | |
| return path | |
| def plot_summary( | |
| explainer: FusionShapExplainer, | |
| frame: pd.DataFrame, | |
| config: ShapConfig, | |
| *, | |
| filename: str = "shap_summary.png", | |
| sample_size: int | None = None, | |
| ) -> Path: | |
| """Overall model behaviour: attribution spread across many region-days. | |
| Each row is a feature; each dot is one region-day, positioned by its | |
| attribution and coloured by whether that day's feature value was high | |
| or low. Reading it: a feature whose high values sit on the right raises | |
| risk when it rises. | |
| """ | |
| config.ensure_output_dirs() | |
| size = sample_size or config.summary_sample_size | |
| sample = frame.sample(min(size, len(frame)), random_state=0) if len(frame) > size else frame | |
| rows: list[dict[str, float]] = [] | |
| values: list[dict[str, float]] = [] | |
| for index in range(len(sample)): | |
| explanation = explainer.explain(sample.iloc[[index]]) | |
| rows.append({c.feature: c.contribution for c in explanation.contributions}) | |
| values.append({c.feature: (c.value if c.value is not None else np.nan) | |
| for c in explanation.contributions}) | |
| attributions = pd.DataFrame(rows).fillna(0.0) | |
| observed = pd.DataFrame(values) | |
| ranking = attributions.abs().mean().sort_values(ascending=False) | |
| features = list(ranking.index[: config.top_k])[::-1] | |
| fig, ax = plt.subplots(figsize=(10, 0.55 * len(features) + 2.6)) | |
| rng = np.random.default_rng(0) | |
| for y, feature in enumerate(features): | |
| x = attributions[feature].to_numpy() | |
| raw = observed[feature].to_numpy(dtype=float) | |
| finite = np.isfinite(raw) | |
| if finite.sum() > 1 and np.nanmax(raw[finite]) > np.nanmin(raw[finite]): | |
| low, high = np.nanmin(raw[finite]), np.nanmax(raw[finite]) | |
| shade = np.clip((raw - low) / (high - low), 0, 1) | |
| else: | |
| shade = np.full_like(x, 0.5, dtype=float) | |
| jitter = rng.uniform(-0.16, 0.16, size=len(x)) | |
| scatter = ax.scatter(x, y + jitter, c=shade, cmap="coolwarm", s=13, | |
| alpha=0.75, linewidths=0) | |
| ax.set_yticks(range(len(features)), [config.label_for(f) for f in features]) | |
| ax.axvline(0, color="#444", linewidth=0.8) | |
| ax.set_xlabel("contribution to fused risk (probability units)") | |
| ax.set_title(f"Overall model behaviour β {len(sample)} region-days") | |
| bar = fig.colorbar(scatter, ax=ax, pad=0.02) | |
| bar.set_label("feature value (low β high)", fontsize=8) | |
| fig.tight_layout() | |
| path = config.reports_dir / filename | |
| fig.savefig(path, dpi=150) | |
| plt.close(fig) | |
| logger.info("SHAP summary (%d rows) -> %s", len(sample), path) | |
| return path | |