Files
E01_textural_features_blast…/scripts/reproducibility_blocks.py
T
2026-09-11 08:28:02 +03:00

1007 lines
43 KiB
Python

from __future__ import annotations
import json
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = PROJECT_ROOT / "src"
SCRIPTS_ROOT = PROJECT_ROOT / "scripts"
VENDOR_ROOT = PROJECT_ROOT / "vendor"
VENV_SITE_PACKAGES = sorted((PROJECT_ROOT / ".venv" / "lib").glob("python*/site-packages"))
for candidate in [*VENV_SITE_PACKAGES, SRC_ROOT, SCRIPTS_ROOT, VENDOR_ROOT]:
candidate_str = str(candidate)
if candidate.exists() and candidate_str not in sys.path:
sys.path.append(candidate_str)
from build_modeling_workset import build_modeling_workset
from dataset.build_index import MAIN_PROTOCOL_EXCLUDES, build_index_artifacts
from dataset.build_patient_summary import build_patient_summary
from dataset.build_subsets import build_subsets
from dataset.build_tasks import build_tasks
from dataset.make_group_splits import make_group_splits
from evaluation.hypothesis_figures import (
generate_h1_figures,
generate_h2_figures,
generate_h3_figures,
generate_h4_figures,
generate_h5_figures,
)
from features.feature_registry import extract_features
from preprocessing.extract_bbox_crops import extract_bbox_crops
from preprocessing.extract_mask_crops import extract_mask_crops
from run_a6_to_a11 import (
_load_required_artifacts,
run_a10_a11,
run_a6,
run_a7,
run_a8_dataset_regimes,
run_a8_mask_usage,
save_a8_summary_plot,
)
from run_a9_deep_baselines import run_a9_deep_baselines
from run_subject_figures_for_articles import append_execution_log, run_subject_figures
from utils.dataset_paths import resolve_dataset_roots
from utils.project import (
CROPS_ROOT,
DATASET_INDEX_ROOT,
EXPERIMENT_EXECUTION_LOG_PATH,
FEATURES_ROOT,
FIGURES_ROOT,
PREDICTIONS_ROOT,
PROJECT_ROOT as PROJECT_ROOT_FROM_UTILS,
REPORTS_ROOT,
SUBJECT_FIGURE_CASES_PATH,
SUBJECT_FIGURE_MANIFEST_PATH,
SUBJECT_FIGURES_SUMMARY_MD,
TABLES_ROOT,
ensure_dir,
ensure_output_layout,
local_date_tag,
utc_now_iso,
write_json,
write_markdown,
)
assert PROJECT_ROOT == PROJECT_ROOT_FROM_UTILS
HYPOTHESIS_REPORTS_ROOT = REPORTS_ROOT / "hypotheses"
HYPOTHESIS_FIGURE_MANIFEST_PATH = TABLES_ROOT / "hypothesis_figure_manifest.csv"
FOUNDATION_REQUIRED = [
DATASET_INDEX_ROOT / "patient_inventory.csv",
DATASET_INDEX_ROOT / "image_index.parquet",
DATASET_INDEX_ROOT / "object_index.parquet",
DATASET_INDEX_ROOT / "subset_full_realistic.parquet",
DATASET_INDEX_ROOT / "subset_clean_cell.parquet",
DATASET_INDEX_ROOT / "subset_strict_morphology.parquet",
DATASET_INDEX_ROOT / "subset_mask_ready.parquet",
PROJECT_ROOT / "outputs" / "splits" / "split_v1.json",
TABLES_ROOT / "subset_summary.csv",
TABLES_ROOT / "split_summary.csv",
TABLES_ROOT / "task_prevalence_summary.csv",
DATASET_INDEX_ROOT / "task_a41_clean_binary.parquet",
DATASET_INDEX_ROOT / "task_a42_strict_binary.parquet",
DATASET_INDEX_ROOT / "task_a43_full_realistic_binary.parquet",
CROPS_ROOT / "bbox_roi_manifest.parquet",
CROPS_ROOT / "roi_manifest.parquet",
FEATURES_ROOT / "features_bbox.parquet",
FEATURES_ROOT / "features_mask.parquet",
FEATURES_ROOT / "features_combined.parquet",
FEATURES_ROOT / "feature_dictionary.md",
TABLES_ROOT / "feature_missingness.csv",
]
H1_REQUIRED = [
TABLES_ROOT / "baseline_model_leaderboard.csv",
TABLES_ROOT / "object_level_metrics_baselines.csv",
PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet",
TABLES_ROOT / "top_features_stability.csv",
REPORTS_ROOT / "feature_interpretation_notes.md",
]
H2_REQUIRED = [TABLES_ROOT / "ablation_mask_usage.csv", PREDICTIONS_ROOT / "h2_mask_usage_predictions.parquet"]
H3_REQUIRED = [TABLES_ROOT / "ablation_dataset_regimes.csv"]
H4_REQUIRED = [
TABLES_ROOT / "image_level_metrics.csv",
TABLES_ROOT / "patient_level_ranking_metrics.csv",
TABLES_ROOT / "figure_manifest.csv",
TABLES_ROOT / "table_manifest.csv",
]
H5_REQUIRED = [
DATASET_INDEX_ROOT / "task_a43_full_realistic_modeling_workset.parquet",
CROPS_ROOT / "roi_manifest_modeling_workset.parquet",
TABLES_ROOT / "deep_vs_handcrafted.csv",
PREDICTIONS_ROOT / "object_level_deep_predictions.parquet",
FIGURES_ROOT / "deep_training_curves.png",
]
SUBJECT_FIGURE_REQUIRED = [
SUBJECT_FIGURE_CASES_PATH,
SUBJECT_FIGURE_MANIFEST_PATH,
SUBJECT_FIGURES_SUMMARY_MD,
]
def _ensure_reports_root() -> None:
ensure_output_layout()
ensure_dir(HYPOTHESIS_REPORTS_ROOT)
def _write_summary(slug: str, title: str, lines: list[str], payload: dict[str, object]) -> dict[str, str]:
_ensure_reports_root()
md_path = HYPOTHESIS_REPORTS_ROOT / f"{slug}_summary.md"
json_path = HYPOTHESIS_REPORTS_ROOT / f"{slug}_summary.json"
write_markdown(md_path, [f"# {title}", "", f"Generated at: `{utc_now_iso()}`", "", *lines])
write_json(json_path, payload)
return {"summary_md": str(md_path), "summary_json": str(json_path)}
def _require(paths: list[Path], message: str) -> None:
missing = [str(path) for path in paths if not path.exists()]
if missing:
missing_block = "\n".join(f"- {path}" for path in missing)
raise FileNotFoundError(f"{message}\nMissing artifacts:\n{missing_block}")
def _all_exist(paths: list[Path]) -> bool:
return all(path.exists() for path in paths)
def _build_figure_records(hypothesis: str, date_tag: str, figure_paths: list[str]) -> list[dict[str, object]]:
return [
{
"hypothesis": hypothesis,
"date_tag": date_tag,
"figure_index": index,
"filename": Path(path).name,
"path": path,
}
for index, path in enumerate(figure_paths, start=1)
]
def _write_hypothesis_figure_manifest(*figure_groups: list[dict[str, object]]) -> str:
rows: list[dict[str, object]] = []
for group in figure_groups:
rows.extend(group)
manifest_df = pd.DataFrame(rows)
manifest_df.to_csv(HYPOTHESIS_FIGURE_MANIFEST_PATH, index=False)
return str(HYPOTHESIS_FIGURE_MANIFEST_PATH)
def _best_rows_by_task(leaderboard_df: pd.DataFrame) -> pd.DataFrame:
ranked = leaderboard_df.sort_values(
["task_name", "validation_pr_auc", "test_pr_auc", "test_recall_blast"],
ascending=[True, False, False, False],
)
return ranked.groupby("task_name", sort=False).head(1).reset_index(drop=True)
def _task_display_name(task_name: str) -> str:
mapping = {
"a41_clean_binary": "A4.1 clean_cell",
"a42_strict_binary": "A4.2 strict_morphology",
"a43_full_realistic_binary": "A4.3 full_realistic",
}
return mapping.get(task_name, task_name)
def run_foundation(
data_root: str | Path | None = None,
masks_root: str | Path | None = None,
config_path: str | Path | None = None,
force: bool = False,
) -> dict[str, object]:
_ensure_reports_root()
resolved_data_root, resolved_masks_root, dataset_path_source = resolve_dataset_roots(
data_root=data_root,
masks_root=masks_root,
config_path=config_path,
)
steps_run: list[str] = []
if force or not _all_exist(
[
DATASET_INDEX_ROOT / "patient_inventory.csv",
DATASET_INDEX_ROOT / "image_index.parquet",
DATASET_INDEX_ROOT / "object_index.parquet",
]
):
build_index_artifacts(
data_root=resolved_data_root,
masks_root=resolved_masks_root,
output_root=PROJECT_ROOT / "outputs",
main_protocol_excludes=MAIN_PROTOCOL_EXCLUDES,
)
steps_run.append("A0")
if force or not _all_exist(
[
DATASET_INDEX_ROOT / "subset_full_realistic.parquet",
DATASET_INDEX_ROOT / "subset_clean_cell.parquet",
DATASET_INDEX_ROOT / "subset_strict_morphology.parquet",
DATASET_INDEX_ROOT / "subset_mask_ready.parquet",
TABLES_ROOT / "subset_summary.csv",
]
):
build_subsets(
object_index_path=DATASET_INDEX_ROOT / "object_index.parquet",
image_index_path=DATASET_INDEX_ROOT / "image_index.parquet",
)
steps_run.append("A1")
patient_summary_path = build_patient_summary(
patient_inventory_path=DATASET_INDEX_ROOT / "patient_inventory.csv",
image_index_path=DATASET_INDEX_ROOT / "image_index.parquet",
object_index_path=DATASET_INDEX_ROOT / "object_index.parquet",
)
if force or not _all_exist(
[
PROJECT_ROOT / "outputs" / "splits" / "split_v1.json",
TABLES_ROOT / "split_summary.csv",
]
):
make_group_splits(
patient_summary_path=patient_summary_path,
object_index_path=DATASET_INDEX_ROOT / "object_index.parquet",
random_state=42,
)
steps_run.append("A2")
if force or not _all_exist(
[
DATASET_INDEX_ROOT / "task_a41_clean_binary.parquet",
DATASET_INDEX_ROOT / "task_a42_strict_binary.parquet",
DATASET_INDEX_ROOT / "task_a43_full_realistic_binary.parquet",
TABLES_ROOT / "task_prevalence_summary.csv",
]
):
build_tasks(PROJECT_ROOT / "outputs" / "splits" / "split_v1.json")
steps_run.append("A4")
if force or not (CROPS_ROOT / "bbox_roi_manifest.parquet").exists():
extract_bbox_crops(DATASET_INDEX_ROOT / "subset_full_realistic.parquet")
steps_run.append("A3_bbox")
if force or not (CROPS_ROOT / "roi_manifest.parquet").exists():
extract_mask_crops(CROPS_ROOT / "bbox_roi_manifest.parquet")
steps_run.append("A3_mask")
if force or not _all_exist(
[
FEATURES_ROOT / "features_bbox.parquet",
FEATURES_ROOT / "features_mask.parquet",
FEATURES_ROOT / "features_combined.parquet",
FEATURES_ROOT / "feature_dictionary.md",
TABLES_ROOT / "feature_missingness.csv",
]
):
extract_features(CROPS_ROOT / "roi_manifest.parquet")
steps_run.append("A5")
_require(FOUNDATION_REQUIRED, "Foundation validation failed after stepwise preparation.")
patient_inventory_df = pd.read_csv(DATASET_INDEX_ROOT / "patient_inventory.csv")
image_index_df = pd.read_parquet(DATASET_INDEX_ROOT / "image_index.parquet")
object_index_df = pd.read_parquet(DATASET_INDEX_ROOT / "object_index.parquet")
subset_summary_df = pd.read_csv(TABLES_ROOT / "subset_summary.csv")
split_summary_df = pd.read_csv(TABLES_ROOT / "split_summary.csv")
feature_missingness_df = pd.read_csv(TABLES_ROOT / "feature_missingness.csv")
combined_missingness_df = feature_missingness_df.loc[feature_missingness_df["matrix"].eq("combined")].copy()
bbox_manifest_df = pd.read_parquet(CROPS_ROOT / "bbox_roi_manifest.parquet")
roi_manifest_df = pd.read_parquet(CROPS_ROOT / "roi_manifest.parquet")
features_combined_df = pd.read_parquet(FEATURES_ROOT / "features_combined.parquet")
main_patient_df = patient_inventory_df.loc[patient_inventory_df["include_in_main_protocol"].eq(True)].copy()
main_image_df = image_index_df.loc[
image_index_df["include_in_main_protocol"].eq(True) & image_index_df["has_image"].eq(True)
].copy()
main_object_df = object_index_df.loc[object_index_df["include_in_main_protocol"].eq(True)].copy()
lines = [
"## Что запускает этот блок",
"",
"- `A0`: аудит данных и построение индексов.",
"- `A1`: формирование подмножеств `full_realistic`, `clean_cell`, `strict_morphology`, `mask_ready`.",
"- `A2`: patient-level split `train / val / test`.",
"- `A4`: подготовка трех бинарных задач.",
"- `A3`: построение ROI по bbox и маскам.",
"- `A5`: извлечение признаков `bbox`, `mask`, `combined`.",
"",
"## Ключевые результаты",
"",
f"- Dataset path source: `{dataset_path_source}`.",
f"- Original dataset root: `{resolved_data_root}`.",
f"- Masks dataset root: `{resolved_masks_root}`.",
f"- Recomputed steps in this run: `{', '.join(steps_run) if steps_run else 'none; reused existing validated artifacts'}`.",
f"- Main-protocol objects: `{len(main_object_df)}`.",
f"- Main-protocol images: `{len(main_image_df)}`.",
f"- Main-protocol patients: `{len(main_patient_df)}`.",
f"- Full-realistic subset rows: `{int(subset_summary_df.loc[subset_summary_df['subset_name'].eq('full_realistic'), 'object_count'].iloc[0])}`.",
f"- ROI rows in bbox manifest: `{len(bbox_manifest_df)}`.",
f"- ROI rows in mask manifest: `{len(roi_manifest_df)}`.",
f"- Combined feature rows: `{len(features_combined_df)}`.",
f"- Combined numeric features: `{len(combined_missingness_df)}`.",
f"- Combined features with missing values: `{int((combined_missingness_df['missing_fraction'] > 0).sum())}`.",
"",
"## Split summary",
"",
"| Split | Patients | Active patients | Images | Objects | Blast objects |",
"|---|---:|---:|---:|---:|---:|",
]
for row in split_summary_df.to_dict("records"):
lines.append(
f"| {row['split_name']} | {int(row['patient_count'])} | {int(row['active_patient_count'])} | "
f"{int(row['image_count'])} | {int(row['object_count'])} | {int(row['blast_count'])} |"
)
lines.extend(
[
"",
"## Основные артефакты",
"",
f"- Dataset index: `{DATASET_INDEX_ROOT / 'object_index.parquet'}`.",
f"- Split definition: `{PROJECT_ROOT / 'outputs' / 'splits' / 'split_v1.json'}`.",
f"- ROI manifest: `{CROPS_ROOT / 'roi_manifest.parquet'}`.",
f"- Combined features: `{FEATURES_ROOT / 'features_combined.parquet'}`.",
f"- Feature dictionary: `{FEATURES_ROOT / 'feature_dictionary.md'}`.",
]
)
payload = {
"block": "foundation",
"dataset_roots": {
"data_root": str(resolved_data_root),
"masks_root": str(resolved_masks_root),
"source": dataset_path_source,
},
"recomputed_steps": steps_run,
"patient_summary_csv": str(patient_summary_path),
"main_protocol_summary": {
"patient_count": int(len(main_patient_df)),
"image_count": int(len(main_image_df)),
"object_count": int(len(main_object_df)),
},
"a3": {
"bbox_manifest_rows": len(bbox_manifest_df),
"roi_manifest_rows": len(roi_manifest_df),
"bbox_roi_manifest_parquet": str(CROPS_ROOT / "bbox_roi_manifest.parquet"),
"roi_manifest_parquet": str(CROPS_ROOT / "roi_manifest.parquet"),
},
"a5": {
"features_bbox": str(FEATURES_ROOT / "features_bbox.parquet"),
"features_mask": str(FEATURES_ROOT / "features_mask.parquet"),
"features_combined": str(FEATURES_ROOT / "features_combined.parquet"),
"feature_dictionary_md": str(FEATURES_ROOT / "feature_dictionary.md"),
"feature_missingness_csv": str(TABLES_ROOT / "feature_missingness.csv"),
},
}
payload.update(_write_summary("foundation", "Foundation Reproducibility Summary", lines, payload))
return payload
def run_h1_interpretable_features(force: bool = False) -> dict[str, object]:
_require(
[
FEATURES_ROOT / "features_combined.parquet",
DATASET_INDEX_ROOT / "task_a41_clean_binary.parquet",
DATASET_INDEX_ROOT / "task_a42_strict_binary.parquet",
DATASET_INDEX_ROOT / "task_a43_full_realistic_binary.parquet",
],
"H1 requires foundation outputs. Run `scripts/run_foundation.py` first.",
)
h1_recomputed: list[str] = []
if force or not _all_exist(
[
TABLES_ROOT / "baseline_model_leaderboard.csv",
TABLES_ROOT / "object_level_metrics_baselines.csv",
PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet",
]
):
a6_result = run_a6()
h1_recomputed.append("A6")
else:
a6_result = {
"baseline_model_leaderboard_csv": str(TABLES_ROOT / "baseline_model_leaderboard.csv"),
"object_level_metrics_csv": str(TABLES_ROOT / "object_level_metrics_baselines.csv"),
"object_level_predictions_parquet": str(PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet"),
}
features_combined, _, _, tasks = _load_required_artifacts()
leaderboard_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
if force or not _all_exist(
[
TABLES_ROOT / "top_features_stability.csv",
REPORTS_ROOT / "feature_interpretation_notes.md",
]
):
run_a7(leaderboard_df, tasks["a43_full_realistic_binary"], features_combined)
h1_recomputed.append("A7")
_require(H1_REQUIRED, "H1 validation failed after preparation.")
best_rows = _best_rows_by_task(leaderboard_df)
top_features_df = pd.read_csv(TABLES_ROOT / "top_features_stability.csv").head(10)
h1_date_tag = local_date_tag()
h1_figure_paths = generate_h1_figures(
leaderboard_df=leaderboard_df,
predictions_df=pd.read_parquet(PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet"),
top_features_df=pd.read_csv(TABLES_ROOT / "top_features_stability.csv"),
date_tag=h1_date_tag,
)
h1_figure_records = _build_figure_records("H1", h1_date_tag, h1_figure_paths)
lines = [
"## Что проверяет блок H1",
"",
"- Полезны ли интерпретируемые признаки формы, цвета и текстуры для различения `blast / non-blast`.",
"- Какие baseline-модели и группы признаков дают лучший результат на patient-level split.",
f"- Recomputed steps in this run: `{', '.join(h1_recomputed) if h1_recomputed else 'none; reused existing validated artifacts'}`.",
f"- Hypothesis figures generated: `{len(h1_figure_paths)}` (`H1_{h1_date_tag}_*.png`).",
"",
"## Лучшие конфигурации по задачам",
"",
"| Task | Best model | Feature group | Validation PR-AUC | Test PR-AUC | Recall blast | Specificity |",
"|---|---|---|---:|---:|---:|---:|",
]
for row in best_rows.to_dict("records"):
lines.append(
f"| {_task_display_name(str(row['task_name']))} | {row['model_name']} | {row['feature_group']} | "
f"{float(row['validation_pr_auc']):.4f} | {float(row['test_pr_auc']):.4f} | "
f"{float(row['test_recall_blast']):.4f} | {float(row['test_specificity']):.4f} |"
)
lines.extend(
[
"",
"## Наиболее стабильные признаки для лучшей full_realistic-модели",
"",
"| Feature | Top-20 frequency | Mean importance | Best rank |",
"|---|---:|---:|---:|",
]
)
for row in top_features_df.to_dict("records"):
lines.append(
f"| {row['feature_name']} | {int(row['top20_frequency'])} | "
f"{float(row['mean_importance']):.6f} | {int(row['best_rank'])} |"
)
lines.extend(
[
"",
"## Основные артефакты",
"",
f"- Baseline leaderboard: `{TABLES_ROOT / 'baseline_model_leaderboard.csv'}`.",
f"- Object-level metrics: `{TABLES_ROOT / 'object_level_metrics_baselines.csv'}`.",
f"- Predictions: `{PREDICTIONS_ROOT / 'object_level_baseline_predictions.parquet'}`.",
f"- Stable features: `{TABLES_ROOT / 'top_features_stability.csv'}`.",
f"- Interpretation notes: `{REPORTS_ROOT / 'feature_interpretation_notes.md'}`.",
]
)
payload = {
"block": "h1",
"hypothesis": "interpretable_features_help",
"recomputed_steps": h1_recomputed,
"a6": a6_result,
"a7_report_md": str(REPORTS_ROOT / "feature_interpretation_notes.md"),
"figure_date_tag": h1_date_tag,
"hypothesis_figure_paths": h1_figure_paths,
"hypothesis_figure_records": h1_figure_records,
"best_rows": best_rows.to_dict("records"),
"top_features_preview": top_features_df.to_dict("records"),
}
payload.update(_write_summary("h1_interpretable_features", "H1 Interpretable Features Summary", lines, payload))
return payload
def run_h2_mask_effect(force: bool = False) -> dict[str, object]:
_require(
[
TABLES_ROOT / "baseline_model_leaderboard.csv",
FEATURES_ROOT / "features_bbox.parquet",
FEATURES_ROOT / "features_mask.parquet",
FEATURES_ROOT / "features_combined.parquet",
],
"H2 requires foundation outputs and H1 baseline results. Run `scripts/run_foundation.py` and `scripts/run_h1_interpretable_features.py` first.",
)
leaderboard_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
features_combined, features_bbox, features_mask, tasks = _load_required_artifacts()
if force or not _all_exist(H2_REQUIRED):
mask_ablation_df = run_a8_mask_usage(
leaderboard_df=leaderboard_df,
tasks=tasks,
features_combined=features_combined,
features_bbox=features_bbox,
features_mask=features_mask,
)
h2_recomputed = True
else:
mask_ablation_df = pd.read_csv(TABLES_ROOT / "ablation_mask_usage.csv")
h2_recomputed = False
pivot_df = (
mask_ablation_df.pivot(index="task_name", columns="label", values="pr_auc")
.reset_index()
.rename_axis(None, axis=1)
)
pivot_df["delta_mask_vs_bbox"] = pivot_df["mask_only"] - pivot_df["bbox_only"]
pivot_df["delta_bbox_plus_mask_vs_bbox"] = pivot_df["bbox_plus_mask"] - pivot_df["bbox_only"]
h2_date_tag = local_date_tag()
h2_figure_paths = generate_h2_figures(mask_ablation_df=mask_ablation_df, date_tag=h2_date_tag)
h2_figure_records = _build_figure_records("H2", h2_date_tag, h2_figure_paths)
lines = [
"## Что проверяет блок H2",
"",
"- Улучшает ли использование маски клетки качество признаков и итоговой классификации.",
f"- Recomputed in this run: `{'A8 mask usage' if h2_recomputed else 'none; reused existing validated artifact'}`.",
f"- Hypothesis figures generated: `{len(h2_figure_paths)}` (`H2_{h2_date_tag}_*.png`).",
"",
"## Сравнение по PR-AUC",
"",
"| Task | bbox_only | mask_only | bbox_plus_mask | mask_only - bbox_only | bbox_plus_mask - bbox_only |",
"|---|---:|---:|---:|---:|---:|",
]
for row in pivot_df.to_dict("records"):
lines.append(
f"| {_task_display_name(str(row['task_name']))} | {float(row['bbox_only']):.4f} | "
f"{float(row['mask_only']):.4f} | {float(row['bbox_plus_mask']):.4f} | "
f"{float(row['delta_mask_vs_bbox']):.4f} | {float(row['delta_bbox_plus_mask_vs_bbox']):.4f} |"
)
lines.extend(
[
"",
"## Основные артефакты",
"",
f"- Mask ablation table: `{TABLES_ROOT / 'ablation_mask_usage.csv'}`.",
f"- Combined ablation figure (если уже собрана совместно с H3): `{FIGURES_ROOT / 'ablation_summary.png'}`.",
]
)
payload = {
"block": "h2",
"hypothesis": "mask_improves_features",
"recomputed_steps": ["A8_mask_usage"] if h2_recomputed else [],
"ablation_mask_usage_csv": str(TABLES_ROOT / "ablation_mask_usage.csv"),
"figure_date_tag": h2_date_tag,
"hypothesis_figure_paths": h2_figure_paths,
"hypothesis_figure_records": h2_figure_records,
"pr_auc_comparison": pivot_df.to_dict("records"),
}
payload.update(_write_summary("h2_mask_effect", "H2 Mask Effect Summary", lines, payload))
return payload
def run_h3_noise_regimes(force: bool = False) -> dict[str, object]:
_require(
[
TABLES_ROOT / "baseline_model_leaderboard.csv",
TABLES_ROOT / "top_features_stability.csv",
FEATURES_ROOT / "features_combined.parquet",
],
"H3 requires foundation outputs and H1 interpretation artifacts. Run `scripts/run_foundation.py` and `scripts/run_h1_interpretable_features.py` first.",
)
leaderboard_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
features_combined, _, _, tasks = _load_required_artifacts()
if force or not _all_exist(H3_REQUIRED):
dataset_ablation_df = run_a8_dataset_regimes(
leaderboard_df=leaderboard_df,
tasks=tasks,
features_combined=features_combined,
)
h3_recomputed = True
else:
dataset_ablation_df = pd.read_csv(TABLES_ROOT / "ablation_dataset_regimes.csv")
h3_recomputed = False
primary_rows = dataset_ablation_df.loc[
dataset_ablation_df["label"].eq("combined_with_class_weight")
].copy()
extras_rows = dataset_ablation_df.loc[
dataset_ablation_df["task_name"].eq("a43_full_realistic_binary")
& dataset_ablation_df["label"].ne("combined_with_class_weight")
].copy()
h3_date_tag = local_date_tag()
h3_figure_paths = generate_h3_figures(dataset_ablation_df=dataset_ablation_df, date_tag=h3_date_tag)
h3_figure_records = _build_figure_records("H3", h3_date_tag, h3_figure_paths)
lines = [
"## Что проверяет блок H3",
"",
"- Насколько шумовые классы и более реалистичный режим корпуса усложняют задачу.",
f"- Recomputed in this run: `{'A8 dataset regimes' if h3_recomputed else 'none; reused existing validated artifact'}`.",
f"- Hypothesis figures generated: `{len(h3_figure_paths)}` (`H3_{h3_date_tag}_*.png`).",
"",
"## Основные режимы корпуса",
"",
"| Task | PR-AUC | ROC-AUC | Recall blast | Specificity |",
"|---|---:|---:|---:|---:|",
]
for row in primary_rows.to_dict("records"):
lines.append(
f"| {_task_display_name(str(row['task_name']))} | {float(row['pr_auc']):.4f} | "
f"{float(row['roc_auc']):.4f} | {float(row['recall_blast']):.4f} | {float(row['specificity']):.4f} |"
)
lines.extend(
[
"",
"## Дополнительные ablation-проверки для full_realistic",
"",
"| Label | PR-AUC | Recall blast | MCC |",
"|---|---:|---:|---:|",
]
)
for row in extras_rows.to_dict("records"):
lines.append(
f"| {row['label']} | {float(row['pr_auc']):.4f} | {float(row['recall_blast']):.4f} | "
f"{float(row['matthews_corrcoef']):.4f} |"
)
lines.extend(
[
"",
"## Основные артефакты",
"",
f"- Dataset regime ablation: `{TABLES_ROOT / 'ablation_dataset_regimes.csv'}`.",
f"- Combined ablation figure (появляется после совместного запуска H2 и H3 или `all`): `{FIGURES_ROOT / 'ablation_summary.png'}`.",
]
)
payload = {
"block": "h3",
"hypothesis": "noisy_regimes_make_task_harder",
"recomputed_steps": ["A8_dataset_regimes"] if h3_recomputed else [],
"ablation_dataset_regimes_csv": str(TABLES_ROOT / "ablation_dataset_regimes.csv"),
"figure_date_tag": h3_date_tag,
"hypothesis_figure_paths": h3_figure_paths,
"hypothesis_figure_records": h3_figure_records,
"primary_regime_rows": primary_rows.to_dict("records"),
"full_realistic_extra_rows": extras_rows.to_dict("records"),
}
payload.update(_write_summary("h3_noise_regimes", "H3 Noise Regimes Summary", lines, payload))
return payload
def run_h4_aggregation(force: bool = False) -> dict[str, object]:
_require(
[
TABLES_ROOT / "baseline_model_leaderboard.csv",
PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet",
],
"H4 requires H1 outputs. Run `scripts/run_h1_interpretable_features.py` first.",
)
if force or not _all_exist(H4_REQUIRED):
a10_a11_result = run_a10_a11()
h4_recomputed = True
else:
a10_a11_result = {
"image_level_metrics_csv": str(TABLES_ROOT / "image_level_metrics.csv"),
"patient_level_ranking_metrics_csv": str(TABLES_ROOT / "patient_level_ranking_metrics.csv"),
"figure_manifest_csv": str(TABLES_ROOT / "figure_manifest.csv"),
"table_manifest_csv": str(TABLES_ROOT / "table_manifest.csv"),
}
h4_recomputed = False
image_metrics_df = pd.read_csv(TABLES_ROOT / "image_level_metrics.csv")
patient_metrics_df = pd.read_csv(TABLES_ROOT / "patient_level_ranking_metrics.csv")
image_scores_df = pd.read_parquet(PREDICTIONS_ROOT / "image_level_scores.parquet")
patient_scores_df = pd.read_parquet(PREDICTIONS_ROOT / "patient_level_scores.parquet")
best_image = image_metrics_df.sort_values(["pr_auc", "roc_auc"], ascending=[False, False]).iloc[0]
best_patient = patient_metrics_df.sort_values(["pr_auc", "roc_auc"], ascending=[False, False]).iloc[0]
h4_date_tag = local_date_tag()
h4_figure_paths = generate_h4_figures(
image_metrics_df=image_metrics_df,
patient_metrics_df=patient_metrics_df,
image_scores_df=image_scores_df,
patient_scores_df=patient_scores_df,
date_tag=h4_date_tag,
)
h4_figure_records = _build_figure_records("H4", h4_date_tag, h4_figure_paths)
lines = [
"## Что проверяет блок H4",
"",
"- Можно ли превратить object-level score в полезную оценку изображения и пациента.",
f"- Recomputed in this run: `{'A10-A11' if h4_recomputed else 'none; reused existing validated artifacts'}`.",
f"- Hypothesis figures generated: `{len(h4_figure_paths)}` (`H4_{h4_date_tag}_*.png`).",
"",
"## Лучший image-level метод",
"",
f"- Method: `{best_image['aggregation_method']}`.",
f"- PR-AUC: `{float(best_image['pr_auc']):.4f}`.",
f"- ROC-AUC: `{float(best_image['roc_auc']):.4f}`.",
f"- Recall at FPR 10%: `{float(best_image['recall_at_fpr_10pct']):.4f}`.",
"",
"## Лучший patient-level метод",
"",
f"- Method: `{best_patient['aggregation_method']}`.",
f"- PR-AUC: `{float(best_patient['pr_auc']):.4f}`.",
f"- ROC-AUC: `{float(best_patient['roc_auc']):.4f}`.",
f"- Recall at FPR 10%: `{float(best_patient['recall_at_fpr_10pct']):.4f}`.",
"",
"## Основные артефакты",
"",
f"- Image-level metrics: `{TABLES_ROOT / 'image_level_metrics.csv'}`.",
f"- Patient-level metrics: `{TABLES_ROOT / 'patient_level_ranking_metrics.csv'}`.",
f"- Image ranking figure: `{FIGURES_ROOT / 'image_ranking_recall_curve.png'}`.",
f"- Patient ranking figure: `{FIGURES_ROOT / 'patient_ranking_curve.png'}`.",
]
payload = {
"block": "h4",
"hypothesis": "object_scores_are_useful_after_aggregation",
"recomputed_steps": ["A10_A11"] if h4_recomputed else [],
"a10_a11": a10_a11_result,
"figure_date_tag": h4_date_tag,
"hypothesis_figure_paths": h4_figure_paths,
"hypothesis_figure_records": h4_figure_records,
"best_image_method": best_image.to_dict(),
"best_patient_method": best_patient.to_dict(),
}
payload.update(_write_summary("h4_aggregation", "H4 Aggregation Summary", lines, payload))
return payload
def run_h5_deep_vs_handcrafted(force: bool = False) -> dict[str, object]:
_require(
[
TABLES_ROOT / "baseline_model_leaderboard.csv",
FEATURES_ROOT / "features_combined.parquet",
CROPS_ROOT / "roi_manifest.parquet",
DATASET_INDEX_ROOT / "task_a43_full_realistic_binary.parquet",
],
"H5 requires foundation outputs and H1 baseline leaderboard. Run `scripts/run_foundation.py` and `scripts/run_h1_interpretable_features.py` first.",
)
h5_recomputed: list[str] = []
if force or not _all_exist(
[
DATASET_INDEX_ROOT / "task_a43_full_realistic_modeling_workset.parquet",
CROPS_ROOT / "roi_manifest_modeling_workset.parquet",
]
):
workset_result = build_modeling_workset()
h5_recomputed.append("modeling_workset")
else:
workset_result = {
"task_workset_parquet": str(DATASET_INDEX_ROOT / "task_a43_full_realistic_modeling_workset.parquet"),
"roi_workset_parquet": str(CROPS_ROOT / "roi_manifest_modeling_workset.parquet"),
"qc_report_md": str(REPORTS_ROOT / "modeling_workset_qc.md"),
}
if force or not _all_exist(H5_REQUIRED):
a9_result = run_a9_deep_baselines()
h5_recomputed.append("A9")
else:
a9_result = {
"deep_vs_handcrafted_csv": str(TABLES_ROOT / "deep_vs_handcrafted.csv"),
"deep_predictions_parquet": str(PREDICTIONS_ROOT / "object_level_deep_predictions.parquet"),
"training_curves_png": str(FIGURES_ROOT / "deep_training_curves.png"),
"a9_qc_md": str(REPORTS_ROOT / "a9_deep_qc.md"),
}
_require(H5_REQUIRED, "H5 validation failed after preparation.")
comparison_df = pd.read_csv(TABLES_ROOT / "deep_vs_handcrafted.csv")
handcrafted_row = comparison_df.loc[comparison_df["source_type"].eq("handcrafted")].iloc[0]
best_deep_row = (
comparison_df.loc[comparison_df["source_type"].eq("deep")]
.sort_values(["test_pr_auc", "test_roc_auc"], ascending=[False, False])
.iloc[0]
)
delta_pr_auc = float(best_deep_row["test_pr_auc"]) - float(handcrafted_row["test_pr_auc"])
baseline_predictions_df = pd.read_parquet(PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet")
handcrafted_predictions_df = baseline_predictions_df.loc[
baseline_predictions_df["run_id"].eq(handcrafted_row["reference_run_id"])
& baseline_predictions_df["split_name"].eq("test")
].copy()
deep_predictions_df = pd.read_parquet(PREDICTIONS_ROOT / "object_level_deep_predictions.parquet")
with (PROJECT_ROOT / "outputs" / "logs" / "cnn_training_history.json").open("r", encoding="utf-8") as handle:
training_history = json.load(handle)
h5_date_tag = local_date_tag()
h5_figure_paths = generate_h5_figures(
comparison_df=comparison_df,
handcrafted_predictions=handcrafted_predictions_df,
deep_predictions=deep_predictions_df,
training_history=training_history,
date_tag=h5_date_tag,
)
h5_figure_records = _build_figure_records("H5", h5_date_tag, h5_figure_paths)
lines = [
"## Что проверяет блок H5",
"",
"- Как lightweight deep-baseline соотносится с лучшим handcrafted baseline на тех же split и данных.",
f"- Recomputed in this run: `{', '.join(h5_recomputed) if h5_recomputed else 'none; reused existing validated artifacts'}`.",
f"- Hypothesis figures generated: `{len(h5_figure_paths)}` (`H5_{h5_date_tag}_*.png`).",
"",
"## Сравнение лучшего handcrafted и лучшего deep baseline",
"",
f"- Best handcrafted: `{handcrafted_row['model_name']}` with test PR-AUC `{float(handcrafted_row['test_pr_auc']):.4f}`.",
f"- Best deep: `{best_deep_row['model_name']}` with test PR-AUC `{float(best_deep_row['test_pr_auc']):.4f}`.",
f"- PR-AUC delta (deep - handcrafted): `{delta_pr_auc:.4f}`.",
"",
"## Полная таблица сравнения",
"",
"| Model | Source | Validation PR-AUC | Test PR-AUC | Test ROC-AUC | Recall blast | MCC |",
"|---|---|---:|---:|---:|---:|---:|",
]
for row in comparison_df.to_dict("records"):
lines.append(
f"| {row['model_name']} | {row['source_type']} | {float(row['validation_pr_auc']):.4f} | "
f"{float(row['test_pr_auc']):.4f} | {float(row['test_roc_auc']):.4f} | "
f"{float(row['test_recall_blast']):.4f} | {float(row['test_matthews_corrcoef']):.4f} |"
)
lines.extend(
[
"",
"## Основные артефакты",
"",
f"- Modeling workset QC: `{REPORTS_ROOT / 'modeling_workset_qc.md'}`.",
f"- Deep comparison table: `{TABLES_ROOT / 'deep_vs_handcrafted.csv'}`.",
f"- Deep predictions: `{PREDICTIONS_ROOT / 'object_level_deep_predictions.parquet'}`.",
f"- Training curves: `{FIGURES_ROOT / 'deep_training_curves.png'}`.",
]
)
payload = {
"block": "h5",
"hypothesis": "deep_vs_handcrafted",
"recomputed_steps": h5_recomputed,
"workset": workset_result,
"a9": a9_result,
"figure_date_tag": h5_date_tag,
"hypothesis_figure_paths": h5_figure_paths,
"hypothesis_figure_records": h5_figure_records,
"handcrafted_row": handcrafted_row.to_dict(),
"best_deep_row": best_deep_row.to_dict(),
"delta_pr_auc": delta_pr_auc,
}
payload.update(_write_summary("h5_deep_vs_handcrafted", "H5 Deep vs Handcrafted Summary", lines, payload))
return payload
def run_subject_figures_block(force: bool = False, variants: int = 5) -> dict[str, object]:
_require(
[
PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet",
PREDICTIONS_ROOT / "object_level_deep_predictions.parquet",
PREDICTIONS_ROOT / "image_level_scores.parquet",
PREDICTIONS_ROOT / "patient_level_scores.parquet",
PREDICTIONS_ROOT / "h2_mask_usage_predictions.parquet",
],
"Subject-figure rendering requires completed H1-H5 outputs.",
)
if force or not _all_exist(SUBJECT_FIGURE_REQUIRED):
subject_payload = run_subject_figures(variants=variants, overwrite=True)
append_execution_log(subject_payload)
recomputed = True
else:
with SUBJECT_FIGURES_SUMMARY_MD.open("r", encoding="utf-8") as handle:
summary_preview = handle.read()
subject_payload = {
"variants_per_figure": variants,
"cases_csv": str(SUBJECT_FIGURE_CASES_PATH),
"figure_manifest_csv": str(SUBJECT_FIGURE_MANIFEST_PATH),
"summary_md": str(SUBJECT_FIGURES_SUMMARY_MD),
"summary_preview": summary_preview[:400],
}
recomputed = False
manifest_df = pd.read_csv(SUBJECT_FIGURE_MANIFEST_PATH)
counts_by_figure = manifest_df.groupby("figure_id").size().to_dict()
counts_by_hypothesis = manifest_df.groupby("hypothesis").size().to_dict()
lines = [
"## Что делает блок subject figures",
"",
"- Генерирует предметные иллюстрации для A01 и A02 на основе реальных клеток, ROI, масок и model outputs.",
f"- Recomputed in this run: `{'yes' if recomputed else 'no; reused existing subject figures'}`.",
f"- Variants per required figure: `{variants}`.",
"",
"## Counts by figure",
"",
]
for figure_id in sorted(counts_by_figure):
lines.append(f"- `{figure_id}`: `{counts_by_figure[figure_id]}` figure files.")
lines.extend(["", "## Counts by hypothesis", ""])
for hypothesis in sorted(counts_by_hypothesis):
lines.append(f"- `{hypothesis}`: `{counts_by_hypothesis[hypothesis]}` figure files.")
lines.extend(
[
"",
"## Main artifacts",
"",
f"- Cases CSV: `{SUBJECT_FIGURE_CASES_PATH}`.",
f"- Figure manifest CSV: `{SUBJECT_FIGURE_MANIFEST_PATH}`.",
f"- Summary MD: `{SUBJECT_FIGURES_SUMMARY_MD}`.",
f"- Execution log: `{EXPERIMENT_EXECUTION_LOG_PATH}`.",
]
)
payload = {
"block": "subject_figures",
"recomputed": recomputed,
"variants_per_figure": variants,
"counts_by_figure": counts_by_figure,
"counts_by_hypothesis": counts_by_hypothesis,
**subject_payload,
}
payload.update(_write_summary("subject_figures", "Subject Figures Summary", lines, payload))
return payload
def run_all_hypotheses(
data_root: str | Path | None = None,
masks_root: str | Path | None = None,
config_path: str | Path | None = None,
force: bool = False,
with_subject_figures: bool = False,
subject_figure_variants: int = 5,
) -> dict[str, object]:
foundation = run_foundation(data_root=data_root, masks_root=masks_root, config_path=config_path, force=force)
h1 = run_h1_interpretable_features(force=force)
h2 = run_h2_mask_effect(force=force)
h3 = run_h3_noise_regimes(force=force)
if (TABLES_ROOT / "ablation_mask_usage.csv").exists() and (TABLES_ROOT / "ablation_dataset_regimes.csv").exists():
save_a8_summary_plot(
pd.read_csv(TABLES_ROOT / "ablation_mask_usage.csv"),
pd.read_csv(TABLES_ROOT / "ablation_dataset_regimes.csv"),
)
h4 = run_h4_aggregation(force=force)
h5 = run_h5_deep_vs_handcrafted(force=force)
subject_figures = (
run_subject_figures_block(force=force, variants=subject_figure_variants) if with_subject_figures else None
)
hypothesis_figure_manifest_csv = _write_hypothesis_figure_manifest(
h1["hypothesis_figure_records"],
h2["hypothesis_figure_records"],
h3["hypothesis_figure_records"],
h4["hypothesis_figure_records"],
h5["hypothesis_figure_records"],
)
lines = [
"## Выполненные блоки",
"",
f"- Foundation: `{foundation['summary_md']}`.",
f"- H1: `{h1['summary_md']}`.",
f"- H2: `{h2['summary_md']}`.",
f"- H3: `{h3['summary_md']}`.",
f"- H4: `{h4['summary_md']}`.",
f"- H5: `{h5['summary_md']}`.",
"",
"## Сводные артефакты полного прогона",
"",
f"- Combined ablation figure: `{FIGURES_ROOT / 'ablation_summary.png'}`.",
f"- Hypothesis figure manifest: `{hypothesis_figure_manifest_csv}`.",
f"- Subject figure manifest: `{SUBJECT_FIGURE_MANIFEST_PATH}`." if with_subject_figures else "- Subject figures were not requested in this run.",
f"- Figure manifest: `{TABLES_ROOT / 'figure_manifest.csv'}`.",
f"- Table manifest: `{TABLES_ROOT / 'table_manifest.csv'}`.",
f"- Full detailed report: `{PROJECT_ROOT / 'reports' / 'E01_detailed_report.md'}`.",
]
payload = {
"block": "all",
"foundation": foundation,
"h1": h1,
"h2": h2,
"h3": h3,
"h4": h4,
"h5": h5,
"subject_figures": subject_figures,
"hypothesis_figure_manifest_csv": hypothesis_figure_manifest_csv,
}
payload.update(_write_summary("run_all_hypotheses", "Run-All Hypotheses Summary", lines, payload))
return payload
def print_payload(payload: dict[str, object]) -> None:
print(json.dumps(payload, ensure_ascii=False, indent=2))