Add analysis scripts and experiment configurations for bridge attention and sensitivity studies
- Introduced `bridge_attention_ceiling_check.py` for variance decomposition analysis on bridge attention configurations. - Added `bridge_attention_readout.py` to perform per-tower gate and contribution readouts, including AUC sanity checks. - Created multiple JSON configuration files for backbone replication experiments, including anonymous CV variants and basic backbones. - Implemented sensitivity experiments to evaluate the impact of axial length inclusion and EfficientNetV2-M performance at higher resolutions. - Added a memory probe script to assess GPU memory usage during training with EfficientNetV2-M.
@@ -1,50 +1,56 @@
|
||||
"""F2 — Backbone selection panel.
|
||||
"""F2 - Backbone selection panel.
|
||||
|
||||
Box plot in the style of v3/figures/phase2_analysis.png (black-bordered boxes,
|
||||
red median lines, baseline median reference). Three left-to-right sections:
|
||||
|
||||
Block 1 (blue) — Basic backbones (img-only, single-eye, ImageNet pretraining):
|
||||
Block 1 (blue) -- Basic backbones (img-only, single-eye, ImageNet pretraining):
|
||||
VGG16, MobileNetV2, DenseNet121, InceptionV3, ResNet50
|
||||
Sourced from v3 phase 1 / phase 2 fold AUCs. Will be refined with v4
|
||||
10x5 runs later; means should not move much.
|
||||
Sourced from v4 experiments/backbone_replication/basic_* (10x5 = 50 fold-rep
|
||||
AUCs each, img-only single-eye, patient-grouped CV).
|
||||
|
||||
Block 2 (blue) — ResNet50 preprocessing/CV variations:
|
||||
leaky CV, GT crop, U-Net crop (all 2.5x scale; 1.1x dropped from labels)
|
||||
Sourced from v3 phase 2 'classic_test_auc' (single-mode image-only).
|
||||
Block 2 (blue) -- ResNet50 preprocessing/CV variations:
|
||||
Anonymous CV, GT crop, U-Net crop (disc crops use margin 2.5x)
|
||||
"Anonymous CV" = patient-identity-agnostic cross-validation: fold
|
||||
assignment ignores PAPILA's patient IDs, allowing the same patient's
|
||||
OD/OS pair to be split across train and test. Reflects the standard
|
||||
protocol in benchmark reports that do not have patient-level labels
|
||||
(or do not respect them).
|
||||
Sourced from v4 experiments/backbone_replication/{anonymous_cv,gtcrop,
|
||||
unetcrop}_refugelike.
|
||||
|
||||
Block 3 (orange) — Baseline reference:
|
||||
"Baseline (fine-tuned ResNet50)" — what we previously called refugelike.
|
||||
Sourced from v3 phase 2 imageonly_refugelike_proper.
|
||||
Block 3 (orange) -- Baseline reference:
|
||||
"Baseline (fine-tuned ResNet50)" -- REFUGE-pretrained R50 image-only,
|
||||
sourced from v4 experiments/refuge_v2m_baseline/img_solo_single_refugelike.
|
||||
|
||||
Each non-baseline box is labelled with a Wilcoxon two-sided p-value comparing
|
||||
its fold AUCs to the baseline.
|
||||
its fold-rep AUCs to the baseline fold-rep AUCs.
|
||||
|
||||
Re-run anytime:
|
||||
python -m v4.figures.F2_papila_replication_and_single_mode
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import wilcoxon
|
||||
|
||||
from v4.figures.util.loaders import REPO_ROOT
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "F2_backbones.png"
|
||||
|
||||
# ── Colors / styling (mirrors v3 phase2_analysis) ────────────────────────────
|
||||
C_VAR = "#4c72b0" # blue — non-baseline boxes (basic backbones + variants)
|
||||
C_BASE = "#dd8452" # orange — baseline reference box
|
||||
C_MEDIAN = "#c44e52" # red — median line inside boxes
|
||||
# Colors / styling (mirrors v3 phase2_analysis)
|
||||
C_VAR = "#4c72b0" # blue - non-baseline boxes
|
||||
C_BASE = "#dd8452" # orange - baseline reference box
|
||||
C_MEDIAN = "#c44e52" # red - median line inside boxes
|
||||
ALPHA = 0.82
|
||||
|
||||
V3_PHASE1_DIR = REPO_ROOT / "v3" / "results" / "phase1"
|
||||
V3_PHASE2_DIR = REPO_ROOT / "v3" / "results" / "phase2"
|
||||
# Stage key for img-only single-eye fusion
|
||||
STAGE_KEY = "img_fuse_test_auc"
|
||||
|
||||
|
||||
def _wilcoxon_p(a: np.ndarray, b: np.ndarray) -> float:
|
||||
@@ -57,67 +63,64 @@ def _wilcoxon_p(a: np.ndarray, b: np.ndarray) -> float:
|
||||
return float("nan")
|
||||
|
||||
|
||||
def _load_phase1_fold_aucs(subdir: str) -> np.ndarray:
|
||||
fp = V3_PHASE1_DIR / subdir / "fold_metrics.csv"
|
||||
if not fp.exists():
|
||||
return np.array([])
|
||||
df = pd.read_csv(fp)
|
||||
return df["auc"].dropna().astype(float).values
|
||||
|
||||
|
||||
def _load_phase2_classic_aucs(run_name: str) -> np.ndarray:
|
||||
"""Collect classic_test_auc across all rep×fold for a phase 2 run folder."""
|
||||
root = V3_PHASE2_DIR / run_name
|
||||
def _load_fold_aucs(rel: str) -> np.ndarray:
|
||||
"""Collect STAGE_KEY across all rep x fold for a v4 results subdirectory."""
|
||||
root = RESULTS_ROOT / rel
|
||||
if not root.exists():
|
||||
return np.array([])
|
||||
out: list[float] = []
|
||||
for rep in sorted(root.glob("rep*")):
|
||||
fp = rep / "binary" / "single" / "fold_results.csv"
|
||||
if not fp.exists(): continue
|
||||
df = pd.read_csv(fp)
|
||||
if "classic_test_auc" not in df.columns: continue
|
||||
out.extend(df["classic_test_auc"].dropna().astype(float).tolist())
|
||||
for s in sorted(root.glob("rep*/binary/summary.json")):
|
||||
d = json.loads(s.read_text())
|
||||
for fr in d.get("fold_results", []):
|
||||
v = fr.get(STAGE_KEY)
|
||||
if v is None or not np.isfinite(v):
|
||||
continue
|
||||
out.append(float(v))
|
||||
return np.array(out)
|
||||
|
||||
|
||||
# ── Per-section data definitions ─────────────────────────────────────────────
|
||||
# Each entry: (label, loader_fn, *args)
|
||||
# Per-section data definitions (label, results-subdir under RESULTS_ROOT)
|
||||
|
||||
BASIC_BACKBONES = [
|
||||
("VGG16", _load_phase1_fold_aucs, "cnn_vgg16"),
|
||||
("MobileNetV2", _load_phase1_fold_aucs, "cnn_mobilenet_v2"),
|
||||
("DenseNet121", _load_phase1_fold_aucs, "cnn_densenet121"),
|
||||
("InceptionV3", _load_phase1_fold_aucs, "cnn_inception_v3"),
|
||||
# Use phase 2 ResNet50 (50 fold AUCs) for tighter statistics on the
|
||||
# backbone that we sweep variations of in block 2.
|
||||
("ResNet50", _load_phase2_classic_aucs, "imageonly_resnet50_proper"),
|
||||
("VGG16", "backbone_replication/basic_vgg16"),
|
||||
("MobileNetV2", "backbone_replication/basic_mobilenet_v2"),
|
||||
("DenseNet121", "backbone_replication/basic_densenet121"),
|
||||
("InceptionV3", "backbone_replication/basic_inception_v3"),
|
||||
("ResNet50", "backbone_replication/basic_resnet50"),
|
||||
]
|
||||
|
||||
RESNET_VARIATIONS = [
|
||||
("leaky CV", _load_phase2_classic_aucs, "imageonly_resnet50_leaky"),
|
||||
("GT crop", _load_phase2_classic_aucs, "imageonly_resnet50_gtcrop_2.5"),
|
||||
("U-Net crop", _load_phase2_classic_aucs, "imageonly_resnet50_unetcrop_2.5"),
|
||||
("Anonymous CV", "backbone_replication/anonymous_cv_refugelike"),
|
||||
("GT crop", "backbone_replication/gtcrop_refugelike"),
|
||||
("U-Net crop", "backbone_replication/unetcrop_refugelike"),
|
||||
]
|
||||
|
||||
BASELINE_LABEL = "baseline\n(fine-tuned ResNet50)"
|
||||
BASELINE_DATA = (_load_phase2_classic_aucs, "imageonly_refugelike_proper")
|
||||
BASELINE_REL = "refuge_v2m_baseline/img_solo_single_refugelike"
|
||||
|
||||
|
||||
def render() -> None:
|
||||
# Load everything
|
||||
block1 = [(lbl, fn(arg)) for lbl, fn, arg in BASIC_BACKBONES]
|
||||
block2 = [(lbl, fn(arg)) for lbl, fn, arg in RESNET_VARIATIONS]
|
||||
base_fn, base_arg = BASELINE_DATA
|
||||
base_aucs = base_fn(base_arg)
|
||||
block1 = [(lbl, _load_fold_aucs(rel)) for lbl, rel in BASIC_BACKBONES]
|
||||
block2 = [(lbl, _load_fold_aucs(rel)) for lbl, rel in RESNET_VARIATIONS]
|
||||
base_aucs = _load_fold_aucs(BASELINE_REL)
|
||||
|
||||
print("Block 1 — Basic backbones:")
|
||||
print("Block 1 - Basic backbones (ImageNet pretraining):")
|
||||
for lbl, a in block1:
|
||||
print(f" {lbl:<14s} n={len(a):>3d} mean={a.mean():.3f}±{a.std():.3f}" if len(a) else f" {lbl:<14s} no data")
|
||||
print("Block 2 — ResNet50 variations:")
|
||||
if len(a):
|
||||
print(f" {lbl:<14s} n={len(a):>3d} mean={a.mean():.4f} +/- {a.std():.4f}")
|
||||
else:
|
||||
print(f" {lbl:<14s} no data")
|
||||
print("Block 2 - ResNet50 (REFUGE) variations:")
|
||||
for lbl, a in block2:
|
||||
print(f" {lbl:<14s} n={len(a):>3d} mean={a.mean():.3f}±{a.std():.3f}" if len(a) else f" {lbl:<14s} no data")
|
||||
print(f"Block 3 — Baseline: n={len(base_aucs)} "
|
||||
f"mean={base_aucs.mean():.3f}±{base_aucs.std():.3f}" if len(base_aucs) else "Block 3 — no baseline data")
|
||||
if len(a):
|
||||
print(f" {lbl:<14s} n={len(a):>3d} mean={a.mean():.4f} +/- {a.std():.4f}")
|
||||
else:
|
||||
print(f" {lbl:<14s} no data")
|
||||
if len(base_aucs):
|
||||
print(f"Block 3 - Baseline: n={len(base_aucs)} "
|
||||
f"mean={base_aucs.mean():.4f} +/- {base_aucs.std():.4f}")
|
||||
else:
|
||||
print("Block 3 - no baseline data")
|
||||
|
||||
# Lay out positions
|
||||
gap = 0.7
|
||||
@@ -135,7 +138,6 @@ def render() -> None:
|
||||
section3_left = p
|
||||
pos.append(p)
|
||||
section3_right = p
|
||||
total_w = p + 0.6
|
||||
|
||||
fig, ax = plt.subplots(figsize=(13, 5.8))
|
||||
fig.suptitle("Backbone Selection", fontsize=13, fontweight="bold")
|
||||
@@ -156,10 +158,10 @@ def render() -> None:
|
||||
all_labels.append(lbl); all_aucs.append(a); all_colors.append(C_VAR)
|
||||
all_labels.append(BASELINE_LABEL); all_aucs.append(base_aucs); all_colors.append(C_BASE)
|
||||
|
||||
# Draw boxes
|
||||
for x, aucs, color in zip(pos, all_aucs, all_colors):
|
||||
if not len(aucs): continue
|
||||
bp = ax.boxplot(
|
||||
if not len(aucs):
|
||||
continue
|
||||
ax.boxplot(
|
||||
aucs, positions=[x], widths=box_w, patch_artist=True, manage_ticks=False,
|
||||
boxprops=dict(facecolor=color, alpha=ALPHA, **boxprops_kw),
|
||||
medianprops=medianprops,
|
||||
@@ -168,31 +170,27 @@ def render() -> None:
|
||||
flierprops=flierprops,
|
||||
)
|
||||
|
||||
# Baseline median reference line spanning the variant blocks
|
||||
if len(base_aucs):
|
||||
ax.axhline(np.median(base_aucs),
|
||||
color=C_BASE, linewidth=1.2, linestyle="--", alpha=0.55,
|
||||
label="Baseline median")
|
||||
|
||||
# Dividers between sections (vertical light lines)
|
||||
div1 = (section1_right + section2_left) / 2
|
||||
div2 = (section2_right + section3_left) / 2
|
||||
for d in (div1, div2):
|
||||
ax.axvline(d, color="#aaa", linewidth=0.7, alpha=0.65, linestyle="-")
|
||||
|
||||
# Section labels just above each block
|
||||
y_band = 1.02
|
||||
section_centers = [
|
||||
((pos[0] + section1_right) / 2, "Basic backbones (img-only, single)"),
|
||||
((section2_left + section2_right) / 2, "ResNet50 variations"),
|
||||
((section3_left + section3_right) / 2, "Baseline"),
|
||||
((pos[0] + section1_right) / 2, "Basic backbones (img-only, single)"),
|
||||
((section2_left + section2_right) / 2, "ResNet50 variations"),
|
||||
((section3_left + section3_right) / 2, "Baseline"),
|
||||
]
|
||||
for cx, txt in section_centers:
|
||||
ax.text(cx, y_band, txt, ha="center", va="bottom",
|
||||
fontsize=10, color="#333", fontweight="bold",
|
||||
transform=ax.get_xaxis_transform())
|
||||
|
||||
# X-tick labels (with p-values vs baseline beneath each variant box)
|
||||
tick_labels = []
|
||||
for lbl, aucs, color in zip(all_labels, all_aucs, all_colors):
|
||||
if color == C_BASE or not len(aucs) or not len(base_aucs):
|
||||
|
||||
@@ -58,9 +58,9 @@ SEV_COLORS = {
|
||||
"unknown": C_UNKNOWN,
|
||||
}
|
||||
|
||||
SEV_ORDER = ["normal", "unknown", "early", "moderate", "severe"]
|
||||
SEV_ALPHA = {"normal": 0.40, "unknown": 0.35, "early": 0.55, "moderate": 0.70, "severe": 0.85}
|
||||
SEV_SIZE = {"normal": 6, "unknown": 6, "early": 8, "moderate": 10, "severe": 12}
|
||||
SEV_ORDER = ["severe", "moderate", "unknown", "early", "normal"]
|
||||
SEV_ALPHA = {"normal": 0.55, "unknown": 0.55, "early": 0.55, "moderate": 0.55, "severe": 0.55}
|
||||
SEV_SIZE = {"normal": 8, "unknown": 8, "early": 8, "moderate": 8, "severe": 8}
|
||||
|
||||
# ── Per-panel definitions: (label, results dir, eval_stage) ──────────────────
|
||||
# Top row: single-modality reference runs
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""F3b - Hadamard L1 fusion ROC: patient-grouped vs anonymous CV (overlay).
|
||||
|
||||
Single panel overlaying the ROC of the same single-eye img+cd Hadamard L1
|
||||
fusion configuration evaluated under patient-grouped 5-fold CV (the headline
|
||||
protocol) and patient-anonymous 5-fold CV (the prevailing benchmark protocol).
|
||||
Both runs use matched seeds, matched backbones, matched training schedules;
|
||||
only the fold-grouping rule changes.
|
||||
|
||||
For each configuration the figure shows:
|
||||
- per-fold-rep ROC curves as faint coloured lines (50 curves per condition)
|
||||
- mean ROC across fold-reps with a shaded SD band
|
||||
- the rep-mean test AUC ± SD in a corner annotation
|
||||
|
||||
Re-run:
|
||||
python -m v4.figures.F3b_anonymous_cv_roc
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from sklearn.metrics import roc_auc_score, roc_curve
|
||||
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "F3b_anonymous_cv_roc.png"
|
||||
|
||||
CONDITIONS = [
|
||||
("Patient-grouped CV",
|
||||
RESULTS_ROOT / "refuge_v2m_baseline" / "ensemble_single_refugelike",
|
||||
"nt", "#1f6fb0"),
|
||||
("Anonymous CV",
|
||||
RESULTS_ROOT / "backbone_replication" / "anonymous_cv_ensemble_single_refugelike",
|
||||
"nt", "#c44e52"),
|
||||
]
|
||||
|
||||
# Common FPR grid for per-fold-rep ROC interpolation
|
||||
FPR_GRID = np.linspace(0.0, 1.0, 201)
|
||||
|
||||
|
||||
def collect_per_foldrep(run_dir: Path, eval_stage: str):
|
||||
"""Return list of (y_true, y_score) tuples, one per (rep, fold)."""
|
||||
per: list[tuple[np.ndarray, np.ndarray]] = []
|
||||
for rep in sorted(run_dir.glob("rep*")):
|
||||
fp = next(iter(rep.rglob("predictions.h5")), None)
|
||||
if fp is None:
|
||||
continue
|
||||
with h5py.File(fp, "r") as f:
|
||||
if eval_stage not in f:
|
||||
continue
|
||||
grp = f[eval_stage]
|
||||
logits = grp["logits"][:]
|
||||
y_true = grp["y_true"][:].astype(int)
|
||||
split = grp["split"][:]
|
||||
n_folds, n_epochs, _, n_heads, n_outputs = logits.shape
|
||||
if n_outputs != 2:
|
||||
continue
|
||||
ep, head = n_epochs - 1, n_heads - 1
|
||||
for fold in range(n_folds):
|
||||
labels = np.array(
|
||||
[s.decode() if isinstance(s, bytes) else str(s) for s in split[fold]]
|
||||
)
|
||||
test_mask = labels == "test"
|
||||
if not test_mask.any():
|
||||
continue
|
||||
lg = logits[fold, ep, test_mask, head, :]
|
||||
e = np.exp(lg - lg.max(axis=1, keepdims=True))
|
||||
p = e / e.sum(axis=1, keepdims=True)
|
||||
y = y_true[test_mask]
|
||||
s = p[:, 1]
|
||||
per.append((y, s))
|
||||
return per
|
||||
|
||||
|
||||
def interp_tpr(y: np.ndarray, s: np.ndarray) -> np.ndarray:
|
||||
if len(np.unique(y)) < 2:
|
||||
return np.full_like(FPR_GRID, np.nan, dtype=float)
|
||||
fpr, tpr, _ = roc_curve(y, s)
|
||||
return np.interp(FPR_GRID, fpr, tpr)
|
||||
|
||||
|
||||
def render() -> None:
|
||||
fig, ax = plt.subplots(figsize=(7.4, 6.4))
|
||||
|
||||
# Diagonal reference first so it sits behind everything
|
||||
ax.plot([0, 1], [0, 1], color="#aaa", linewidth=0.8, linestyle=":", zorder=1)
|
||||
|
||||
annotations = []
|
||||
|
||||
for label, run_dir, stage, color in CONDITIONS:
|
||||
per = collect_per_foldrep(run_dir, stage)
|
||||
if not per:
|
||||
continue
|
||||
|
||||
tprs = np.array([interp_tpr(y, s) for (y, s) in per])
|
||||
valid = ~np.isnan(tprs).any(axis=1)
|
||||
tprs = tprs[valid]
|
||||
|
||||
per_aucs = np.array(
|
||||
[roc_auc_score(y, s) for (y, s) in per if len(np.unique(y)) >= 2]
|
||||
)
|
||||
rep_means = (per_aucs.reshape(-1, 5).mean(axis=1)
|
||||
if len(per_aucs) % 5 == 0 else per_aucs)
|
||||
|
||||
# Per-fold-rep curves
|
||||
for (y, s) in per:
|
||||
if len(np.unique(y)) < 2:
|
||||
continue
|
||||
fpr, tpr, _ = roc_curve(y, s)
|
||||
ax.plot(fpr, tpr, color=color, linewidth=0.4, alpha=0.13, zorder=2)
|
||||
|
||||
# Mean ± SD band
|
||||
mean_tpr = tprs.mean(axis=0)
|
||||
sd_tpr = tprs.std(axis=0)
|
||||
ax.fill_between(
|
||||
FPR_GRID, np.clip(mean_tpr - sd_tpr, 0, 1),
|
||||
np.clip(mean_tpr + sd_tpr, 0, 1),
|
||||
color=color, alpha=0.20, zorder=3,
|
||||
)
|
||||
ax.plot(
|
||||
FPR_GRID, mean_tpr, color=color, linewidth=2.2, zorder=4,
|
||||
label=f"{label} (AUC = {rep_means.mean():.3f} ± {rep_means.std():.3f})",
|
||||
)
|
||||
|
||||
annotations.append((label, rep_means))
|
||||
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_xlabel("False Positive Rate", fontsize=11)
|
||||
ax.set_ylabel("True Positive Rate", fontsize=11)
|
||||
ax.set_title(
|
||||
"Hadamard L1 fusion ROC under matched architecture,\n"
|
||||
"patient-grouped vs anonymous cross-validation",
|
||||
fontsize=12, fontweight="bold",
|
||||
)
|
||||
ax.grid(alpha=0.25, linestyle="--")
|
||||
ax.legend(loc="lower right", fontsize=10, framealpha=0.94)
|
||||
|
||||
fig.tight_layout()
|
||||
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()
|
||||
@@ -58,15 +58,15 @@ SEV_LABELS = {
|
||||
"severe": "Glaucoma — severe (VF_MD < −12)",
|
||||
"unknown": "Glaucoma — VF_MD not recorded",
|
||||
}
|
||||
SEV_ORDER = ["normal", "unknown", "early", "moderate", "severe"]
|
||||
SEV_ORDER = ["severe", "moderate", "unknown", "early", "normal"]
|
||||
SEV_ALPHA = {
|
||||
"normal": 0.40,
|
||||
"unknown": 0.35,
|
||||
"normal": 0.55,
|
||||
"unknown": 0.55,
|
||||
"early": 0.55,
|
||||
"moderate": 0.70,
|
||||
"severe": 0.85,
|
||||
"moderate": 0.55,
|
||||
"severe": 0.55,
|
||||
}
|
||||
SEV_SIZE = {"normal": 6, "unknown": 6, "early": 8, "moderate": 10, "severe": 12}
|
||||
SEV_SIZE = {"normal": 8, "unknown": 8, "early": 8, "moderate": 8, "severe": 8}
|
||||
|
||||
# Panel grid: [row][col] = (label, run_dir, eval_stage)
|
||||
GRID = [
|
||||
|
||||
@@ -27,7 +27,10 @@ from sklearn.metrics import roc_curve, roc_auc_score
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "F6_regression.png"
|
||||
RUN_DIR = RESULTS_ROOT / "reg_head" / "baseline_reg_nt50"
|
||||
# Points at the post-fix run that stores VF_MD as float64. The earlier
|
||||
# baseline_reg_nt50 run stored y_true as int64, silently rounding the
|
||||
# regression targets; do not mix the two.
|
||||
RUN_DIR = RESULTS_ROOT / "reg_head" / "baseline_reg_nt50_floaty"
|
||||
|
||||
# Prediction-side bin boundaries
|
||||
NP_THRESH = -1.097 # mean of measured-healthy MD
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""F8 combined explainability panel: disc-centred attention + quadrant breakdown.
|
||||
|
||||
Composes a single A/B figure from two existing renderings:
|
||||
A: ``F8_gradcam/disc_attention_detail.png`` — 2x2 grid of mean Grad-CAM
|
||||
heatmaps for {correct, incorrect} x {Normal, Glaucoma} cells, with the
|
||||
mean disc boundary annotated as a dashed circle.
|
||||
B: ``F8_quadrant_attention.png`` — grouped-bar chart of mean full-image
|
||||
Grad-CAM fraction per optic-disc quadrant, by cell.
|
||||
|
||||
Both source panels are produced by ``v4.figures.F8_explainability`` and
|
||||
``v4.figures.F8_quadrant_plot`` respectively; this script just stitches the
|
||||
two PNGs into a single combined figure with A/B subfigure labels.
|
||||
|
||||
Re-run:
|
||||
python -m v4.figures.F8_attention_combined
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from PIL import Image
|
||||
|
||||
|
||||
SRC_A = Path(__file__).parent / "output" / "F8_gradcam" / "disc_attention_detail.png"
|
||||
SRC_B = Path(__file__).parent / "output" / "F8_quadrant_attention.png"
|
||||
OUT = Path(__file__).parent / "output" / "F8_attention_combined.png"
|
||||
|
||||
|
||||
def render() -> None:
|
||||
for p in (SRC_A, SRC_B):
|
||||
if not p.exists():
|
||||
raise SystemExit(
|
||||
f"Source panel missing: {p}\n"
|
||||
"Run F8_explainability (for A) and F8_quadrant_plot (for B) first."
|
||||
)
|
||||
|
||||
img_a = Image.open(SRC_A)
|
||||
img_b = Image.open(SRC_B)
|
||||
|
||||
# Stack vertically: A on top (square), B below (wider).
|
||||
fig = plt.figure(figsize=(13.0, 13.6))
|
||||
gs = fig.add_gridspec(
|
||||
2, 1,
|
||||
height_ratios=[img_a.size[1] / img_a.size[0],
|
||||
img_b.size[1] / img_b.size[0] * 13.0 / 13.0],
|
||||
hspace=0.06,
|
||||
)
|
||||
|
||||
ax_a = fig.add_subplot(gs[0])
|
||||
ax_a.imshow(img_a)
|
||||
ax_a.axis("off")
|
||||
ax_a.text(-0.01, 1.01, "A", transform=ax_a.transAxes,
|
||||
ha="left", va="bottom", fontsize=22, fontweight="bold")
|
||||
|
||||
ax_b = fig.add_subplot(gs[1])
|
||||
ax_b.imshow(img_b)
|
||||
ax_b.axis("off")
|
||||
ax_b.text(-0.01, 1.01, "B", transform=ax_b.transAxes,
|
||||
ha="left", va="bottom", fontsize=22, fontweight="bold")
|
||||
|
||||
fig.tight_layout()
|
||||
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()
|
||||
@@ -1,8 +1,10 @@
|
||||
"""F8 - Explainability figures from the V2-M checkpointed v4 run.
|
||||
"""F8 - Explainability figures from the R50 checkpointed v4 run.
|
||||
|
||||
Sources predictions and Grad-CAM panels exclusively from the
|
||||
``experiments/explainability/ensemble_v2m_ckpt`` run (img+cd ensemble with the
|
||||
refuge_efficientnet_v2_m backbone, save_checkpoints=true).
|
||||
``experiments/explainability/ensemble_refugelike_ckpt`` run (img+cd ensemble
|
||||
with the refugelike R50 backbone, save_checkpoints=true). rep00 (seed=1234)
|
||||
is the single rep used for the figure; matches the headline configuration in
|
||||
section 3.
|
||||
|
||||
GradCAM machinery lives in ``v4.classes.accessory.explainability``; PAPILA
|
||||
specific knowledge (disc contour rasterisation, OS→OD orientation flip) is
|
||||
@@ -33,7 +35,7 @@ from v4.figures.util.loaders import REPO_ROOT
|
||||
|
||||
OUT_DIR = Path(__file__).parent / "output"
|
||||
GRADCAM_DIR = OUT_DIR / "F8_gradcam"
|
||||
V4_CKPT_RUN = REPO_ROOT / "v4" / "results" / "experiments" / "explainability" / "ensemble_v2m_ckpt" / "binary"
|
||||
V4_CKPT_RUN = REPO_ROOT / "v4" / "results" / "experiments" / "explainability" / "ensemble_refugelike_ckpt" / "rep00" / "binary"
|
||||
|
||||
LABEL_NAMES = {0: "Normal", 1: "Glaucoma"}
|
||||
EVENT_ORDER = [
|
||||
@@ -43,11 +45,11 @@ EVENT_ORDER = [
|
||||
]
|
||||
EVENT_LABELS = {
|
||||
"full_correction": "Both wrong -> fused right",
|
||||
"img_assist": "Image right, MD wrong",
|
||||
"md_assist": "MD right, image wrong",
|
||||
"img_assist": "Image right, clinical wrong",
|
||||
"md_assist": "Clinical right, image wrong",
|
||||
"full_error": "Both right -> fused wrong",
|
||||
"img_drag": "MD right, image wrong -> fused wrong",
|
||||
"md_drag": "Image right, MD wrong -> fused wrong",
|
||||
"img_drag": "Clinical right, image wrong -> fused wrong",
|
||||
"md_drag": "Image right, clinical wrong -> fused wrong",
|
||||
"concordant_correct": "All correct",
|
||||
"concordant_wrong": "All wrong",
|
||||
}
|
||||
@@ -339,6 +341,8 @@ def make_fusion_event_panel(split: str = "test") -> None:
|
||||
ax.invert_yaxis()
|
||||
ax.set_xlabel("Count")
|
||||
ax.set_title("Fusion event taxonomy", fontsize=10, fontweight="bold")
|
||||
max_count = max(display_counts[k] for k in bars) if bars else 0
|
||||
ax.set_xlim(0, max_count * 1.10 + 1)
|
||||
for yi, k in enumerate(bars):
|
||||
ax.text(display_counts[k] + 0.8, yi, str(int(display_counts[k])),
|
||||
va="center", fontsize=8)
|
||||
@@ -346,7 +350,7 @@ def make_fusion_event_panel(split: str = "test") -> None:
|
||||
ax = fig.add_subplot(gs[0, 1])
|
||||
per_fold = pd.DataFrame(
|
||||
{
|
||||
"fold": [f"{r}/{f}" for (r, f), _ in fold_groups],
|
||||
"fold": [str(f) for (_, f), _ in fold_groups],
|
||||
"positive": [sum((g["event_type"] == k).sum() for k in positive_keys) for _, g in fold_groups],
|
||||
"negative": [sum((g["event_type"] == k).sum() for k in negative_keys) for _, g in fold_groups],
|
||||
}
|
||||
@@ -437,10 +441,6 @@ def make_fusion_event_panel(split: str = "test") -> None:
|
||||
ax.legend(handles=point_handles + shade_handles, ncol=5, fontsize=7,
|
||||
loc="upper center", bbox_to_anchor=(0.5, -0.14), frameon=False)
|
||||
|
||||
fig.suptitle(
|
||||
f"S8a - Checkpoint Fusion Events ({split}; AUC={auc:.3f}, n={len(df)})",
|
||||
fontsize=12, fontweight="bold",
|
||||
)
|
||||
out = OUT_DIR / "S8a_comparison_panel.png"
|
||||
fig.savefig(out, dpi=180, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
@@ -450,7 +450,7 @@ def make_fusion_event_panel(split: str = "test") -> None:
|
||||
def make_clinical_importance(n_permutations: int = 30, seed: int = 0) -> None:
|
||||
"""S8e clinical permutation importance via the cd-tower → cd_aux head.
|
||||
|
||||
Isolates the clinical-only prediction path at the V2-M ckpt run, then
|
||||
Isolates the clinical-only prediction path at the R50 ckpt run, then
|
||||
column-shuffles the encoded clinical vector to measure per-feature AUC
|
||||
drop. Per-original-column grouping comes from
|
||||
``ClinicalDataView.feature_groups`` (one-hot encoded dims for a
|
||||
@@ -590,7 +590,7 @@ def make_clinical_importance(n_permutations: int = 30, seed: int = 0) -> None:
|
||||
ax.set_xlabel("Mean AUC drop on shuffling (averaged across folds)", fontsize=10)
|
||||
ax.axvline(0, color="black", linewidth=0.7)
|
||||
ax.set_title(
|
||||
f"S8e — Clinical permutation importance (cd-only head, V2-M ckpt run)\n"
|
||||
f"S8e — Clinical permutation importance (cd-only head, R50 ckpt run)\n"
|
||||
f"baseline AUC = {baseline_mean:.3f}; n_permutations = {n_permutations}",
|
||||
fontsize=10, fontweight="bold",
|
||||
)
|
||||
@@ -692,6 +692,60 @@ def _disc_centred_patch_array(
|
||||
return patch_out.astype(np.float32), out * disc_r / (2 * half)
|
||||
|
||||
|
||||
QUAD_ORDER = ("ST", "SN", "IT", "IN") # superotemporal, superonasal, inferotemporal, inferonasal
|
||||
|
||||
|
||||
def _quadrant_fractions(
|
||||
cam: np.ndarray,
|
||||
disc_mask: np.ndarray,
|
||||
*,
|
||||
peri_inner: float = 1.0,
|
||||
peri_outer: float = 2.0,
|
||||
) -> tuple[dict[str, float], dict[str, float], dict[str, float]] | tuple[None, None, None]:
|
||||
"""Per-quadrant Grad-CAM fractions in OD-oriented coordinates, for three
|
||||
region scopes.
|
||||
|
||||
Quadrant boundaries are the disc-mask centroid (cx, cy). In the OD-oriented
|
||||
frame nasal is left (x < cx) and temporal is right (x > cx); superior is
|
||||
top (y < cy) and inferior is bottom (y > cy):
|
||||
|
||||
ST = x > cx, y < cy
|
||||
SN = x < cx, y < cy
|
||||
IT = x > cx, y > cy
|
||||
IN = x < cx, y > cy
|
||||
|
||||
Three region scopes are returned:
|
||||
disc_q : fractions of CAM intensity that fall inside the GT disc mask
|
||||
peri_q : fractions inside a peri-disc annulus of disc-radius units
|
||||
(peri_inner to peri_outer, default 1x-2x), excluding the disc
|
||||
full_q : fractions over the entire image
|
||||
Each dict sums to 1 (within floating-point error). Returns (None, None, None)
|
||||
if the disc mask is empty.
|
||||
"""
|
||||
if disc_mask is None or disc_mask.sum() == 0:
|
||||
return None, None, None
|
||||
ys, xs = np.where(disc_mask)
|
||||
cy = float(ys.mean()); cx = float(xs.mean())
|
||||
disc_r = float(np.sqrt(disc_mask.sum() / np.pi))
|
||||
h, w = cam.shape
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
dist = np.sqrt((xx - cx) ** 2 + (yy - cy) ** 2)
|
||||
peri_mask = (dist >= peri_inner * disc_r) & (dist <= peri_outer * disc_r) & ~disc_mask
|
||||
quads = {
|
||||
"ST": (xx > cx) & (yy < cy),
|
||||
"SN": (xx < cx) & (yy < cy),
|
||||
"IT": (xx > cx) & (yy > cy),
|
||||
"IN": (xx < cx) & (yy > cy),
|
||||
}
|
||||
disc_total = float(cam[disc_mask].sum()) + 1e-8
|
||||
peri_total = float(cam[peri_mask].sum()) + 1e-8
|
||||
full_total = float(cam.sum()) + 1e-8
|
||||
disc_q = {k: float(cam[disc_mask & q].sum()) / disc_total for k, q in quads.items()}
|
||||
peri_q = {k: float(cam[peri_mask & q].sum()) / peri_total for k, q in quads.items()}
|
||||
full_q = {k: float(cam[q].sum()) / full_total for k, q in quads.items()}
|
||||
return disc_q, peri_q, full_q
|
||||
|
||||
|
||||
def _annotate_nasal_temporal(ax, *, fontsize: int = 9, color: str = "white",
|
||||
pad: float = 2.5) -> None:
|
||||
"""Label the disc-side (nasal) and macula-side (temporal) edges of an
|
||||
@@ -868,6 +922,10 @@ def _make_oriented_gradcam(n_grid: int = 16, alpha: float = 0.45,
|
||||
disc_patch_count: dict[tuple[str, str], int] = {}
|
||||
disc_radius_sum: dict[tuple[str, str], float] = {}
|
||||
disc_frac_sum: dict[tuple[str, str], float] = {}
|
||||
# Per-eye quadrant fractions for three region scopes; aggregated per cell.
|
||||
quad_disc_list: dict[tuple[str, str], list[dict[str, float]]] = {}
|
||||
quad_peri_list: dict[tuple[str, str], list[dict[str, float]]] = {}
|
||||
quad_full_list: dict[tuple[str, str], list[dict[str, float]]] = {}
|
||||
examples: dict[tuple[str, str], tuple[np.ndarray, int, str, int]] = {}
|
||||
|
||||
fold_range = range(cfg.get("folds", 5))
|
||||
@@ -927,6 +985,11 @@ def _make_oriented_gradcam(n_grid: int = 16, alpha: float = 0.45,
|
||||
disc_frac_sum[key] = disc_frac_sum.get(key, 0.0) + float(
|
||||
cam_np[roi_mask].sum() / (cam_np.sum() + 1e-8)
|
||||
)
|
||||
disc_q, peri_q, full_q = _quadrant_fractions(cam_np, roi_mask)
|
||||
if disc_q is not None:
|
||||
quad_disc_list.setdefault(key, []).append(disc_q)
|
||||
quad_peri_list.setdefault(key, []).append(peri_q)
|
||||
quad_full_list.setdefault(key, []).append(full_q)
|
||||
if key not in examples:
|
||||
ov = overlay_gradcam(pil_oriented, cam_np, alpha)
|
||||
ov_small = np.array(ov.resize(cam_np.shape[::-1], Image.BILINEAR))
|
||||
@@ -1020,6 +1083,32 @@ def _make_oriented_gradcam(n_grid: int = 16, alpha: float = 0.45,
|
||||
if mean_patches:
|
||||
_make_oriented_disc_detail(mean_patches, examples, GRADCAM_DIR / "disc_attention_detail.png")
|
||||
|
||||
# Per-quadrant CAM fractions (within-disc and full-image scopes).
|
||||
# One CSV row per (class, outcome, scope, quadrant) cell, with mean and SD
|
||||
# computed over the per-eye fractions in that cell.
|
||||
rows = []
|
||||
for key in sorted(quad_disc_list.keys()):
|
||||
cls_name, outcome = key
|
||||
n_eyes = len(quad_disc_list[key])
|
||||
for scope_label, scope_list in (("disc", quad_disc_list[key]),
|
||||
("peri", quad_peri_list[key]),
|
||||
("full", quad_full_list[key])):
|
||||
for q in QUAD_ORDER:
|
||||
vals = np.array([d[q] for d in scope_list], dtype=np.float64)
|
||||
rows.append({
|
||||
"class": cls_name,
|
||||
"outcome": outcome,
|
||||
"scope": scope_label,
|
||||
"quadrant": q,
|
||||
"n_eyes": n_eyes,
|
||||
"mean": float(vals.mean()),
|
||||
"sd": float(vals.std(ddof=1)) if n_eyes > 1 else float("nan"),
|
||||
})
|
||||
if rows:
|
||||
out_csv = OUT_DIR / "F8_quadrant_fractions.csv"
|
||||
pd.DataFrame(rows).to_csv(out_csv, index=False)
|
||||
print(f"saved quadrant fractions: {out_csv}")
|
||||
|
||||
|
||||
def make_oriented_gradcam(n_grid: int = 16, alpha: float = 0.45,
|
||||
target_class: int | None = None) -> None:
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""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.set_title(
|
||||
"Image-tower Grad-CAM by optic-disc quadrant (OD-oriented; centroid-split)",
|
||||
fontsize=12.5, fontweight="bold",
|
||||
)
|
||||
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()
|
||||
@@ -0,0 +1,277 @@
|
||||
"""S2 - Variance decomposition of the four-bridge L1 fusion comparison.
|
||||
|
||||
Two-panel supplementary figure summarising the variance decomposition
|
||||
reported alongside section 3.2 of the manuscript.
|
||||
|
||||
Panel A (left): paired-line plot.
|
||||
x-axis : the 4 bridge variants (Concat, Pairwise, Gated, Hadamard)
|
||||
y-axis : eye-level test AUC
|
||||
each line : one fold-rep, connecting that fold-rep's 4 bridge AUCs
|
||||
overlay : per-bridge boxplot showing the marginal AUC distribution
|
||||
annotation : pooled across-architecture and across-fold-rep SDs,
|
||||
and the SD ratio
|
||||
|
||||
Panel B (right): centered-offset KDEs.
|
||||
x-axis : AUC offset from grouping mean (centered at 0)
|
||||
y-axis : density
|
||||
four coloured curves : per-bridge fold-rep distributions (50 fold-reps
|
||||
per bridge, centered by subtracting each bridge's
|
||||
own mean)
|
||||
dashed dark curve : architectural offset distribution (200 values,
|
||||
centered by subtracting each fold-rep's mean)
|
||||
annotation : variance ratio
|
||||
|
||||
Read directly from v4/results/experiments/{phase3_v4,refuge_v2m_baseline}.
|
||||
|
||||
Re-run:
|
||||
python -m v4.figures.S2_variance_decomposition
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import gaussian_kde
|
||||
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "S2_variance_decomposition.png"
|
||||
|
||||
|
||||
# Bridge label, results dir relative to experiments/, and stage key
|
||||
BRIDGES = [
|
||||
("Concat", "phase3_v4/single_bcd_concat"),
|
||||
("Pairwise", "phase3_v4/single_bcd_pairwise"),
|
||||
("Gated", "phase3_v4/single_bcd_gated"),
|
||||
("Hadamard", "refuge_v2m_baseline/ensemble_single_refugelike"),
|
||||
]
|
||||
STAGE_KEY = "nt_test_auc"
|
||||
|
||||
|
||||
# Panel-A colour palette (paired lines + boxplots)
|
||||
C_LINE = "#1f6fb0" # single blue for all paired-cell lines
|
||||
C_LINE_ALPHA = 0.22
|
||||
C_MARKER = "#1f6fb0"
|
||||
C_MEDIAN = "#c44e52" # red box median line
|
||||
C_BOX_FILL = "#dbe6f0" # pale blue box fill
|
||||
|
||||
# Panel-B colour palette (per-bridge KDEs)
|
||||
C_ARCH = "#222" # dark grey for the architectural curve
|
||||
BRIDGE_COLORS = {
|
||||
"Concat": "#7f7f7f",
|
||||
"Pairwise": "#ff7f0e",
|
||||
"Gated": "#2ca02c",
|
||||
"Hadamard": "#1f77b4",
|
||||
}
|
||||
|
||||
|
||||
# ── Data loading + decomposition ────────────────────────────────────────────
|
||||
|
||||
def collect() -> pd.DataFrame:
|
||||
rows = []
|
||||
for label, rel in BRIDGES:
|
||||
root = RESULTS_ROOT / rel
|
||||
for s in sorted(root.glob("rep*/binary/summary.json")):
|
||||
rep = int(s.parents[1].name.replace("rep", ""))
|
||||
d = json.loads(s.read_text())
|
||||
for fr in d.get("fold_results", []):
|
||||
v = fr.get(STAGE_KEY)
|
||||
if v is None or not np.isfinite(v):
|
||||
continue
|
||||
rows.append({
|
||||
"bridge": label,
|
||||
"rep": rep,
|
||||
"fold": int(fr["fold"]),
|
||||
"auc": float(v),
|
||||
})
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def decompose(df: pd.DataFrame) -> tuple[float, float, float, dict[str, float]]:
|
||||
"""Return (arch_sd_pooled, fold_sd_pooled, ratio, per_bridge_sd)."""
|
||||
cell_var = df.groupby(["rep", "fold"])["auc"].var(ddof=1)
|
||||
bridge_var = df.groupby("bridge")["auc"].var(ddof=1)
|
||||
arch_sd = float(np.sqrt(cell_var.mean()))
|
||||
fold_sd = float(np.sqrt(bridge_var.mean()))
|
||||
ratio = fold_sd / arch_sd if arch_sd > 0 else float("inf")
|
||||
per_bridge_sd = {b: float(np.sqrt(v)) for b, v in bridge_var.items()}
|
||||
return arch_sd, fold_sd, ratio, per_bridge_sd
|
||||
|
||||
|
||||
# ── Panel A: paired-line plot ───────────────────────────────────────────────
|
||||
|
||||
def draw_panel_a(ax, df: pd.DataFrame,
|
||||
arch_sd: float, fold_sd: float, ratio: float,
|
||||
n_cells: int) -> None:
|
||||
bridge_order = [b for b, _ in BRIDGES]
|
||||
pivot = df.pivot_table(
|
||||
index=["rep", "fold"], columns="bridge", values="auc"
|
||||
)[bridge_order]
|
||||
x_positions = np.arange(len(bridge_order), dtype=float)
|
||||
|
||||
# Paired cell lines: one per fold-rep
|
||||
for _, row in pivot.iterrows():
|
||||
if row.isna().any():
|
||||
continue
|
||||
ax.plot(
|
||||
x_positions, row.values, color=C_LINE, alpha=C_LINE_ALPHA,
|
||||
linewidth=0.9, marker="o", markersize=2.0,
|
||||
markerfacecolor=C_MARKER, markeredgecolor="none", zorder=2,
|
||||
)
|
||||
|
||||
# Per-bridge boxplot
|
||||
box_data = [pivot[b].dropna().values for b in bridge_order]
|
||||
ax.boxplot(
|
||||
box_data,
|
||||
positions=x_positions,
|
||||
widths=0.32,
|
||||
patch_artist=True,
|
||||
manage_ticks=False,
|
||||
zorder=3,
|
||||
boxprops=dict(facecolor=C_BOX_FILL, edgecolor="black",
|
||||
linewidth=1.0, alpha=0.85),
|
||||
whiskerprops=dict(color="black", linewidth=0.9),
|
||||
capprops=dict(color="black", linewidth=0.9),
|
||||
medianprops=dict(color=C_MEDIAN, linewidth=1.8),
|
||||
flierprops=dict(marker="", markersize=0),
|
||||
)
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels(bridge_order, fontsize=11)
|
||||
ax.set_xlabel("L1 fusion bridge", fontsize=11)
|
||||
ax.set_ylabel("Eye-level test AUC", fontsize=11)
|
||||
ax.set_xlim(-0.5, len(bridge_order) - 0.5)
|
||||
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
||||
|
||||
txt = (
|
||||
f"n = {n_cells} fold-rep AUC values | variance ratio {ratio:.2f}\n"
|
||||
f" across-architecture SD = {arch_sd:.3f}\n"
|
||||
f" across-fold-rep SD = {fold_sd:.3f}"
|
||||
)
|
||||
ax.text(
|
||||
0.985, 0.025, txt, transform=ax.transAxes,
|
||||
ha="right", va="bottom", fontsize=9.5, family="monospace",
|
||||
bbox=dict(boxstyle="round,pad=0.5", facecolor="white",
|
||||
edgecolor="#888", alpha=0.92),
|
||||
)
|
||||
|
||||
|
||||
# ── Panel B: centered-offset KDE curves ─────────────────────────────────────
|
||||
|
||||
def draw_panel_b(ax, df: pd.DataFrame,
|
||||
arch_sd: float, fold_sd: float, ratio: float,
|
||||
per_bridge_sd: dict[str, float]) -> None:
|
||||
df = df.copy()
|
||||
fold_rep_mean = df.groupby(["rep", "fold"])["auc"].transform("mean")
|
||||
bridge_mean = df.groupby("bridge")["auc"].transform("mean")
|
||||
df["arch_offset"] = df["auc"] - fold_rep_mean
|
||||
df["fold_offset"] = df["auc"] - bridge_mean
|
||||
|
||||
all_offsets = np.concatenate(
|
||||
[df["arch_offset"].values, df["fold_offset"].values]
|
||||
)
|
||||
x_max = float(np.abs(all_offsets).max()) * 1.10
|
||||
xs = np.linspace(-x_max, x_max, 600)
|
||||
|
||||
# Per-bridge fold-rep offset curves
|
||||
for b in [name for name, _ in BRIDGES]:
|
||||
offs = df.loc[df["bridge"] == b, "fold_offset"].values
|
||||
kde = gaussian_kde(offs)
|
||||
y = kde(xs)
|
||||
sd = per_bridge_sd[b]
|
||||
ax.plot(xs, y, color=BRIDGE_COLORS[b], linewidth=1.6, alpha=0.92,
|
||||
zorder=3,
|
||||
label=f"{b} SD = {sd:.3f}")
|
||||
|
||||
# Architectural offset curve (pooled)
|
||||
arch_offsets = df["arch_offset"].values
|
||||
kde_arch = gaussian_kde(arch_offsets)
|
||||
y_arch = kde_arch(xs)
|
||||
ax.fill_between(xs, y_arch, color=C_ARCH, alpha=0.18, zorder=2)
|
||||
ax.plot(xs, y_arch, color=C_ARCH, linewidth=2.2, linestyle="--", zorder=4,
|
||||
label=f"Architectural SD = {arch_sd:.3f}")
|
||||
|
||||
ax.axvline(0, color="#666", linewidth=0.8, linestyle=":",
|
||||
alpha=0.6, zorder=0)
|
||||
ax.set_xlabel("AUC offset from grouping mean", fontsize=11)
|
||||
ax.set_ylabel("Probability density", fontsize=11)
|
||||
ax.set_xlim(-x_max, x_max)
|
||||
ax.set_yticklabels([])
|
||||
ax.tick_params(axis="y", which="both", left=True, labelleft=False)
|
||||
ax.grid(axis="y", alpha=0.25, linestyle="--")
|
||||
|
||||
# Headroom on the y-axis so the variance-ratio box does not crowd the
|
||||
# architectural-curve peak.
|
||||
ymin, ymax = ax.get_ylim()
|
||||
ax.set_ylim(0, ymax * 1.10)
|
||||
|
||||
# Legend below the top so it clears the variance-ratio annotation.
|
||||
ax.legend(
|
||||
loc="upper left", bbox_to_anchor=(0.0, 0.82),
|
||||
fontsize=9, framealpha=0.92,
|
||||
)
|
||||
|
||||
txt = f"variance ratio fold-SD / arch-SD = {ratio:.2f}"
|
||||
ax.text(
|
||||
0.985, 0.975, txt, transform=ax.transAxes,
|
||||
ha="right", va="top", fontsize=10.5, family="monospace",
|
||||
bbox=dict(boxstyle="round,pad=0.45", facecolor="white",
|
||||
edgecolor="#888", alpha=0.92),
|
||||
)
|
||||
|
||||
|
||||
# ── Combined render ─────────────────────────────────────────────────────────
|
||||
|
||||
def render() -> None:
|
||||
df = collect()
|
||||
if df.empty:
|
||||
print("No data collected; check the source paths.")
|
||||
return
|
||||
|
||||
n_cells = df.groupby(["rep", "fold"]).ngroups
|
||||
n_bridges = df["bridge"].nunique()
|
||||
arch_sd, fold_sd, ratio, per_bridge_sd = decompose(df)
|
||||
|
||||
print(f"Collected {len(df)} observations ({n_bridges} bridges x {n_cells} cells)")
|
||||
print(f" across-architecture SD (within cell) : {arch_sd:.4f}")
|
||||
print(f" across-fold-rep SD (within bridge) : {fold_sd:.4f}")
|
||||
print(f" ratio fold-SD / arch-SD : {ratio:.2f}x")
|
||||
print("Per-bridge fold-rep SDs:")
|
||||
for b in [name for name, _ in BRIDGES]:
|
||||
print(f" {b:<10s} SD = {per_bridge_sd[b]:.4f}")
|
||||
|
||||
fig, (axA, axB) = plt.subplots(
|
||||
nrows=1, ncols=2, figsize=(16.0, 6.0),
|
||||
gridspec_kw=dict(wspace=0.22),
|
||||
)
|
||||
|
||||
draw_panel_a(axA, df, arch_sd, fold_sd, ratio, n_cells)
|
||||
draw_panel_b(axB, df, arch_sd, fold_sd, ratio, per_bridge_sd)
|
||||
|
||||
# Subfigure labels
|
||||
for ax, label in ((axA, "A"), (axB, "B")):
|
||||
ax.text(
|
||||
-0.07, 1.03, label, transform=ax.transAxes,
|
||||
ha="left", va="bottom", fontsize=15, fontweight="bold",
|
||||
)
|
||||
|
||||
fig.suptitle(
|
||||
"Variance decomposition: fold-assignment noise vs L1 bridge choice",
|
||||
fontsize=13.0, fontweight="bold", y=1.00,
|
||||
)
|
||||
fig.tight_layout(rect=(0, 0, 1, 0.97))
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,162 @@
|
||||
"""S3 - Centered-offset distributions of architectural vs fold-rep variance.
|
||||
|
||||
Companion to S2, presenting the same variance decomposition as two
|
||||
overlapping KDE curves on a common centered axis.
|
||||
|
||||
For each of the 200 (fold-rep, bridge) AUC observations, compute two
|
||||
mean-centered offsets:
|
||||
|
||||
architectural offset = AUC - mean(AUC over the 4 bridges in that fold-rep)
|
||||
fold-rep offset = AUC - mean(AUC over the 50 fold-reps for that bridge)
|
||||
|
||||
Both sets have 200 values, both are centered at 0 by construction, and the
|
||||
spread of each distribution corresponds directly to one of the two SDs in
|
||||
the variance decomposition. Plotted as KDE curves on a shared x-axis, the
|
||||
ratio of their widths is the variance ratio reported in S2.
|
||||
|
||||
Re-run:
|
||||
python -m v4.figures.S3_variance_distributions
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import gaussian_kde
|
||||
|
||||
from v4.figures.util.loaders import RESULTS_ROOT
|
||||
|
||||
|
||||
OUT = Path(__file__).parent / "output" / "S3_variance_distributions.png"
|
||||
|
||||
BRIDGES = [
|
||||
("Concat", "phase3_v4/single_bcd_concat"),
|
||||
("Pairwise", "phase3_v4/single_bcd_pairwise"),
|
||||
("Gated", "phase3_v4/single_bcd_gated"),
|
||||
("Hadamard", "refuge_v2m_baseline/ensemble_single_refugelike"),
|
||||
]
|
||||
STAGE_KEY = "nt_test_auc"
|
||||
|
||||
C_ARCH = "#222" # dark grey for the architectural curve
|
||||
BRIDGE_COLORS = {
|
||||
"Concat": "#7f7f7f", # grey (underperformer)
|
||||
"Pairwise": "#ff7f0e", # orange
|
||||
"Gated": "#2ca02c", # green
|
||||
"Hadamard": "#1f77b4", # blue (default)
|
||||
}
|
||||
|
||||
|
||||
def collect() -> pd.DataFrame:
|
||||
rows = []
|
||||
for label, rel in BRIDGES:
|
||||
root = RESULTS_ROOT / rel
|
||||
for s in sorted(root.glob("rep*/binary/summary.json")):
|
||||
rep = int(s.parents[1].name.replace("rep", ""))
|
||||
d = json.loads(s.read_text())
|
||||
for fr in d.get("fold_results", []):
|
||||
v = fr.get(STAGE_KEY)
|
||||
if v is None or not np.isfinite(v):
|
||||
continue
|
||||
rows.append({
|
||||
"bridge": label,
|
||||
"rep": rep,
|
||||
"fold": int(fr["fold"]),
|
||||
"auc": float(v),
|
||||
})
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def render() -> None:
|
||||
df = collect()
|
||||
if df.empty:
|
||||
print("No data collected.")
|
||||
return
|
||||
|
||||
# Compute centering offsets per (fold-rep, bridge) cell
|
||||
fold_rep_mean = df.groupby(["rep", "fold"])["auc"].transform("mean")
|
||||
bridge_mean = df.groupby("bridge")["auc"].transform("mean")
|
||||
df["arch_offset"] = df["auc"] - fold_rep_mean
|
||||
df["fold_offset"] = df["auc"] - bridge_mean
|
||||
|
||||
# Use the same SD formula as S2 (within-group SD averaged across groups)
|
||||
# so the two figures report identical pooled numbers.
|
||||
cell_var = df.groupby(["rep", "fold"])["auc"].var(ddof=1)
|
||||
bridge_var = df.groupby("bridge")["auc"].var(ddof=1)
|
||||
arch_sd_pooled = float(np.sqrt(cell_var.mean()))
|
||||
fold_sd_pooled = float(np.sqrt(bridge_var.mean()))
|
||||
ratio = fold_sd_pooled / arch_sd_pooled if arch_sd_pooled > 0 else float("inf")
|
||||
|
||||
# Per-bridge fold-rep SDs (50 fold-reps per bridge)
|
||||
per_bridge_sd = {b: float(np.sqrt(v)) for b, v in bridge_var.items()}
|
||||
|
||||
print(f"n cells = {len(df)}")
|
||||
print(f"architectural SD (pooled, within-fold-rep avg) = {arch_sd_pooled:.4f}")
|
||||
print(f"fold-rep SD (pooled, within-bridge avg) = {fold_sd_pooled:.4f}")
|
||||
print(f"ratio fold-SD / arch-SD = {ratio:.2f}")
|
||||
print("Per-bridge fold-rep SDs:")
|
||||
for b in [name for name, _ in BRIDGES]:
|
||||
print(f" {b:<10s} SD = {per_bridge_sd[b]:.4f}")
|
||||
|
||||
# KDE x-axis: cover the union of all offset ranges
|
||||
all_offsets = np.concatenate([df["arch_offset"].values, df["fold_offset"].values])
|
||||
x_max = float(np.abs(all_offsets).max()) * 1.10
|
||||
xs = np.linspace(-x_max, x_max, 600)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9.4, 5.6))
|
||||
|
||||
# Per-bridge fold-rep offset curves (4 curves, 50 values each)
|
||||
bridge_order = [name for name, _ in BRIDGES]
|
||||
for b in bridge_order:
|
||||
offs = df.loc[df["bridge"] == b, "fold_offset"].values
|
||||
kde = gaussian_kde(offs)
|
||||
y = kde(xs)
|
||||
sd = per_bridge_sd[b]
|
||||
ax.plot(xs, y, color=BRIDGE_COLORS[b], linewidth=1.6, alpha=0.92,
|
||||
zorder=3,
|
||||
label=f"{b} fold-rep SD = {sd:.4f}")
|
||||
|
||||
# Architectural offset curve (200 values pooled across cells)
|
||||
arch_offsets = df["arch_offset"].values
|
||||
kde_arch = gaussian_kde(arch_offsets)
|
||||
y_arch = kde_arch(xs)
|
||||
ax.fill_between(xs, y_arch, color=C_ARCH, alpha=0.18, zorder=2)
|
||||
ax.plot(xs, y_arch, color=C_ARCH, linewidth=2.2, linestyle="--", zorder=4,
|
||||
label=f"Architectural (pooled) SD = {arch_sd_pooled:.4f}")
|
||||
|
||||
# Mean line at 0 (every distribution is centered there)
|
||||
ax.axvline(0, color="#666", linewidth=0.8, linestyle=":", alpha=0.6, zorder=0)
|
||||
|
||||
ax.set_xlabel("AUC offset from grouping mean", fontsize=11)
|
||||
ax.set_ylabel("Density", fontsize=11)
|
||||
ax.set_xlim(-x_max, x_max)
|
||||
ax.grid(axis="y", alpha=0.25, linestyle="--")
|
||||
ax.legend(loc="upper left", fontsize=9.5, framealpha=0.92)
|
||||
|
||||
# Top-right annotation with the variance ratio
|
||||
txt = f"variance ratio fold-SD / arch-SD = {ratio:.2f}"
|
||||
ax.text(
|
||||
0.985, 0.975, txt, transform=ax.transAxes,
|
||||
ha="right", va="top", fontsize=10.5, family="monospace",
|
||||
bbox=dict(boxstyle="round,pad=0.45", facecolor="white",
|
||||
edgecolor="#888", alpha=0.92),
|
||||
)
|
||||
|
||||
fig.suptitle(
|
||||
"Architectural vs fold-rep variance: centered-offset distributions",
|
||||
fontsize=12.5, fontweight="bold", y=0.995,
|
||||
)
|
||||
fig.tight_layout()
|
||||
|
||||
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()
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 507 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 474 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,49 @@
|
||||
class,outcome,scope,quadrant,n_eyes,mean,sd
|
||||
Glaucoma,correct,disc,ST,46,0.14268406172610618,0.08672064804334276
|
||||
Glaucoma,correct,disc,SN,46,0.1891374642326569,0.15141795670756453
|
||||
Glaucoma,correct,disc,IT,46,0.28698060971321787,0.1321960653720743
|
||||
Glaucoma,correct,disc,IN,46,0.3811978502755397,0.17407907371247724
|
||||
Glaucoma,correct,peri,ST,46,0.0801709241030986,0.08494386999472202
|
||||
Glaucoma,correct,peri,SN,46,0.15918948407117592,0.20257575474186357
|
||||
Glaucoma,correct,peri,IT,46,0.34826832191551693,0.2606073381849083
|
||||
Glaucoma,correct,peri,IN,46,0.41237125804664543,0.249789089870656
|
||||
Glaucoma,correct,full,ST,46,0.09742898919008372,0.08228642449987539
|
||||
Glaucoma,correct,full,SN,46,0.18659980165443252,0.20125297725836833
|
||||
Glaucoma,correct,full,IT,46,0.36286098988076154,0.26458782651352963
|
||||
Glaucoma,correct,full,IN,46,0.35311022712131146,0.2246580892242915
|
||||
Glaucoma,incorrect,disc,ST,34,0.252112440145696,0.15034014496616682
|
||||
Glaucoma,incorrect,disc,SN,34,0.1746002632983389,0.09578946023984732
|
||||
Glaucoma,incorrect,disc,IT,34,0.3293620129186265,0.17212955919145329
|
||||
Glaucoma,incorrect,disc,IN,34,0.24392528597147015,0.17660301429323946
|
||||
Glaucoma,incorrect,peri,ST,34,0.20385530932089996,0.17952535419921464
|
||||
Glaucoma,incorrect,peri,SN,34,0.15875330500366588,0.15449712019023593
|
||||
Glaucoma,incorrect,peri,IT,34,0.3862569142356727,0.2418765080584544
|
||||
Glaucoma,incorrect,peri,IN,34,0.25113447428742586,0.24028533916258976
|
||||
Glaucoma,incorrect,full,ST,34,0.21217464524321497,0.15540407279111246
|
||||
Glaucoma,incorrect,full,SN,34,0.16330973974559065,0.12739118311502012
|
||||
Glaucoma,incorrect,full,IT,34,0.37856896329311157,0.20473194944523931
|
||||
Glaucoma,incorrect,full,IN,34,0.24594665148198902,0.20319230952963316
|
||||
Normal,correct,disc,ST,313,0.21529517366946307,0.0928060891129116
|
||||
Normal,correct,disc,SN,313,0.1571976673739274,0.08983072836980605
|
||||
Normal,correct,disc,IT,313,0.3536251021009069,0.14392434591451472
|
||||
Normal,correct,disc,IN,313,0.27388205685844386,0.13935549411695708
|
||||
Normal,correct,peri,ST,313,0.16039412908580125,0.10491506604844639
|
||||
Normal,correct,peri,SN,313,0.11198831344232964,0.1038424179426872
|
||||
Normal,correct,peri,IT,313,0.4372451776941388,0.22439216417926178
|
||||
Normal,correct,peri,IN,313,0.29037237912573877,0.21697235549864913
|
||||
Normal,correct,full,ST,313,0.17585699926973924,0.09265567144523121
|
||||
Normal,correct,full,SN,313,0.1305376994147368,0.09449518811759064
|
||||
Normal,correct,full,IT,313,0.4112080738659084,0.19447938630719017
|
||||
Normal,correct,full,IN,313,0.28239722323230754,0.18195870133945977
|
||||
Normal,incorrect,disc,ST,27,0.12299851888544425,0.11261550366741999
|
||||
Normal,incorrect,disc,SN,27,0.2121276641099002,0.1995218721251311
|
||||
Normal,incorrect,disc,IT,27,0.26471045076221783,0.17809359884921286
|
||||
Normal,incorrect,disc,IN,27,0.40016336326290614,0.22355291760060642
|
||||
Normal,incorrect,peri,ST,27,0.07760201435962506,0.10155210979149742
|
||||
Normal,incorrect,peri,SN,27,0.1965738090304749,0.2495116802723263
|
||||
Normal,incorrect,peri,IT,27,0.2814199416369157,0.2308903646345705
|
||||
Normal,incorrect,peri,IN,27,0.4444042475568166,0.27092685369261976
|
||||
Normal,incorrect,full,ST,27,0.09632595685606479,0.13797078057713352
|
||||
Normal,incorrect,full,SN,27,0.20212570434383328,0.2336043948759492
|
||||
Normal,incorrect,full,IT,27,0.3081102746977408,0.2523555953549531
|
||||
Normal,incorrect,full,IN,27,0.3934380562908973,0.24633953116106946
|
||||
|
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 220 KiB |
@@ -0,0 +1,10 @@
|
||||
feature,mean_drop,std_drop,baseline_auc_mean
|
||||
Age,0.1487745098039216,0.07246596166317897,0.7117647058823529
|
||||
IOP_corr,0.0645588235294118,0.053595964337907635,0.7117647058823529
|
||||
Phakic/Pseudophakic,0.031004901960784353,0.05039528009700277,0.7117647058823529
|
||||
Pachymetry,0.011421568627451003,0.016547911692771797,0.7117647058823529
|
||||
Gender,0.006102941176470622,0.022596178307964714,0.7117647058823529
|
||||
eyeID,0.0,0.0,0.7117647058823529
|
||||
dioptre_2,-0.0010294117647058861,0.003622645200240852,0.7117647058823529
|
||||
astigmatism,-0.002156862745098008,0.008336072214271729,0.7117647058823529
|
||||
dioptre_1,-0.006593137254901939,0.011003619078899692,0.7117647058823529
|
||||
|
|
After Width: | Height: | Size: 67 KiB |