136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Describe PAPILA splits and V2 slot-based loaders."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import torch
|
|
|
|
# Ensure repo root is importable when running as: python3 scripts/test_v2_papila_loaders.py
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from classes import build_papila_clinical
|
|
from classes.v2 import build_papila_profile, PatientFirstSplitManager, SlotLoaderFactory
|
|
|
|
|
|
def _describe_split(name: str, df, label_col: str) -> str:
|
|
if df is None or df.empty:
|
|
return f"{name}: empty"
|
|
patient_ids = set(df["Patient ID"].tolist())
|
|
class_counts = dict(df.groupby(label_col).size().to_dict())
|
|
return (
|
|
f"{name}: patients={len(patient_ids)} rows={len(df)} "
|
|
f"class_rows={class_counts}"
|
|
)
|
|
|
|
|
|
def _describe_batch(batch: dict) -> list[str]:
|
|
lines = []
|
|
for key, val in batch.items():
|
|
if isinstance(val, torch.Tensor):
|
|
lines.append(f"{key}: tensor shape={tuple(val.shape)} dtype={val.dtype}")
|
|
elif isinstance(val, list):
|
|
non_none = next((v for v in val if v is not None), None)
|
|
lines.append(
|
|
f"{key}: list len={len(val)} sample_type={type(non_none).__name__ if non_none is not None else 'None'}"
|
|
)
|
|
else:
|
|
lines.append(f"{key}: {type(val).__name__}")
|
|
return lines
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description="Describe PAPILA splits + V2 slot-based loaders.")
|
|
ap.add_argument("--image-dir", default="Papila/FundusImages")
|
|
ap.add_argument("--clinical-dir", default="Papila/ClinicalData")
|
|
ap.add_argument("--label-col", default="Diagnosis")
|
|
ap.add_argument(
|
|
"--cat-cols",
|
|
nargs="*",
|
|
default=["Gender", "Phakic/Pseudophakic"],
|
|
help="Categorical columns for PAPILA builder.",
|
|
)
|
|
ap.add_argument("--n-splits", type=int, default=5)
|
|
ap.add_argument("--fold-seed", type=int, default=42)
|
|
ap.add_argument("--holdout-per-class", type=int, default=1)
|
|
ap.add_argument("--holdout-seed", type=int, default=123)
|
|
ap.add_argument("--batch-size", type=int, default=4)
|
|
ap.add_argument("--num-workers", type=int, default=0)
|
|
ap.add_argument("--fold", type=int, default=0, help="Which fold to inspect in detail.")
|
|
ap.add_argument(
|
|
"--sample-mode",
|
|
choices=["patient", "eye"],
|
|
default="patient",
|
|
help="Build samples per patient (multi-slot) or per eye (row-level).",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
clinical = build_papila_clinical(
|
|
image_dir=args.image_dir,
|
|
clinical_dir=args.clinical_dir,
|
|
label_col=args.label_col,
|
|
cat_cols=args.cat_cols,
|
|
n_splits=args.n_splits,
|
|
random_seed=args.fold_seed,
|
|
)
|
|
profile = build_papila_profile(
|
|
patient_col="Patient ID",
|
|
label_col=args.label_col,
|
|
sample_mode=args.sample_mode,
|
|
)
|
|
|
|
print("=== PAPILA profile slots ===")
|
|
for key, desc in profile.slot_descriptors().items():
|
|
print(f"{key}: kind={desc.kind} required={desc.required} desc={desc.description}")
|
|
print("aliases:", profile.semantic_aliases())
|
|
|
|
split_args = SimpleNamespace(
|
|
eval_mode="multiclass",
|
|
holdout_per_class=args.holdout_per_class,
|
|
holdout_seed=args.holdout_seed,
|
|
n_splits=args.n_splits,
|
|
fold_seed=args.fold_seed,
|
|
)
|
|
split_manager = PatientFirstSplitManager(patient_col="Patient ID", label_col=args.label_col)
|
|
plans = split_manager.build_plans(clinical=clinical, args=split_args, profile=profile)
|
|
|
|
print("\n=== Split summaries ===")
|
|
for i, split in enumerate(plans):
|
|
print(f"fold {i}:")
|
|
print(" " + _describe_split("train", split.train, args.label_col))
|
|
print(" " + _describe_split("val", split.val, args.label_col))
|
|
print(" " + _describe_split("holdout", split.holdout, args.label_col))
|
|
|
|
if args.fold < 0 or args.fold >= len(plans):
|
|
raise SystemExit(f"Requested fold {args.fold} but only {len(plans)} folds are available")
|
|
split = plans[args.fold]
|
|
|
|
loader_factory = SlotLoaderFactory(num_workers=args.num_workers)
|
|
loaders = loader_factory.build(
|
|
clinical=clinical,
|
|
split=split,
|
|
args=SimpleNamespace(batch_size=args.batch_size),
|
|
fold=args.fold,
|
|
profile=profile,
|
|
)
|
|
|
|
print(f"\n=== Loader inspection (fold {args.fold}) ===")
|
|
for name, loader in (("train", loaders.train), ("val", loaders.val), ("holdout", loaders.holdout)):
|
|
if loader is None:
|
|
print(f"{name}: None")
|
|
continue
|
|
print(f"{name}: batches={len(loader)} batch_size={loader.batch_size}")
|
|
batch = next(iter(loader))
|
|
for line in _describe_batch(batch):
|
|
print(f" {line}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|