Files
hypertower/v4/figures/F8_quadrant_plot.py
rpotter6298 3d7777f010 Add new experiments and analysis scripts for dropzero features
- Introduced `fused_importance_with_axial.py` to evaluate the importance of Axial_Length in the fused-head model.
- Created JSON configurations for various experiments excluding zero-importance clinical features:
  - `cd_solo_bilateral_dropzero.json`: Bilateral clinical-only evaluation.
  - `cd_solo_single_dropzero.json`: Single-eye clinical-only evaluation.
  - `ensemble_refugelike_ckpt_dropzero.json`: Ensemble model with dropped zero-importance features.
  - `ensemble_single_refugelike_dropzero.json`: Single-eye ensemble model with dropped features.
2026-08-24 12:26:16 +02:00

120 lines
3.8 KiB
Python

"""F8 quadrant attention chart (full-image scope).
Reads the per-cell quadrant fractions produced by
``v4.figures.F8_explainability`` (see CSV at
``output/F8_quadrant_fractions.csv``) and renders a single-panel grouped bar
chart of full-image Grad-CAM intensity by optic-disc quadrant. Within-disc
and peri-disc scopes are present in the CSV but are not plotted here; see
the CSV for the per-cell numbers if you need them.
Re-run:
python -m v4.figures.F8_quadrant_plot
"""
from __future__ import annotations
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
CSV = Path(__file__).parent / "output" / "F8_quadrant_fractions.csv"
OUT = Path(__file__).parent / "output" / "F8_quadrant_attention.png"
QUAD_ORDER = ("ST", "SN", "IT", "IN")
QUAD_LABEL = {
"ST": "Superotemporal",
"SN": "Superonasal",
"IT": "Inferotemporal",
"IN": "Inferonasal",
}
CELL_ORDER = [
("Normal", "correct"),
("Normal", "incorrect"),
("Glaucoma", "correct"),
("Glaucoma", "incorrect"),
]
CELL_COLOR = {
("Normal", "correct"): "#3B6FB5",
("Normal", "incorrect"): "#8BB0DA",
("Glaucoma", "correct"): "#c44e52",
("Glaucoma", "incorrect"): "#e6a3a4",
}
CELL_LABEL = {
("Normal", "correct"): "Normal correct",
("Normal", "incorrect"): "Normal incorrect",
("Glaucoma", "correct"): "Glaucoma correct",
("Glaucoma", "incorrect"): "Glaucoma incorrect",
}
def render() -> None:
if not CSV.exists():
raise SystemExit(
f"CSV {CSV} not found. Run `python -m v4.figures.F8_explainability "
"--only-gradcam --run-gradcam` first."
)
df = pd.read_csv(CSV)
df_full = df[df["scope"] == "full"]
fig, ax = plt.subplots(figsize=(9.6, 6.0))
n_quad = len(QUAD_ORDER)
n_cell = len(CELL_ORDER)
bar_w = 0.18
x = np.arange(n_quad, dtype=float)
for i, cell in enumerate(CELL_ORDER):
means, sds = [], []
n_eyes = None
for q in QUAD_ORDER:
row = df_full[
(df_full["class"] == cell[0])
& (df_full["outcome"] == cell[1])
& (df_full["quadrant"] == q)
]
means.append(float(row["mean"].iloc[0]) if len(row) else float("nan"))
sds.append(float(row["sd"].iloc[0]) if len(row) else float("nan"))
if n_eyes is None and len(row):
n_eyes = int(row["n_eyes"].iloc[0])
offsets = (i - (n_cell - 1) / 2.0) * bar_w
bars = ax.bar(
x + offsets, means, width=bar_w,
yerr=sds, capsize=2,
color=CELL_COLOR[cell], alpha=0.92,
edgecolor="black", linewidth=0.6,
label=f"{CELL_LABEL[cell]} (n = {n_eyes})",
error_kw=dict(ecolor="#444", linewidth=0.8, capthick=0.8),
)
for rect, m in zip(bars, means):
if np.isnan(m):
continue
ax.text(rect.get_x() + rect.get_width() / 2, m + 0.005,
f"{m:.2f}", ha="center", va="bottom",
fontsize=8.5, color="#222")
ax.set_xticks(x)
ax.set_xticklabels([QUAD_LABEL[q] for q in QUAD_ORDER], fontsize=11)
ax.set_ylabel("Mean fraction of full-image Grad-CAM intensity", fontsize=11)
ax.grid(axis="y", alpha=0.3, linestyle="--")
ax.set_ylim(0, max(ax.get_ylim()[1], 0.7))
ax.axhline(0.25, color="#888", linestyle=":", linewidth=0.8, alpha=0.6, zorder=0)
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.10), ncol=4,
framealpha=0.94, fontsize=10)
fig.tight_layout(rect=(0, 0.04, 1, 0.98))
OUT.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(OUT, dpi=180, bbox_inches="tight")
plt.close(fig)
print(f"saved {OUT}")
if __name__ == "__main__":
render()