| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- # pyright: basic
- from __future__ import annotations
- import argparse
- from pathlib import Path
- from typing import Any
- import pandas as pd
- from analysis.analysis_modules import (
- run_calibration,
- run_longitudinal,
- run_performance,
- run_physician,
- )
- from analysis.data_access import load_backend_evaluation, load_clinical_table
- from analysis.defaults import (
- DEFAULT_BACKENDS,
- DEFAULT_BAYESIAN_MC_PASSES,
- DEFAULT_CALIBRATION_BINS,
- DEFAULT_DECISION_THRESHOLD,
- DEFAULT_POSITIVE_CLASS_INDEX,
- noise_factor_grid,
- threshold_grid,
- )
- from analysis.holdout_evaluation import ensure_backend_netcdf
- from analysis.noise_analysis import run_noise_analysis
- from analysis.runtime import backend_dir, init_runtime_paths, load_config, write_json
- def _plot_description(filename: str) -> str:
- descriptions = {
- "performance_threshold_sweep.png": "Accuracy and F1 as the decision threshold varies.",
- "performance_uncertainty_cutoff.png": "Performance while progressively restricting to lower-uncertainty predictions.",
- "performance_uncertainty_percentile_cutoff.png": "Percentile-ranked low-uncertainty subset performance from least to most restricted.",
- "calibration_reliability.png": "Reliability diagram comparing predicted probability to empirical outcome frequency.",
- "physician_confidence_boxplot.png": "Model confidence grouped by physician confidence ratings.",
- "physician_std_boxplot.png": "Model secondary uncertainty grouped by physician confidence ratings.",
- "physician_predictive_entropy_boxplot.png": "Predictive entropy grouped by physician confidence ratings.",
- "longitudinal_cohort_confidence.png": "Longitudinal cohort comparison using model confidence.",
- "longitudinal_cohort_std.png": "Longitudinal cohort comparison using ensemble standard deviation uncertainty.",
- "longitudinal_cohort_predictive_entropy.png": "Longitudinal cohort comparison using predictive entropy uncertainty.",
- "noise_sensitivity.png": "Performance metrics across increasing Gaussian noise factors.",
- "noise_uncertainty.png": "Uncertainty metrics across increasing Gaussian noise factors.",
- "noise_confidence_certainty.png": "Confidence certainty trend across increasing Gaussian noise factors.",
- "ensemble_noise_examples.png": "Representative image slices with progressively larger Gaussian noise factors.",
- "bayesian_noise_examples.png": "Representative image slices with progressively larger Gaussian noise factors.",
- }
- return descriptions.get(filename, "Generated analysis plot.")
- def _write_backend_plot_report(backend: str, out_dir: Path) -> Path:
- plots_dir = out_dir / "plots"
- images = sorted(plots_dir.rglob("*.png")) if plots_dir.exists() else []
- report_path = out_dir / "plots_report.md"
- lines = [
- f"# {backend.title()} Analysis Plot Report",
- "",
- "This document lists generated analysis plots with brief descriptions.",
- "",
- ]
- if not images:
- lines.append("No plot images were generated for this backend run.")
- else:
- for image_path in images:
- rel = image_path.relative_to(out_dir).as_posix()
- title = image_path.stem.replace("_", " ").title()
- lines.append(f"## {title}")
- lines.append(_plot_description(image_path.name))
- lines.append("")
- lines.append(f"")
- lines.append("")
- report_path.write_text("\n".join(lines), encoding="utf-8")
- return report_path
- def _parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(
- description=(
- "Run modular evaluation analyses for ensemble and bayesian models. "
- "All outputs are written to alnn_rewrite/analysis_output."
- )
- )
- parser.add_argument(
- "--backend",
- nargs="+",
- choices=["ensemble", "bayesian"],
- default=DEFAULT_BACKENDS,
- help="Backends to evaluate.",
- )
- parser.add_argument(
- "--run-name",
- default=None,
- help="Optional run directory name under analysis_output.",
- )
- parser.add_argument(
- "--skip-noise",
- action="store_true",
- help="Skip Gaussian noise sensitivity analysis.",
- )
- return parser.parse_args()
- def _run_backend(
- config: dict[str, Any],
- root_dir: Path,
- backend: str,
- clinical_df: pd.DataFrame,
- skip_noise: bool,
- out_dir: Path,
- ) -> dict[str, Any]:
- netcdf_path = ensure_backend_netcdf(
- config=config,
- root_dir=root_dir,
- backend=backend,
- bayesian_mc_passes=DEFAULT_BAYESIAN_MC_PASSES,
- )
- evaluation = load_backend_evaluation(
- config=config,
- backend=backend,
- class_index=DEFAULT_POSITIVE_CLASS_INDEX,
- )
- thresholds = threshold_grid()
- noise_factors = noise_factor_grid()
- summary: dict[str, Any] = {
- "backend": backend,
- "netcdf": str(netcdf_path),
- "source_file": str(evaluation.source_file),
- "uncertainty_metric": evaluation.uncertainty_metric,
- }
- summary["performance"] = run_performance(
- evaluation=evaluation,
- output_dir=out_dir,
- thresholds=thresholds,
- )
- summary["calibration"] = run_calibration(
- evaluation=evaluation,
- output_dir=out_dir,
- bins=DEFAULT_CALIBRATION_BINS,
- )
- summary["physician"] = run_physician(
- evaluation=evaluation,
- clinical_df=clinical_df,
- output_dir=out_dir,
- )
- summary["longitudinal"] = run_longitudinal(
- evaluation=evaluation,
- clinical_df=clinical_df,
- output_dir=out_dir,
- )
- if skip_noise:
- summary["noise"] = {"skipped": True, "reason": "--skip-noise supplied"}
- else:
- try:
- summary["noise"] = run_noise_analysis(
- config=config,
- root_dir=root_dir,
- backend=backend,
- output_dir=out_dir,
- class_index=DEFAULT_POSITIVE_CLASS_INDEX,
- noise_sigmas=noise_factors,
- threshold=DEFAULT_DECISION_THRESHOLD,
- calibration_bins=DEFAULT_CALIBRATION_BINS,
- bayesian_mc_passes=DEFAULT_BAYESIAN_MC_PASSES,
- )
- except Exception as exc:
- summary["noise"] = {
- "skipped": True,
- "reason": f"Noise analysis failed: {exc}",
- }
- report_path = _write_backend_plot_report(backend=backend, out_dir=out_dir)
- summary["plots_report"] = str(report_path)
- write_json(out_dir / "backend_summary.json", summary)
- return summary
- def main() -> None:
- args = _parse_args()
- analysis_dir = Path(__file__).resolve().parent
- paths = init_runtime_paths(analysis_dir=analysis_dir, run_name=args.run_name)
- config = load_config(paths.root_dir)
- clinical_df = load_clinical_table(config=config, root_dir=paths.root_dir)
- manifest: dict[str, Any] = {
- "run_dir": str(paths.run_dir),
- "output_root": str(paths.output_root),
- "positive_class_index": DEFAULT_POSITIVE_CLASS_INDEX,
- "threshold_sweep": {
- "values": [float(v) for v in threshold_grid().tolist()],
- },
- "calibration_bins": DEFAULT_CALIBRATION_BINS,
- "noise_factors": noise_factor_grid(),
- "bayesian_mc_passes": DEFAULT_BAYESIAN_MC_PASSES,
- "decision_threshold": DEFAULT_DECISION_THRESHOLD,
- "backends": {},
- }
- for backend in args.backend:
- out_dir = backend_dir(paths, backend)
- manifest["backends"][backend] = _run_backend(
- config=config,
- root_dir=paths.root_dir,
- backend=backend,
- clinical_df=clinical_df,
- skip_noise=bool(args.skip_noise),
- out_dir=out_dir,
- )
- write_json(paths.run_dir / "run_manifest.json", manifest)
- print(f"Analysis complete. Results saved to {paths.run_dir}")
- if __name__ == "__main__":
- main()
|