Initial commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Scripts Overview
|
||||
|
||||
This directory contains two kinds of scripts.
|
||||
|
||||
## Public entry points
|
||||
|
||||
These scripts are intended to be launched directly by a researcher:
|
||||
|
||||
- `run_foundation.py` - prepares the shared experiment base (`A0-A5`).
|
||||
- `run_h1_interpretable_features.py` - checks hypothesis H1.
|
||||
- `run_h2_mask_effect.py` - checks hypothesis H2.
|
||||
- `run_h3_noise_regimes.py` - checks hypothesis H3.
|
||||
- `run_h4_aggregation.py` - checks hypothesis H4.
|
||||
- `run_h5_deep_vs_handcrafted.py` - checks hypothesis H5.
|
||||
- `run_all_hypotheses.py` - runs the full reproducible experiment end to end.
|
||||
|
||||
## Internal orchestration and stage helpers
|
||||
|
||||
These files are used by the public entry points and are kept in the repository
|
||||
because they contain reusable implementation details:
|
||||
|
||||
- `reproducibility_blocks.py` - high-level orchestration shared by public runners.
|
||||
- `run_a1_to_a4.py`, `run_a3_to_a5.py`, `run_a6_to_a11.py`, `run_a9_deep_baselines.py` - lower-level stage runners for the original experiment plan.
|
||||
- `build_modeling_workset.py` - prepares the modeling subset used by H5.
|
||||
- `render_markdown_report_html.py` - utility for rendering Markdown reports into HTML.
|
||||
|
||||
If you only want to reproduce the experiment, start with the public entry points
|
||||
or the `Makefile` targets documented in `README.md` and `docs/reproducibility_guide.md`.
|
||||
@@ -0,0 +1,94 @@
|
||||
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"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
from utils.project import CROPS_ROOT, DATASET_INDEX_ROOT, REPORTS_ROOT, ensure_output_layout, write_markdown
|
||||
|
||||
|
||||
KEY_COLUMNS = [
|
||||
"patient_id",
|
||||
"image_id",
|
||||
"object_id_within_image",
|
||||
"class_id",
|
||||
"class_name",
|
||||
"has_mask",
|
||||
]
|
||||
|
||||
|
||||
def _cap_frame(frame: pd.DataFrame, max_rows: int, random_state: int) -> pd.DataFrame:
|
||||
if len(frame) <= max_rows:
|
||||
return frame
|
||||
sampled_groups: list[pd.DataFrame] = []
|
||||
remaining = max_rows
|
||||
groups = list(frame.groupby("target_binary", sort=True))
|
||||
for index, (_, group) in enumerate(groups):
|
||||
if index == len(groups) - 1:
|
||||
n_take = min(len(group), remaining)
|
||||
else:
|
||||
n_take = max(1, int(round(max_rows * len(group) / len(frame))))
|
||||
n_take = min(len(group), n_take)
|
||||
remaining -= n_take
|
||||
sampled_groups.append(group.sample(n=n_take, random_state=random_state))
|
||||
return pd.concat(sampled_groups, ignore_index=True)
|
||||
|
||||
|
||||
def build_modeling_workset() -> dict[str, str]:
|
||||
ensure_output_layout()
|
||||
full_task = pd.read_parquet(DATASET_INDEX_ROOT / "task_a43_full_realistic_binary.parquet")
|
||||
roi_manifest = pd.read_parquet(CROPS_ROOT / "roi_manifest.parquet")
|
||||
|
||||
train_df = _cap_frame(full_task.loc[full_task["split_name"] == "train"].copy(), max_rows=20000, random_state=42)
|
||||
val_df = _cap_frame(full_task.loc[full_task["split_name"] == "val"].copy(), max_rows=5000, random_state=43)
|
||||
test_df = full_task.loc[full_task["split_name"] == "test"].copy()
|
||||
workset_df = pd.concat([train_df, val_df, test_df], ignore_index=True)
|
||||
|
||||
workset_keys = workset_df[KEY_COLUMNS].drop_duplicates()
|
||||
workset_roi = roi_manifest.merge(workset_keys, on=KEY_COLUMNS, how="inner", validate="one_to_one")
|
||||
|
||||
workset_task_path = DATASET_INDEX_ROOT / "task_a43_full_realistic_modeling_workset.parquet"
|
||||
workset_roi_path = CROPS_ROOT / "roi_manifest_modeling_workset.parquet"
|
||||
workset_df.to_parquet(workset_task_path, index=False)
|
||||
workset_roi.to_parquet(workset_roi_path, index=False)
|
||||
|
||||
report_lines = [
|
||||
"# Modeling Workset QC",
|
||||
"",
|
||||
f"- Train rows: `{len(train_df)}`",
|
||||
f"- Validation rows: `{len(val_df)}`",
|
||||
f"- Test rows: `{len(test_df)}`",
|
||||
f"- Total workset rows: `{len(workset_df)}`",
|
||||
f"- ROI rows matched: `{len(workset_roi)}`",
|
||||
f"- Task artifact: `{workset_task_path}`",
|
||||
f"- ROI artifact: `{workset_roi_path}`",
|
||||
]
|
||||
report_path = REPORTS_ROOT / "modeling_workset_qc.md"
|
||||
write_markdown(report_path, report_lines)
|
||||
|
||||
return {
|
||||
"task_workset_parquet": str(workset_task_path),
|
||||
"roi_workset_parquet": str(workset_roi_path),
|
||||
"qc_report_md": str(report_path),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(json.dumps(build_modeling_workset(), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import html
|
||||
import mimetypes
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
|
||||
IMAGE_RE = re.compile(r"^!\[(.*?)\]\((.*?)\)\s*$")
|
||||
UL_RE = re.compile(r"^-\s+(.*)$")
|
||||
OL_RE = re.compile(r"^\d+\.\s+(.*)$")
|
||||
TABLE_RULE_RE = re.compile(r"^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$")
|
||||
CODE_RE = re.compile(r"`([^`]+)`")
|
||||
LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
||||
STRONG_RE = re.compile(r"\*\*([^*]+)\*\*")
|
||||
EM_RE = re.compile(r"(?<!\*)\*([^*]+)\*(?!\*)")
|
||||
|
||||
|
||||
def to_href(target: str) -> str:
|
||||
candidate = Path(target)
|
||||
if candidate.is_absolute() and candidate.exists():
|
||||
return candidate.as_uri()
|
||||
return html.escape(target, quote=True)
|
||||
|
||||
|
||||
def to_image_src(target: str) -> str:
|
||||
candidate = Path(target)
|
||||
if candidate.is_absolute() and candidate.exists():
|
||||
mime_type, _ = mimetypes.guess_type(candidate.name)
|
||||
if mime_type:
|
||||
encoded = base64.b64encode(candidate.read_bytes()).decode("ascii")
|
||||
return f"data:{mime_type};base64,{encoded}"
|
||||
return html.escape(target, quote=True)
|
||||
|
||||
|
||||
def render_inline(text: str) -> str:
|
||||
placeholders: list[str] = []
|
||||
|
||||
def stash(fragment: str) -> str:
|
||||
placeholders.append(fragment)
|
||||
return f"@@PLACEHOLDER_{len(placeholders) - 1}@@"
|
||||
|
||||
escaped = html.escape(text)
|
||||
|
||||
def code_replace(match: re.Match[str]) -> str:
|
||||
return stash(f"<code>{html.escape(match.group(1))}</code>")
|
||||
|
||||
escaped = CODE_RE.sub(code_replace, escaped)
|
||||
|
||||
def link_replace(match: re.Match[str]) -> str:
|
||||
label = render_inline(match.group(1))
|
||||
href = to_href(match.group(2))
|
||||
return stash(f'<a href="{href}">{label}</a>')
|
||||
|
||||
escaped = LINK_RE.sub(link_replace, escaped)
|
||||
escaped = STRONG_RE.sub(r"<strong>\1</strong>", escaped)
|
||||
escaped = EM_RE.sub(r"<em>\1</em>", escaped)
|
||||
|
||||
for index, fragment in enumerate(placeholders):
|
||||
escaped = escaped.replace(f"@@PLACEHOLDER_{index}@@", fragment)
|
||||
return escaped
|
||||
|
||||
|
||||
def split_table_row(line: str) -> list[str]:
|
||||
raw_cells = line.strip().strip("|").split("|")
|
||||
return [cell.strip() for cell in raw_cells]
|
||||
|
||||
|
||||
def cell_alignment(spec: str) -> str:
|
||||
stripped = spec.strip()
|
||||
left = stripped.startswith(":")
|
||||
right = stripped.endswith(":")
|
||||
if left and right:
|
||||
return "center"
|
||||
if right:
|
||||
return "right"
|
||||
return "left"
|
||||
|
||||
|
||||
def render_table(table_lines: list[str]) -> str:
|
||||
rows = [split_table_row(line) for line in table_lines]
|
||||
alignments: list[str] = []
|
||||
body_rows = rows[1:]
|
||||
if len(rows) > 1 and TABLE_RULE_RE.match(table_lines[1].strip()):
|
||||
alignments = [cell_alignment(spec) for spec in rows[1]]
|
||||
body_rows = rows[2:]
|
||||
|
||||
header = rows[0]
|
||||
if not alignments:
|
||||
alignments = ["left"] * len(header)
|
||||
|
||||
parts = ["<table>", "<thead>", "<tr>"]
|
||||
for cell, align in zip(header, alignments):
|
||||
parts.append(f'<th style="text-align: {align};">{render_inline(cell)}</th>')
|
||||
parts.extend(["</tr>", "</thead>", "<tbody>"])
|
||||
|
||||
for row in body_rows:
|
||||
parts.append("<tr>")
|
||||
for idx, cell in enumerate(row):
|
||||
align = alignments[idx] if idx < len(alignments) else "left"
|
||||
parts.append(f'<td style="text-align: {align};">{render_inline(cell)}</td>')
|
||||
parts.append("</tr>")
|
||||
|
||||
parts.extend(["</tbody>", "</table>"])
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def markdown_to_html(markdown_text: str, title: str) -> str:
|
||||
lines = markdown_text.splitlines()
|
||||
body_parts: list[str] = []
|
||||
paragraph_lines: list[str] = []
|
||||
list_items: list[str] = []
|
||||
list_tag: str | None = None
|
||||
table_lines: list[str] = []
|
||||
|
||||
def flush_paragraph() -> None:
|
||||
if not paragraph_lines:
|
||||
return
|
||||
paragraph = " ".join(line.strip() for line in paragraph_lines)
|
||||
body_parts.append(f"<p>{render_inline(paragraph)}</p>")
|
||||
paragraph_lines.clear()
|
||||
|
||||
def flush_list() -> None:
|
||||
nonlocal list_tag
|
||||
if not list_items or not list_tag:
|
||||
return
|
||||
body_parts.append(f"<{list_tag}>")
|
||||
for item in list_items:
|
||||
body_parts.append(f"<li>{render_inline(item)}</li>")
|
||||
body_parts.append(f"</{list_tag}>")
|
||||
list_items.clear()
|
||||
list_tag = None
|
||||
|
||||
def flush_table() -> None:
|
||||
if not table_lines:
|
||||
return
|
||||
body_parts.append(render_table(table_lines))
|
||||
table_lines.clear()
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
if not stripped:
|
||||
flush_paragraph()
|
||||
flush_list()
|
||||
flush_table()
|
||||
continue
|
||||
|
||||
if stripped.startswith("|"):
|
||||
flush_paragraph()
|
||||
flush_list()
|
||||
table_lines.append(stripped)
|
||||
continue
|
||||
|
||||
flush_table()
|
||||
|
||||
image_match = IMAGE_RE.match(stripped)
|
||||
if image_match:
|
||||
flush_paragraph()
|
||||
flush_list()
|
||||
alt_text, image_target = image_match.groups()
|
||||
src = to_image_src(image_target)
|
||||
caption = render_inline(alt_text)
|
||||
body_parts.append(
|
||||
"\n".join(
|
||||
[
|
||||
'<figure class="report-figure">',
|
||||
f'<img src="{src}" alt="{html.escape(alt_text, quote=True)}">',
|
||||
f"<figcaption>{caption}</figcaption>",
|
||||
"</figure>",
|
||||
]
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
heading_match = HEADING_RE.match(stripped)
|
||||
if heading_match:
|
||||
flush_paragraph()
|
||||
flush_list()
|
||||
level = len(heading_match.group(1))
|
||||
heading_text = render_inline(heading_match.group(2))
|
||||
body_parts.append(f"<h{level}>{heading_text}</h{level}>")
|
||||
continue
|
||||
|
||||
unordered_match = UL_RE.match(stripped)
|
||||
if unordered_match:
|
||||
flush_paragraph()
|
||||
if list_tag not in (None, "ul"):
|
||||
flush_list()
|
||||
list_tag = "ul"
|
||||
list_items.append(unordered_match.group(1))
|
||||
continue
|
||||
|
||||
ordered_match = OL_RE.match(stripped)
|
||||
if ordered_match:
|
||||
flush_paragraph()
|
||||
if list_tag not in (None, "ol"):
|
||||
flush_list()
|
||||
list_tag = "ol"
|
||||
list_items.append(ordered_match.group(0).split(". ", 1)[1])
|
||||
continue
|
||||
|
||||
if list_items:
|
||||
flush_list()
|
||||
paragraph_lines.append(stripped)
|
||||
|
||||
flush_paragraph()
|
||||
flush_list()
|
||||
flush_table()
|
||||
|
||||
css = """
|
||||
@page {
|
||||
margin: 22mm 18mm 20mm 22mm;
|
||||
}
|
||||
body {
|
||||
font-family: "Times New Roman", serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.45;
|
||||
color: #111111;
|
||||
max-width: 175mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 700;
|
||||
page-break-after: avoid;
|
||||
margin-top: 1.1em;
|
||||
margin-bottom: 0.45em;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 18pt;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 15pt;
|
||||
}
|
||||
h3 {
|
||||
font-size: 13pt;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 0.65em 0;
|
||||
text-align: justify;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0.2em 0 0.8em 1.5em;
|
||||
}
|
||||
li {
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 10.5pt;
|
||||
background: #f2f2f2;
|
||||
padding: 0.05em 0.25em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.75em 0 1.0em 0;
|
||||
font-size: 10.5pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #444444;
|
||||
padding: 5px 7px;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #e9edf2;
|
||||
}
|
||||
.report-figure {
|
||||
margin: 1em auto 1.2em auto;
|
||||
text-align: center;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.report-figure img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #b5b5b5;
|
||||
}
|
||||
.report-figure figcaption {
|
||||
margin-top: 0.45em;
|
||||
font-size: 10.5pt;
|
||||
font-style: italic;
|
||||
}
|
||||
a {
|
||||
color: #0b57d0;
|
||||
text-decoration: none;
|
||||
}
|
||||
"""
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
"<!DOCTYPE html>",
|
||||
'<html lang="ru">',
|
||||
"<head>",
|
||||
' <meta charset="utf-8">',
|
||||
f" <title>{html.escape(title)}</title>",
|
||||
" <style>",
|
||||
css,
|
||||
" </style>",
|
||||
"</head>",
|
||||
"<body>",
|
||||
*body_parts,
|
||||
"</body>",
|
||||
"</html>",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Render a Markdown report to standalone HTML.")
|
||||
parser.add_argument("input_markdown", type=Path)
|
||||
parser.add_argument("output_html", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
markdown_text = args.input_markdown.read_text(encoding="utf-8")
|
||||
html_text = markdown_to_html(markdown_text, title=args.input_markdown.stem)
|
||||
args.output_html.write_text(html_text, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = PROJECT_ROOT / "src"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
subset_result = build_subsets(
|
||||
object_index_path=PROJECT_ROOT / "outputs" / "dataset_index" / "object_index.parquet",
|
||||
image_index_path=PROJECT_ROOT / "outputs" / "dataset_index" / "image_index.parquet",
|
||||
)
|
||||
patient_summary_path = build_patient_summary(
|
||||
patient_inventory_path=PROJECT_ROOT / "outputs" / "dataset_index" / "patient_inventory.csv",
|
||||
image_index_path=PROJECT_ROOT / "outputs" / "dataset_index" / "image_index.parquet",
|
||||
object_index_path=PROJECT_ROOT / "outputs" / "dataset_index" / "object_index.parquet",
|
||||
)
|
||||
split_result = make_group_splits(
|
||||
patient_summary_path=patient_summary_path,
|
||||
object_index_path=PROJECT_ROOT / "outputs" / "dataset_index" / "object_index.parquet",
|
||||
random_state=42,
|
||||
)
|
||||
task_result = build_tasks(PROJECT_ROOT / "outputs" / "splits" / "split_v1.json")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"a1": subset_result,
|
||||
"a2": {"patient_summary_csv": str(patient_summary_path), **split_result},
|
||||
"a4": task_result,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = PROJECT_ROOT / "src"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
bbox_manifest_df = extract_bbox_crops(PROJECT_ROOT / "outputs" / "dataset_index" / "subset_full_realistic.parquet")
|
||||
roi_manifest_df = extract_mask_crops(PROJECT_ROOT / "outputs" / "crops" / "bbox_roi_manifest.parquet")
|
||||
feature_result = extract_features(PROJECT_ROOT / "outputs" / "crops" / "roi_manifest.parquet")
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"a3": {
|
||||
"bbox_manifest_rows": len(bbox_manifest_df),
|
||||
"roi_manifest_rows": len(roi_manifest_df),
|
||||
"roi_manifest_path": str(PROJECT_ROOT / "outputs" / "crops" / "roi_manifest.parquet"),
|
||||
},
|
||||
"a5": feature_result,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,737 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import joblib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.inspection import permutation_importance
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = PROJECT_ROOT / "src"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
from evaluation.aggregate_scores import aggregate_object_scores, compute_aggregate_metrics
|
||||
from evaluation.metrics import bootstrap_metric_interval, compute_binary_metrics, optimize_threshold
|
||||
from evaluation.visualization import (
|
||||
save_bar_plot,
|
||||
save_confusion_matrix,
|
||||
save_example_rois,
|
||||
save_pipeline_overview,
|
||||
save_pr_roc_summary,
|
||||
save_ranking_curve,
|
||||
)
|
||||
from models.baseline_pipeline import (
|
||||
FEATURE_GROUPS,
|
||||
KEY_COLUMNS,
|
||||
build_model,
|
||||
feature_columns_by_group,
|
||||
fit_predict_probability,
|
||||
merge_task_with_features,
|
||||
)
|
||||
from utils.project import CHECKPOINTS_ROOT, CROPS_ROOT, FEATURES_ROOT, FIGURES_ROOT, PREDICTIONS_ROOT, REPORTS_ROOT, TABLES_ROOT, ensure_output_layout, write_markdown
|
||||
|
||||
|
||||
TASK_FILES = {
|
||||
"a41_clean_binary": PROJECT_ROOT / "outputs" / "dataset_index" / "task_a41_clean_binary.parquet",
|
||||
"a42_strict_binary": PROJECT_ROOT / "outputs" / "dataset_index" / "task_a42_strict_binary.parquet",
|
||||
"a43_full_realistic_binary": PROJECT_ROOT / "outputs" / "dataset_index" / "task_a43_full_realistic_binary.parquet",
|
||||
}
|
||||
MODEL_NAMES = [
|
||||
"logistic_regression",
|
||||
"linear_svm",
|
||||
"rbf_svm",
|
||||
"random_forest",
|
||||
"xgboost",
|
||||
]
|
||||
|
||||
|
||||
def _pick_feature_path(base_name: str) -> Path:
|
||||
candidates = [
|
||||
FEATURES_ROOT / f"{base_name}.parquet",
|
||||
FEATURES_ROOT / f"{base_name}_fast.parquet",
|
||||
FEATURES_ROOT / f"{base_name}_lite.parquet",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
raise FileNotFoundError(f"None of the candidate feature files exist for {base_name}: {candidates}")
|
||||
|
||||
|
||||
def _load_required_artifacts() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, dict[str, pd.DataFrame]]:
|
||||
features_combined = pd.read_parquet(_pick_feature_path("features_combined"))
|
||||
features_bbox = pd.read_parquet(_pick_feature_path("features_bbox"))
|
||||
features_mask = pd.read_parquet(_pick_feature_path("features_mask"))
|
||||
tasks = {task_name: pd.read_parquet(path) for task_name, path in TASK_FILES.items()}
|
||||
return features_combined, features_bbox, features_mask, tasks
|
||||
|
||||
|
||||
def _split_frame(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
||||
train_df = df.loc[df["split_name"] == "train"].copy()
|
||||
val_df = df.loc[df["split_name"] == "val"].copy()
|
||||
test_df = df.loc[df["split_name"] == "test"].copy()
|
||||
if min(len(train_df), len(val_df), len(test_df)) == 0:
|
||||
raise ValueError("One of the splits is empty.")
|
||||
return train_df, val_df, test_df
|
||||
|
||||
|
||||
def _cap_frame(frame: pd.DataFrame, max_rows: int, random_state: int) -> pd.DataFrame:
|
||||
if len(frame) <= max_rows:
|
||||
return frame
|
||||
sampled_groups: list[pd.DataFrame] = []
|
||||
remaining = max_rows
|
||||
groups = list(frame.groupby("target_binary", sort=True))
|
||||
for index, (_, group) in enumerate(groups):
|
||||
if index == len(groups) - 1:
|
||||
n_take = min(len(group), remaining)
|
||||
else:
|
||||
n_take = max(1, int(round(max_rows * len(group) / len(frame))))
|
||||
n_take = min(len(group), n_take)
|
||||
remaining -= n_take
|
||||
sampled_groups.append(group.sample(n=n_take, random_state=random_state))
|
||||
return pd.concat(sampled_groups, ignore_index=True)
|
||||
|
||||
|
||||
def _select_best_row(leaderboard_df: pd.DataFrame, task_name: str | None = None) -> pd.Series:
|
||||
frame = leaderboard_df.copy()
|
||||
if task_name is not None:
|
||||
frame = frame.loc[frame["task_name"] == task_name].copy()
|
||||
if len(frame) == 0:
|
||||
raise ValueError("Leaderboard is empty for selection.")
|
||||
return frame.sort_values(
|
||||
["validation_pr_auc", "test_pr_auc", "test_recall_blast"],
|
||||
ascending=[False, False, False],
|
||||
).iloc[0]
|
||||
|
||||
|
||||
def run_a6() -> dict[str, str]:
|
||||
ensure_output_layout()
|
||||
features_combined, features_bbox, features_mask, tasks = _load_required_artifacts()
|
||||
feature_groups = feature_columns_by_group(features_combined)
|
||||
|
||||
metrics_rows: list[dict[str, object]] = []
|
||||
prediction_rows: list[dict[str, object]] = []
|
||||
checkpoint_rows: list[dict[str, object]] = []
|
||||
|
||||
for task_name, task_df in tasks.items():
|
||||
merged_df = merge_task_with_features(task_df, features_combined)
|
||||
train_df, val_df, test_df = _split_frame(merged_df)
|
||||
train_df = _cap_frame(train_df, max_rows=20000, random_state=42)
|
||||
val_df = _cap_frame(val_df, max_rows=5000, random_state=43)
|
||||
scale_pos_weight = float((train_df["target_binary"].eq(0).sum()) / max(train_df["target_binary"].eq(1).sum(), 1))
|
||||
|
||||
for model_name in MODEL_NAMES:
|
||||
for feature_group_name, feature_columns in feature_groups.items():
|
||||
X_train = train_df[feature_columns]
|
||||
y_train = train_df["target_binary"].astype(int)
|
||||
X_val = val_df[feature_columns]
|
||||
y_val = val_df["target_binary"].astype(int)
|
||||
X_test = test_df[feature_columns]
|
||||
y_test = test_df["target_binary"].astype(int)
|
||||
|
||||
pipeline, spec = build_model(model_name, scale_pos_weight=scale_pos_weight, use_class_weight=True)
|
||||
val_score = fit_predict_probability(pipeline, X_train, y_train, X_val)
|
||||
threshold = optimize_threshold(y_val.to_numpy(), val_score)
|
||||
test_score = np.asarray(pipeline.predict_proba(X_test)[:, 1], dtype=float) if hasattr(pipeline, "predict_proba") else fit_predict_probability(pipeline, X_train, y_train, X_test)
|
||||
|
||||
val_metrics = compute_binary_metrics(y_val.to_numpy(), val_score, threshold)
|
||||
test_metrics = compute_binary_metrics(y_test.to_numpy(), test_score, threshold)
|
||||
ci = bootstrap_metric_interval(y_test.to_numpy(), test_score, threshold, n_bootstrap=60, random_state=42)
|
||||
|
||||
run_id = f"{task_name}__{model_name}__{feature_group_name}"
|
||||
checkpoint_path = CHECKPOINTS_ROOT / f"{run_id}.joblib"
|
||||
joblib.dump(
|
||||
{
|
||||
"pipeline": pipeline,
|
||||
"feature_columns": feature_columns,
|
||||
"threshold": threshold,
|
||||
"task_name": task_name,
|
||||
"model_name": model_name,
|
||||
"feature_group": feature_group_name,
|
||||
},
|
||||
checkpoint_path,
|
||||
)
|
||||
checkpoint_rows.append({"run_id": run_id, "checkpoint_path": str(checkpoint_path)})
|
||||
|
||||
y_pred = (test_score >= threshold).astype(int)
|
||||
split_rows = test_df[KEY_COLUMNS + ["split_name", "target_binary"]].copy()
|
||||
split_rows["run_id"] = run_id
|
||||
split_rows["task_name"] = task_name
|
||||
split_rows["model_name"] = model_name
|
||||
split_rows["implementation"] = spec.implementation
|
||||
split_rows["feature_group"] = feature_group_name
|
||||
split_rows["threshold"] = threshold
|
||||
split_rows["split_name"] = "test"
|
||||
split_rows["y_true"] = y_test.to_numpy()
|
||||
split_rows["y_score"] = test_score
|
||||
split_rows["y_pred"] = y_pred
|
||||
prediction_rows.extend(split_rows.to_dict("records"))
|
||||
|
||||
metrics_rows.append(
|
||||
{
|
||||
"run_id": run_id,
|
||||
"task_name": task_name,
|
||||
"model_name": model_name,
|
||||
"implementation": spec.implementation,
|
||||
"feature_group": feature_group_name,
|
||||
"n_features": int(len(feature_columns)),
|
||||
"threshold": threshold,
|
||||
"validation_pr_auc": val_metrics.pr_auc,
|
||||
"validation_roc_auc": val_metrics.roc_auc,
|
||||
"validation_recall_blast": val_metrics.recall_blast,
|
||||
"test_pr_auc": test_metrics.pr_auc,
|
||||
"test_pr_auc_ci_low": ci["pr_auc"][0],
|
||||
"test_pr_auc_ci_high": ci["pr_auc"][1],
|
||||
"test_roc_auc": test_metrics.roc_auc,
|
||||
"test_roc_auc_ci_low": ci["roc_auc"][0],
|
||||
"test_roc_auc_ci_high": ci["roc_auc"][1],
|
||||
"test_balanced_accuracy": test_metrics.balanced_accuracy,
|
||||
"test_balanced_accuracy_ci_low": ci["balanced_accuracy"][0],
|
||||
"test_balanced_accuracy_ci_high": ci["balanced_accuracy"][1],
|
||||
"test_macro_f1": test_metrics.macro_f1,
|
||||
"test_macro_f1_ci_low": ci["macro_f1"][0],
|
||||
"test_macro_f1_ci_high": ci["macro_f1"][1],
|
||||
"test_recall_blast": test_metrics.recall_blast,
|
||||
"test_recall_blast_ci_low": ci["recall_blast"][0],
|
||||
"test_recall_blast_ci_high": ci["recall_blast"][1],
|
||||
"test_specificity": test_metrics.specificity,
|
||||
"test_specificity_ci_low": ci["specificity"][0],
|
||||
"test_specificity_ci_high": ci["specificity"][1],
|
||||
"test_matthews_corrcoef": test_metrics.matthews_corrcoef,
|
||||
"test_matthews_corrcoef_ci_low": ci["matthews_corrcoef"][0],
|
||||
"test_matthews_corrcoef_ci_high": ci["matthews_corrcoef"][1],
|
||||
"test_brier_score": test_metrics.brier_score,
|
||||
"test_brier_score_ci_low": ci["brier_score"][0],
|
||||
"test_brier_score_ci_high": ci["brier_score"][1],
|
||||
"test_expected_calibration_error": test_metrics.expected_calibration_error,
|
||||
"test_expected_calibration_error_ci_low": ci["expected_calibration_error"][0],
|
||||
"test_expected_calibration_error_ci_high": ci["expected_calibration_error"][1],
|
||||
}
|
||||
)
|
||||
|
||||
metrics_df = pd.DataFrame(metrics_rows).sort_values(
|
||||
["task_name", "test_pr_auc", "test_recall_blast"],
|
||||
ascending=[True, False, False],
|
||||
)
|
||||
predictions_df = pd.DataFrame(prediction_rows)
|
||||
|
||||
if len(metrics_df) != len(TASK_FILES) * len(MODEL_NAMES) * len(FEATURE_GROUPS):
|
||||
raise ValueError("A6 did not produce the expected number of model runs.")
|
||||
if predictions_df.empty:
|
||||
raise ValueError("A6 predictions are empty.")
|
||||
|
||||
object_metrics_path = TABLES_ROOT / "object_level_metrics_baselines.csv"
|
||||
leaderboard_path = TABLES_ROOT / "baseline_model_leaderboard.csv"
|
||||
feature_ablation_path = TABLES_ROOT / "feature_group_ablation.csv"
|
||||
predictions_path = PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet"
|
||||
checkpoint_manifest_path = TABLES_ROOT / "checkpoint_manifest.csv"
|
||||
|
||||
metrics_df.to_csv(object_metrics_path, index=False)
|
||||
metrics_df.to_csv(leaderboard_path, index=False)
|
||||
metrics_df.to_csv(feature_ablation_path, index=False)
|
||||
predictions_df.to_parquet(predictions_path, index=False)
|
||||
pd.DataFrame(checkpoint_rows).to_csv(checkpoint_manifest_path, index=False)
|
||||
|
||||
save_pr_roc_summary(predictions_df, FIGURES_ROOT / "pr_curves_baselines.png", FIGURES_ROOT / "roc_curves_baselines.png")
|
||||
best_full_row = _select_best_row(metrics_df, task_name="a43_full_realistic_binary")
|
||||
best_test_predictions = predictions_df.loc[
|
||||
(predictions_df["run_id"] == best_full_row["run_id"]) & (predictions_df["split_name"] == "test")
|
||||
].copy()
|
||||
save_confusion_matrix(
|
||||
best_test_predictions["y_true"].to_numpy(),
|
||||
best_test_predictions["y_pred"].to_numpy(),
|
||||
FIGURES_ROOT / "confusion_matrix_best_baseline.png",
|
||||
title=f"Best baseline: {best_full_row['model_name']} | {best_full_row['feature_group']}",
|
||||
)
|
||||
|
||||
report_lines = [
|
||||
"# A6 Baseline QC",
|
||||
"",
|
||||
f"- Total baseline runs: `{len(metrics_df)}`",
|
||||
f"- Prediction rows saved: `{len(predictions_df)}`",
|
||||
f"- Leaderboard path: `{leaderboard_path}`",
|
||||
f"- Best full-realistic run: `{best_full_row['run_id']}`",
|
||||
]
|
||||
write_markdown(REPORTS_ROOT / "a6_baseline_qc.md", report_lines)
|
||||
|
||||
return {
|
||||
"object_level_metrics_csv": str(object_metrics_path),
|
||||
"feature_group_ablation_csv": str(feature_ablation_path),
|
||||
"object_level_predictions_parquet": str(predictions_path),
|
||||
"baseline_model_leaderboard_csv": str(leaderboard_path),
|
||||
"pr_curves_png": str(FIGURES_ROOT / "pr_curves_baselines.png"),
|
||||
"roc_curves_png": str(FIGURES_ROOT / "roc_curves_baselines.png"),
|
||||
"confusion_matrix_png": str(FIGURES_ROOT / "confusion_matrix_best_baseline.png"),
|
||||
"a6_qc_report_md": str(REPORTS_ROOT / "a6_baseline_qc.md"),
|
||||
}
|
||||
|
||||
|
||||
def run_a7(leaderboard_df: pd.DataFrame, full_task_df: pd.DataFrame, features_combined: pd.DataFrame) -> None:
|
||||
best_row = _select_best_row(leaderboard_df, task_name="a43_full_realistic_binary")
|
||||
feature_group = best_row["feature_group"]
|
||||
feature_columns = feature_columns_by_group(features_combined)[feature_group]
|
||||
|
||||
merged_df = merge_task_with_features(full_task_df, features_combined)
|
||||
train_df, val_df, test_df = _split_frame(merged_df)
|
||||
train_df = _cap_frame(train_df, max_rows=20000, random_state=42)
|
||||
val_df = _cap_frame(val_df, max_rows=5000, random_state=43)
|
||||
if len(train_df) > 10000:
|
||||
train_df = _cap_frame(train_df, max_rows=10000, random_state=44)
|
||||
scale_pos_weight = float((train_df["target_binary"].eq(0).sum()) / max(train_df["target_binary"].eq(1).sum(), 1))
|
||||
pipeline, spec = build_model(str(best_row["model_name"]), scale_pos_weight=scale_pos_weight, use_class_weight=True)
|
||||
X_train = train_df[feature_columns]
|
||||
y_train = train_df["target_binary"].astype(int)
|
||||
if len(test_df) > 2000:
|
||||
test_df = test_df.sample(n=2000, random_state=42)
|
||||
X_test = test_df[feature_columns]
|
||||
y_test = test_df["target_binary"].astype(int)
|
||||
pipeline.fit(X_train, y_train)
|
||||
|
||||
importance = permutation_importance(
|
||||
pipeline,
|
||||
X_test,
|
||||
y_test,
|
||||
n_repeats=3,
|
||||
random_state=42,
|
||||
n_jobs=1,
|
||||
scoring="average_precision",
|
||||
)
|
||||
stability_rows: list[dict[str, object]] = []
|
||||
for repeat_idx in range(3):
|
||||
sample_df = train_df.sample(frac=0.6, replace=True, random_state=42 + repeat_idx)
|
||||
sample_pipeline, _ = build_model(str(best_row["model_name"]), scale_pos_weight=scale_pos_weight, use_class_weight=True)
|
||||
sample_pipeline.fit(sample_df[feature_columns], sample_df["target_binary"].astype(int))
|
||||
sample_importance = permutation_importance(
|
||||
sample_pipeline,
|
||||
X_test,
|
||||
y_test,
|
||||
n_repeats=2,
|
||||
random_state=100 + repeat_idx,
|
||||
n_jobs=1,
|
||||
scoring="average_precision",
|
||||
)
|
||||
ranking = np.argsort(-sample_importance.importances_mean)[:20]
|
||||
for rank_position, index in enumerate(ranking, start=1):
|
||||
stability_rows.append(
|
||||
{
|
||||
"repeat_id": repeat_idx,
|
||||
"feature_name": feature_columns[index],
|
||||
"importance_mean": float(sample_importance.importances_mean[index]),
|
||||
"importance_std": float(sample_importance.importances_std[index]),
|
||||
"rank_position": rank_position,
|
||||
}
|
||||
)
|
||||
|
||||
stability_df = pd.DataFrame(stability_rows)
|
||||
top_features_df = (
|
||||
stability_df.groupby("feature_name")
|
||||
.agg(
|
||||
top20_frequency=("feature_name", "size"),
|
||||
mean_importance=("importance_mean", "mean"),
|
||||
best_rank=("rank_position", "min"),
|
||||
)
|
||||
.reset_index()
|
||||
.sort_values(["top20_frequency", "mean_importance"], ascending=[False, False])
|
||||
)
|
||||
top_features_path = TABLES_ROOT / "top_features_stability.csv"
|
||||
top_features_df.to_csv(top_features_path, index=False)
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
top_plot_df = top_features_df.head(20).iloc[::-1]
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
ax.barh(top_plot_df["feature_name"], top_plot_df["top20_frequency"], color="#4c72b0")
|
||||
ax.set_title("Feature stability across repeated importance runs")
|
||||
ax.set_xlabel("Top-20 frequency")
|
||||
fig.tight_layout()
|
||||
fig.savefig(FIGURES_ROOT / "feature_importance_stability.png", dpi=180)
|
||||
plt.close(fig)
|
||||
|
||||
shap_like_df = pd.DataFrame(
|
||||
{
|
||||
"feature_name": feature_columns,
|
||||
"importance_mean": importance.importances_mean,
|
||||
"importance_std": importance.importances_std,
|
||||
}
|
||||
).sort_values("importance_mean", ascending=False)
|
||||
top_shap_df = shap_like_df.head(20).iloc[::-1]
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
ax.barh(top_shap_df["feature_name"], top_shap_df["importance_mean"], xerr=top_shap_df["importance_std"], color="#55a868")
|
||||
ax.set_title("Top permutation-importance features for the best model")
|
||||
ax.set_xlabel("Importance")
|
||||
fig.tight_layout()
|
||||
fig.savefig(FIGURES_ROOT / "shap_summary_top_model.png", dpi=180)
|
||||
plt.close(fig)
|
||||
|
||||
notes_lines = [
|
||||
"# Feature Interpretation Notes",
|
||||
"",
|
||||
f"- Selected model: `{best_row['run_id']}`",
|
||||
f"- Implementation: `{spec.implementation}`",
|
||||
"",
|
||||
"## Stable top features",
|
||||
"",
|
||||
]
|
||||
for _, row in top_features_df.head(15).iterrows():
|
||||
notes_lines.append(
|
||||
f"- `{row['feature_name']}`: frequency `{int(row['top20_frequency'])}`, mean importance `{row['mean_importance']:.6f}`."
|
||||
)
|
||||
write_markdown(REPORTS_ROOT / "feature_interpretation_notes.md", notes_lines)
|
||||
|
||||
|
||||
def _evaluate_single_configuration(
|
||||
task_name: str,
|
||||
task_df: pd.DataFrame,
|
||||
feature_df: pd.DataFrame,
|
||||
feature_columns: list[str],
|
||||
model_name: str,
|
||||
use_class_weight: bool,
|
||||
label: str,
|
||||
return_prediction_df: bool = False,
|
||||
) -> dict[str, object] | tuple[dict[str, object], pd.DataFrame]:
|
||||
merged_df = merge_task_with_features(task_df, feature_df)
|
||||
train_df, val_df, test_df = _split_frame(merged_df)
|
||||
train_df = _cap_frame(train_df, max_rows=20000, random_state=42)
|
||||
val_df = _cap_frame(val_df, max_rows=5000, random_state=43)
|
||||
scale_pos_weight = float((train_df["target_binary"].eq(0).sum()) / max(train_df["target_binary"].eq(1).sum(), 1))
|
||||
pipeline, spec = build_model(model_name, scale_pos_weight=scale_pos_weight, use_class_weight=use_class_weight)
|
||||
val_score = fit_predict_probability(pipeline, train_df[feature_columns], train_df["target_binary"], val_df[feature_columns])
|
||||
threshold = optimize_threshold(val_df["target_binary"].to_numpy(), val_score)
|
||||
test_score = np.asarray(pipeline.predict_proba(test_df[feature_columns])[:, 1], dtype=float) if hasattr(pipeline, "predict_proba") else fit_predict_probability(pipeline, train_df[feature_columns], train_df["target_binary"], test_df[feature_columns])
|
||||
metrics = compute_binary_metrics(test_df["target_binary"].to_numpy(), test_score, threshold)
|
||||
result = {
|
||||
"task_name": task_name,
|
||||
"label": label,
|
||||
"model_name": model_name,
|
||||
"implementation": spec.implementation,
|
||||
"use_class_weight": use_class_weight,
|
||||
"n_rows": int(len(test_df)),
|
||||
"n_features": int(len(feature_columns)),
|
||||
"pr_auc": metrics.pr_auc,
|
||||
"roc_auc": metrics.roc_auc,
|
||||
"balanced_accuracy": metrics.balanced_accuracy,
|
||||
"macro_f1": metrics.macro_f1,
|
||||
"recall_blast": metrics.recall_blast,
|
||||
"specificity": metrics.specificity,
|
||||
"matthews_corrcoef": metrics.matthews_corrcoef,
|
||||
"brier_score": metrics.brier_score,
|
||||
"expected_calibration_error": metrics.expected_calibration_error,
|
||||
}
|
||||
if not return_prediction_df:
|
||||
return result
|
||||
|
||||
prediction_df = test_df[KEY_COLUMNS + ["split_name", "target_binary"]].copy()
|
||||
prediction_df["task_name"] = task_name
|
||||
prediction_df["label"] = label
|
||||
prediction_df["model_name"] = model_name
|
||||
prediction_df["implementation"] = spec.implementation
|
||||
prediction_df["use_class_weight"] = use_class_weight
|
||||
prediction_df["threshold"] = float(threshold)
|
||||
prediction_df["y_true"] = test_df["target_binary"].astype(int).to_numpy()
|
||||
prediction_df["y_score"] = test_score.astype(float)
|
||||
prediction_df["y_pred"] = (test_score >= threshold).astype(int)
|
||||
return result, prediction_df
|
||||
|
||||
|
||||
def run_a8_mask_usage(
|
||||
leaderboard_df: pd.DataFrame,
|
||||
tasks: dict[str, pd.DataFrame],
|
||||
features_combined: pd.DataFrame,
|
||||
features_bbox: pd.DataFrame,
|
||||
features_mask: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
best_full_row = _select_best_row(leaderboard_df, task_name="a43_full_realistic_binary")
|
||||
best_model_name = str(best_full_row["model_name"])
|
||||
combined_full_columns = feature_columns_by_group(features_combined)["full"]
|
||||
bbox_full_columns = [column for column in features_bbox.columns if column not in KEY_COLUMNS]
|
||||
mask_full_columns = [column for column in features_mask.columns if column not in KEY_COLUMNS]
|
||||
|
||||
mask_ablation_rows: list[dict[str, object]] = []
|
||||
mask_prediction_frames: list[pd.DataFrame] = []
|
||||
for task_name, task_df in tasks.items():
|
||||
mask_keys = set(
|
||||
tuple(row)
|
||||
for row in features_mask[KEY_COLUMNS[:3]].drop_duplicates().itertuples(index=False, name=None)
|
||||
)
|
||||
task_mask_ready = task_df.loc[
|
||||
task_df[KEY_COLUMNS[:3]].apply(tuple, axis=1).isin(mask_keys)
|
||||
].copy()
|
||||
for label, feature_df, feature_columns in [
|
||||
("bbox_only", features_bbox, bbox_full_columns),
|
||||
("mask_only", features_mask, mask_full_columns),
|
||||
("bbox_plus_mask", features_combined, combined_full_columns),
|
||||
]:
|
||||
metrics_row, prediction_df = _evaluate_single_configuration(
|
||||
task_name=task_name,
|
||||
task_df=task_mask_ready,
|
||||
feature_df=feature_df,
|
||||
feature_columns=feature_columns,
|
||||
model_name=best_model_name,
|
||||
use_class_weight=True,
|
||||
label=label,
|
||||
return_prediction_df=True,
|
||||
)
|
||||
mask_ablation_rows.append(metrics_row)
|
||||
mask_prediction_frames.append(prediction_df)
|
||||
mask_ablation_df = pd.DataFrame(mask_ablation_rows)
|
||||
mask_ablation_df.to_csv(TABLES_ROOT / "ablation_mask_usage.csv", index=False)
|
||||
if mask_prediction_frames:
|
||||
pd.concat(mask_prediction_frames, ignore_index=True).to_parquet(
|
||||
PREDICTIONS_ROOT / "h2_mask_usage_predictions.parquet",
|
||||
index=False,
|
||||
)
|
||||
return mask_ablation_df
|
||||
|
||||
|
||||
def run_a8_dataset_regimes(
|
||||
leaderboard_df: pd.DataFrame,
|
||||
tasks: dict[str, pd.DataFrame],
|
||||
features_combined: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
best_full_row = _select_best_row(leaderboard_df, task_name="a43_full_realistic_binary")
|
||||
best_model_name = str(best_full_row["model_name"])
|
||||
combined_full_columns = feature_columns_by_group(features_combined)["full"]
|
||||
|
||||
dataset_ablation_rows: list[dict[str, object]] = []
|
||||
for task_name, task_df in tasks.items():
|
||||
dataset_ablation_rows.append(
|
||||
_evaluate_single_configuration(
|
||||
task_name=task_name,
|
||||
task_df=task_df,
|
||||
feature_df=features_combined,
|
||||
feature_columns=combined_full_columns,
|
||||
model_name=best_model_name,
|
||||
use_class_weight=True,
|
||||
label="combined_with_class_weight",
|
||||
)
|
||||
)
|
||||
dataset_ablation_rows.append(
|
||||
_evaluate_single_configuration(
|
||||
task_name="a43_full_realistic_binary",
|
||||
task_df=tasks["a43_full_realistic_binary"],
|
||||
feature_df=features_combined,
|
||||
feature_columns=combined_full_columns,
|
||||
model_name=best_model_name,
|
||||
use_class_weight=False,
|
||||
label="combined_without_class_weight",
|
||||
)
|
||||
)
|
||||
top_features_df = pd.read_csv(TABLES_ROOT / "top_features_stability.csv").head(30)
|
||||
selected_columns = [column for column in top_features_df["feature_name"].tolist() if column in combined_full_columns]
|
||||
if not selected_columns:
|
||||
selected_columns = combined_full_columns[: min(30, len(combined_full_columns))]
|
||||
dataset_ablation_rows.append(
|
||||
_evaluate_single_configuration(
|
||||
task_name="a43_full_realistic_binary",
|
||||
task_df=tasks["a43_full_realistic_binary"],
|
||||
feature_df=features_combined,
|
||||
feature_columns=selected_columns,
|
||||
model_name=best_model_name,
|
||||
use_class_weight=True,
|
||||
label="combined_top30_features",
|
||||
)
|
||||
)
|
||||
dataset_ablation_df = pd.DataFrame(dataset_ablation_rows)
|
||||
dataset_ablation_df.to_csv(TABLES_ROOT / "ablation_dataset_regimes.csv", index=False)
|
||||
return dataset_ablation_df
|
||||
|
||||
|
||||
def save_a8_summary_plot(mask_ablation_df: pd.DataFrame, dataset_ablation_df: pd.DataFrame) -> str:
|
||||
ablation_plot_df = pd.concat(
|
||||
[
|
||||
mask_ablation_df.assign(ablation_family="mask_usage"),
|
||||
dataset_ablation_df.assign(ablation_family="dataset_regime"),
|
||||
],
|
||||
ignore_index=True,
|
||||
)
|
||||
save_bar_plot(
|
||||
ablation_plot_df[["label", "pr_auc"]].drop_duplicates(),
|
||||
x="label",
|
||||
y="pr_auc",
|
||||
hue=None,
|
||||
path=FIGURES_ROOT / "ablation_summary.png",
|
||||
title="A8 ablation summary (PR-AUC)",
|
||||
rotation=35,
|
||||
)
|
||||
return str(FIGURES_ROOT / "ablation_summary.png")
|
||||
|
||||
|
||||
def run_a8(
|
||||
leaderboard_df: pd.DataFrame,
|
||||
tasks: dict[str, pd.DataFrame],
|
||||
features_combined: pd.DataFrame,
|
||||
features_bbox: pd.DataFrame,
|
||||
features_mask: pd.DataFrame,
|
||||
) -> dict[str, str]:
|
||||
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,
|
||||
)
|
||||
dataset_ablation_df = run_a8_dataset_regimes(
|
||||
leaderboard_df=leaderboard_df,
|
||||
tasks=tasks,
|
||||
features_combined=features_combined,
|
||||
)
|
||||
plot_path = save_a8_summary_plot(mask_ablation_df, dataset_ablation_df)
|
||||
|
||||
return {
|
||||
"ablation_mask_usage_csv": str(TABLES_ROOT / "ablation_mask_usage.csv"),
|
||||
"ablation_dataset_regimes_csv": str(TABLES_ROOT / "ablation_dataset_regimes.csv"),
|
||||
"ablation_summary_png": plot_path,
|
||||
}
|
||||
|
||||
|
||||
def run_a6_a8() -> dict[str, dict[str, str]]:
|
||||
result_a6 = run_a6()
|
||||
features_combined, features_bbox, features_mask, tasks = _load_required_artifacts()
|
||||
leaderboard_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
|
||||
run_a7(leaderboard_df, tasks["a43_full_realistic_binary"], features_combined)
|
||||
result_a8 = run_a8(leaderboard_df, tasks, features_combined, features_bbox, features_mask)
|
||||
return {"a6": result_a6, "a8": result_a8}
|
||||
|
||||
|
||||
def run_a10_a11() -> dict[str, str]:
|
||||
leaderboard_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
|
||||
predictions_df = pd.read_parquet(PREDICTIONS_ROOT / "object_level_baseline_predictions.parquet")
|
||||
best_full_row = _select_best_row(leaderboard_df, task_name="a43_full_realistic_binary")
|
||||
best_predictions = predictions_df.loc[
|
||||
(predictions_df["run_id"] == best_full_row["run_id"]) & (predictions_df["split_name"] == "test")
|
||||
].copy()
|
||||
if best_predictions.empty:
|
||||
raise ValueError("No object-level predictions found for A10.")
|
||||
|
||||
image_score_df = aggregate_object_scores(
|
||||
best_predictions.assign(image_contains_blast=best_predictions["y_true"]),
|
||||
threshold=float(best_full_row["threshold"]),
|
||||
group_columns=["patient_id", "image_id"],
|
||||
target_column="image_contains_blast",
|
||||
)
|
||||
patient_score_df = aggregate_object_scores(
|
||||
best_predictions.assign(patient_contains_blast=best_predictions["y_true"]),
|
||||
threshold=float(best_full_row["threshold"]),
|
||||
group_columns=["patient_id"],
|
||||
target_column="patient_contains_blast",
|
||||
)
|
||||
image_metrics_df = compute_aggregate_metrics(image_score_df, target_column="image_contains_blast")
|
||||
patient_metrics_df = compute_aggregate_metrics(patient_score_df, target_column="patient_contains_blast")
|
||||
|
||||
image_scores_path = PREDICTIONS_ROOT / "image_level_scores.parquet"
|
||||
patient_scores_path = PREDICTIONS_ROOT / "patient_level_scores.parquet"
|
||||
image_metrics_path = TABLES_ROOT / "image_level_metrics.csv"
|
||||
patient_metrics_path = TABLES_ROOT / "patient_level_ranking_metrics.csv"
|
||||
image_score_df.to_parquet(image_scores_path, index=False)
|
||||
patient_score_df.to_parquet(patient_scores_path, index=False)
|
||||
image_metrics_df.to_csv(image_metrics_path, index=False)
|
||||
patient_metrics_df.to_csv(patient_metrics_path, index=False)
|
||||
|
||||
save_ranking_curve(
|
||||
image_score_df,
|
||||
target_column="image_contains_blast",
|
||||
path=FIGURES_ROOT / "image_ranking_recall_curve.png",
|
||||
title="Image-level ranking recall",
|
||||
)
|
||||
save_ranking_curve(
|
||||
patient_score_df,
|
||||
target_column="patient_contains_blast",
|
||||
path=FIGURES_ROOT / "patient_ranking_curve.png",
|
||||
title="Patient-level ranking recall",
|
||||
)
|
||||
|
||||
subset_summary_df = pd.read_csv(TABLES_ROOT / "subset_summary.csv").assign(section="subset_summary")
|
||||
split_summary_df = pd.read_csv(TABLES_ROOT / "split_summary.csv").assign(section="split_summary")
|
||||
task_prevalence_df = pd.read_csv(TABLES_ROOT / "task_prevalence_summary.csv").assign(section="task_prevalence")
|
||||
dataset_summary_df = pd.concat(
|
||||
[
|
||||
subset_summary_df.rename(columns={"subset_name": "name"}),
|
||||
split_summary_df.rename(columns={"split_name": "name"}),
|
||||
task_prevalence_df.rename(columns={"task_name": "name"}),
|
||||
],
|
||||
ignore_index=True,
|
||||
sort=False,
|
||||
)
|
||||
dataset_summary_df.to_csv(TABLES_ROOT / "dataset_summary.csv", index=False)
|
||||
|
||||
save_pipeline_overview(FIGURES_ROOT / "pipeline_overview.png")
|
||||
roi_manifest = pd.read_parquet(CROPS_ROOT / "roi_manifest.parquet")
|
||||
example_rows = roi_manifest.loc[roi_manifest["mask_crop_path"].notna()].head(4)
|
||||
save_example_rois(
|
||||
[Path(path) for path in example_rows["bbox_crop_path"].tolist()],
|
||||
[Path(path) for path in example_rows["mask_crop_path"].tolist()],
|
||||
FIGURES_ROOT / "example_rois.png",
|
||||
)
|
||||
|
||||
mask_plot_df = subset_summary_df[["subset_name", "mask_ready_ratio"]].copy()
|
||||
save_bar_plot(
|
||||
mask_plot_df,
|
||||
x="subset_name",
|
||||
y="mask_ready_ratio",
|
||||
hue=None,
|
||||
path=FIGURES_ROOT / "mask_coverage.png",
|
||||
title="Mask coverage by subset",
|
||||
)
|
||||
|
||||
figure_manifest = pd.DataFrame(
|
||||
[
|
||||
{"figure_name": "pipeline_overview", "path": str(FIGURES_ROOT / "pipeline_overview.png"), "stage": "A11"},
|
||||
{"figure_name": "example_rois", "path": str(FIGURES_ROOT / "example_rois.png"), "stage": "A11"},
|
||||
{"figure_name": "mask_coverage", "path": str(FIGURES_ROOT / "mask_coverage.png"), "stage": "A11"},
|
||||
{"figure_name": "pr_curves_baselines", "path": str(FIGURES_ROOT / "pr_curves_baselines.png"), "stage": "A6"},
|
||||
{"figure_name": "roc_curves_baselines", "path": str(FIGURES_ROOT / "roc_curves_baselines.png"), "stage": "A6"},
|
||||
{"figure_name": "feature_importance_stability", "path": str(FIGURES_ROOT / "feature_importance_stability.png"), "stage": "A7"},
|
||||
{"figure_name": "ablation_summary", "path": str(FIGURES_ROOT / "ablation_summary.png"), "stage": "A8"},
|
||||
{"figure_name": "image_ranking_recall_curve", "path": str(FIGURES_ROOT / "image_ranking_recall_curve.png"), "stage": "A10"},
|
||||
{"figure_name": "patient_ranking_curve", "path": str(FIGURES_ROOT / "patient_ranking_curve.png"), "stage": "A10"},
|
||||
]
|
||||
)
|
||||
figure_manifest.to_csv(TABLES_ROOT / "figure_manifest.csv", index=False)
|
||||
|
||||
table_manifest = pd.DataFrame(
|
||||
[
|
||||
{"table_name": path.stem, "path": str(path)}
|
||||
for path in sorted(TABLES_ROOT.glob("*.csv"))
|
||||
]
|
||||
)
|
||||
table_manifest.to_csv(TABLES_ROOT / "table_manifest.csv", index=False)
|
||||
|
||||
report_lines = [
|
||||
"# A10-A11 QC",
|
||||
"",
|
||||
f"- Best full-realistic baseline: `{best_full_row['run_id']}`",
|
||||
f"- Image-level rows: `{len(image_score_df)}`",
|
||||
f"- Patient-level rows: `{len(patient_score_df)}`",
|
||||
f"- Figure manifest rows: `{len(figure_manifest)}`",
|
||||
f"- Table manifest rows: `{len(table_manifest)}`",
|
||||
]
|
||||
write_markdown(REPORTS_ROOT / "a10_a11_qc.md", report_lines)
|
||||
|
||||
return {
|
||||
"image_level_metrics_csv": str(image_metrics_path),
|
||||
"patient_level_ranking_metrics_csv": str(patient_metrics_path),
|
||||
"image_level_scores_parquet": str(image_scores_path),
|
||||
"patient_level_scores_parquet": str(patient_scores_path),
|
||||
"figure_manifest_csv": str(TABLES_ROOT / "figure_manifest.csv"),
|
||||
"table_manifest_csv": str(TABLES_ROOT / "table_manifest.csv"),
|
||||
"a10_a11_qc_md": str(REPORTS_ROOT / "a10_a11_qc.md"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result_a6_a8 = run_a6_a8()
|
||||
result_a10_a11 = run_a10_a11()
|
||||
print(json.dumps({"a6_a8": result_a6_a8, "a10_a11": result_a10_a11}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,295 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MPLCONFIGDIR = Path(__file__).resolve().parents[1] / "outputs" / "logs" / "matplotlib"
|
||||
MPLCONFIGDIR.mkdir(parents=True, exist_ok=True)
|
||||
os.environ.setdefault("MPLCONFIGDIR", str(MPLCONFIGDIR))
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = PROJECT_ROOT / "src"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
from models.baseline_pipeline import KEY_COLUMNS
|
||||
from models.deep_baselines import save_training_history, train_deep_baseline
|
||||
from utils.project import CHECKPOINTS_ROOT, FEATURES_ROOT, FIGURES_ROOT, PREDICTIONS_ROOT, REPORTS_ROOT, TABLES_ROOT, ensure_output_layout, write_markdown
|
||||
|
||||
|
||||
def _cap_frame(frame: pd.DataFrame, max_rows: int, random_state: int) -> pd.DataFrame:
|
||||
if len(frame) <= max_rows:
|
||||
return frame
|
||||
sampled_groups: list[pd.DataFrame] = []
|
||||
remaining = max_rows
|
||||
groups = list(frame.groupby("target_binary", sort=True))
|
||||
for index, (_, group) in enumerate(groups):
|
||||
if index == len(groups) - 1:
|
||||
n_take = min(len(group), remaining)
|
||||
else:
|
||||
n_take = max(1, int(round(max_rows * len(group) / len(frame))))
|
||||
n_take = min(len(group), n_take)
|
||||
remaining -= n_take
|
||||
sampled_groups.append(group.sample(n=n_take, random_state=random_state))
|
||||
return pd.concat(sampled_groups, ignore_index=True)
|
||||
|
||||
|
||||
def _split_frame(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
||||
return (
|
||||
df.loc[df["split_name"] == "train"].copy(),
|
||||
df.loc[df["split_name"] == "val"].copy(),
|
||||
df.loc[df["split_name"] == "test"].copy(),
|
||||
)
|
||||
|
||||
|
||||
def _prepare_tabular_splits(
|
||||
train_df: pd.DataFrame,
|
||||
val_df: pd.DataFrame,
|
||||
test_df: pd.DataFrame,
|
||||
feature_columns: list[str],
|
||||
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
||||
if not feature_columns:
|
||||
return train_df, val_df, test_df
|
||||
|
||||
train_df = train_df.copy()
|
||||
val_df = val_df.copy()
|
||||
test_df = test_df.copy()
|
||||
|
||||
for frame in [train_df, val_df, test_df]:
|
||||
for column in feature_columns:
|
||||
frame[column] = pd.to_numeric(frame[column], errors="coerce")
|
||||
|
||||
medians = train_df[feature_columns].replace([float("inf"), float("-inf")], pd.NA).median()
|
||||
train_df[feature_columns] = train_df[feature_columns].replace([float("inf"), float("-inf")], pd.NA).fillna(medians)
|
||||
val_df[feature_columns] = val_df[feature_columns].replace([float("inf"), float("-inf")], pd.NA).fillna(medians)
|
||||
test_df[feature_columns] = test_df[feature_columns].replace([float("inf"), float("-inf")], pd.NA).fillna(medians)
|
||||
|
||||
means = train_df[feature_columns].mean()
|
||||
stds = train_df[feature_columns].std().replace(0.0, 1.0).fillna(1.0)
|
||||
train_df[feature_columns] = (train_df[feature_columns] - means) / stds
|
||||
val_df[feature_columns] = (val_df[feature_columns] - means) / stds
|
||||
test_df[feature_columns] = (test_df[feature_columns] - means) / stds
|
||||
|
||||
return train_df, val_df, test_df
|
||||
|
||||
|
||||
def _comparison_row_from_handcrafted(best_handcrafted: pd.Series) -> dict[str, object]:
|
||||
return {
|
||||
"model_name": "handcrafted_best_full_realistic",
|
||||
"source_type": "handcrafted",
|
||||
"reference_run_id": str(best_handcrafted["run_id"]),
|
||||
"reference_model_name": str(best_handcrafted["model_name"]),
|
||||
"implementation": str(best_handcrafted["implementation"]),
|
||||
"feature_group": str(best_handcrafted["feature_group"]),
|
||||
"validation_pr_auc": float(best_handcrafted["validation_pr_auc"]),
|
||||
"test_pr_auc": float(best_handcrafted["test_pr_auc"]),
|
||||
"test_roc_auc": float(best_handcrafted["test_roc_auc"]),
|
||||
"test_balanced_accuracy": float(best_handcrafted["test_balanced_accuracy"]),
|
||||
"test_macro_f1": float(best_handcrafted["test_macro_f1"]),
|
||||
"test_recall_blast": float(best_handcrafted["test_recall_blast"]),
|
||||
"test_specificity": float(best_handcrafted["test_specificity"]),
|
||||
"test_matthews_corrcoef": float(best_handcrafted["test_matthews_corrcoef"]),
|
||||
"test_brier_score": float(best_handcrafted["test_brier_score"]),
|
||||
"test_expected_calibration_error": float(best_handcrafted["test_expected_calibration_error"]),
|
||||
}
|
||||
|
||||
|
||||
def _comparison_row_from_deep(model_name: str, result) -> dict[str, object]:
|
||||
return {
|
||||
"model_name": model_name,
|
||||
"source_type": "deep",
|
||||
"reference_run_id": None,
|
||||
"reference_model_name": model_name,
|
||||
"implementation": "small_cnn" if model_name != "cnn_late_fusion" else "small_cnn_late_fusion",
|
||||
"feature_group": "image_only" if model_name != "cnn_late_fusion" else "image_plus_tabular",
|
||||
"validation_pr_auc": max((float(epoch["val_pr_auc"]) for epoch in result.history), default=float("nan")),
|
||||
"test_pr_auc": float(result.test_metrics["pr_auc"]),
|
||||
"test_roc_auc": float(result.test_metrics["roc_auc"]),
|
||||
"test_balanced_accuracy": float(result.test_metrics["balanced_accuracy"]),
|
||||
"test_macro_f1": float(result.test_metrics["macro_f1"]),
|
||||
"test_recall_blast": float(result.test_metrics["recall_blast"]),
|
||||
"test_specificity": float(result.test_metrics["specificity"]),
|
||||
"test_matthews_corrcoef": float(result.test_metrics["matthews_corrcoef"]),
|
||||
"test_brier_score": float(result.test_metrics["brier_score"]),
|
||||
"test_expected_calibration_error": float(result.test_metrics["expected_calibration_error"]),
|
||||
}
|
||||
|
||||
|
||||
def run_a9_deep_baselines() -> dict[str, str]:
|
||||
ensure_output_layout()
|
||||
task_path = PROJECT_ROOT / "outputs" / "dataset_index" / "task_a43_full_realistic_modeling_workset.parquet"
|
||||
if not task_path.exists():
|
||||
task_path = PROJECT_ROOT / "outputs" / "dataset_index" / "task_a43_full_realistic_binary.parquet"
|
||||
roi_path = PROJECT_ROOT / "outputs" / "crops" / "roi_manifest_modeling_workset.parquet"
|
||||
if not roi_path.exists():
|
||||
roi_path = PROJECT_ROOT / "outputs" / "crops" / "roi_manifest.parquet"
|
||||
task_df = pd.read_parquet(task_path)
|
||||
roi_df = pd.read_parquet(roi_path)
|
||||
feature_path = FEATURES_ROOT / "features_combined.parquet"
|
||||
if not feature_path.exists():
|
||||
feature_path = FEATURES_ROOT / "features_combined_fast.parquet"
|
||||
if not feature_path.exists():
|
||||
feature_path = FEATURES_ROOT / "features_combined_lite.parquet"
|
||||
feature_df = pd.read_parquet(feature_path)
|
||||
handcrafted_df = pd.read_csv(TABLES_ROOT / "baseline_model_leaderboard.csv")
|
||||
|
||||
merged_roi = task_df.merge(
|
||||
roi_df[KEY_COLUMNS + ["bbox_crop_path", "mask_crop_path"]].copy(),
|
||||
on=KEY_COLUMNS,
|
||||
how="inner",
|
||||
validate="one_to_one",
|
||||
)
|
||||
mask_ready_df = merged_roi.loc[merged_roi["mask_crop_path"].notna()].copy()
|
||||
fusion_df = mask_ready_df.merge(feature_df, on=KEY_COLUMNS, how="inner", validate="one_to_one")
|
||||
mask_ready_df = mask_ready_df.loc[mask_ready_df["target_binary"].notna()].copy()
|
||||
fusion_df = fusion_df.loc[fusion_df["target_binary"].notna()].copy()
|
||||
tabular_columns = [
|
||||
column
|
||||
for column in feature_df.columns
|
||||
if column not in KEY_COLUMNS and pd.api.types.is_numeric_dtype(feature_df[column])
|
||||
]
|
||||
|
||||
bbox_train, bbox_val, bbox_test = _split_frame(mask_ready_df)
|
||||
fusion_train, fusion_val, fusion_test = _split_frame(fusion_df)
|
||||
bbox_train = _cap_frame(bbox_train, max_rows=5000, random_state=42)
|
||||
bbox_val = _cap_frame(bbox_val, max_rows=1000, random_state=43)
|
||||
fusion_train = _cap_frame(fusion_train, max_rows=5000, random_state=42)
|
||||
fusion_val = _cap_frame(fusion_val, max_rows=1000, random_state=43)
|
||||
fusion_train, fusion_val, fusion_test = _prepare_tabular_splits(
|
||||
fusion_train,
|
||||
fusion_val,
|
||||
fusion_test,
|
||||
tabular_columns,
|
||||
)
|
||||
|
||||
bbox_result = train_deep_baseline(
|
||||
bbox_train,
|
||||
bbox_val,
|
||||
bbox_test,
|
||||
image_column="bbox_crop_path",
|
||||
feature_columns=None,
|
||||
checkpoint_path=CHECKPOINTS_ROOT / "cnn_bbox_best.pt",
|
||||
max_epochs=6,
|
||||
patience=2,
|
||||
)
|
||||
mask_result = train_deep_baseline(
|
||||
bbox_train,
|
||||
bbox_val,
|
||||
bbox_test,
|
||||
image_column="mask_crop_path",
|
||||
feature_columns=None,
|
||||
checkpoint_path=CHECKPOINTS_ROOT / "cnn_mask_best.pt",
|
||||
max_epochs=6,
|
||||
patience=2,
|
||||
)
|
||||
fusion_result = train_deep_baseline(
|
||||
fusion_train,
|
||||
fusion_val,
|
||||
fusion_test,
|
||||
image_column="bbox_crop_path",
|
||||
feature_columns=tabular_columns,
|
||||
checkpoint_path=CHECKPOINTS_ROOT / "cnn_late_fusion_best.pt",
|
||||
max_epochs=6,
|
||||
patience=2,
|
||||
)
|
||||
|
||||
best_handcrafted = handcrafted_df.loc[
|
||||
handcrafted_df["task_name"] == "a43_full_realistic_binary"
|
||||
].sort_values(["validation_pr_auc", "test_pr_auc"], ascending=[False, False]).iloc[0]
|
||||
|
||||
comparison_rows = [
|
||||
_comparison_row_from_handcrafted(best_handcrafted),
|
||||
_comparison_row_from_deep("cnn_bbox", bbox_result),
|
||||
_comparison_row_from_deep("cnn_mask", mask_result),
|
||||
_comparison_row_from_deep("cnn_late_fusion", fusion_result),
|
||||
]
|
||||
deep_vs_path = TABLES_ROOT / "deep_vs_handcrafted.csv"
|
||||
pd.DataFrame(comparison_rows).to_csv(deep_vs_path, index=False)
|
||||
|
||||
prediction_rows = []
|
||||
for model_name, result in [
|
||||
("cnn_bbox", bbox_result),
|
||||
("cnn_mask", mask_result),
|
||||
("cnn_late_fusion", fusion_result),
|
||||
]:
|
||||
frame = result.test_predictions[KEY_COLUMNS + ["split_name", "target_binary", "y_true", "y_score", "y_pred"]].copy()
|
||||
frame["model_name"] = model_name
|
||||
prediction_rows.append(frame)
|
||||
deep_predictions_path = PREDICTIONS_ROOT / "object_level_deep_predictions.parquet"
|
||||
pd.concat(prediction_rows, ignore_index=True).to_parquet(deep_predictions_path, index=False)
|
||||
|
||||
history_payload = {
|
||||
"cnn_bbox": bbox_result.history,
|
||||
"cnn_mask": mask_result.history,
|
||||
"cnn_late_fusion": fusion_result.history,
|
||||
}
|
||||
history_path = PROJECT_ROOT / "outputs" / "logs" / "cnn_training_history.json"
|
||||
save_training_history(history_payload, history_path)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
||||
for model_name, result in [
|
||||
("cnn_bbox", bbox_result),
|
||||
("cnn_mask", mask_result),
|
||||
("cnn_late_fusion", fusion_result),
|
||||
]:
|
||||
history_df = pd.DataFrame(result.history)
|
||||
axes[0].plot(history_df["epoch"], history_df["train_loss"], label=f"{model_name} train")
|
||||
axes[0].plot(history_df["epoch"], history_df["val_loss"], linestyle="--", label=f"{model_name} val")
|
||||
axes[1].plot(history_df["epoch"], history_df["val_pr_auc"], label=model_name)
|
||||
axes[0].set_title("Deep training loss")
|
||||
axes[1].set_title("Validation PR-AUC")
|
||||
for axis in axes:
|
||||
axis.set_xlabel("Epoch")
|
||||
axis.legend(fontsize=8)
|
||||
fig.tight_layout()
|
||||
training_curves_path = FIGURES_ROOT / "deep_training_curves.png"
|
||||
fig.savefig(training_curves_path, dpi=180)
|
||||
plt.close(fig)
|
||||
|
||||
report_lines = [
|
||||
"# A9 Deep Baseline QC",
|
||||
"",
|
||||
f"- Task source: `{task_path}`",
|
||||
f"- ROI source: `{roi_path}`",
|
||||
f"- Device: `CPU fallback`",
|
||||
f"- Mask-ready full-realistic rows: `{len(mask_ready_df)}`",
|
||||
f"- Fusion rows: `{len(fusion_df)}`",
|
||||
f"- BBox checkpoint: `{bbox_result.model_path}`",
|
||||
f"- Mask checkpoint: `{mask_result.model_path}`",
|
||||
f"- Fusion checkpoint: `{fusion_result.model_path}`",
|
||||
f"- Deep comparison table: `{deep_vs_path}`",
|
||||
f"- Best handcrafted reference run: `{best_handcrafted['run_id']}`",
|
||||
]
|
||||
write_markdown(REPORTS_ROOT / "a9_deep_qc.md", report_lines)
|
||||
|
||||
return {
|
||||
"deep_vs_handcrafted_csv": str(deep_vs_path),
|
||||
"deep_predictions_parquet": str(deep_predictions_path),
|
||||
"bbox_checkpoint": bbox_result.model_path,
|
||||
"mask_checkpoint": mask_result.model_path,
|
||||
"fusion_checkpoint": fusion_result.model_path,
|
||||
"history_json": str(history_path),
|
||||
"training_curves_png": str(training_curves_path),
|
||||
"a9_qc_md": str(REPORTS_ROOT / "a9_deep_qc.md"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(json.dumps(run_a9_deep_baselines(), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from reproducibility_blocks import print_payload, run_all_hypotheses
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run all E01 reproducibility blocks.")
|
||||
parser.add_argument("--data-root", type=Path, default=None, help="Absolute path to the original dataset `data`.")
|
||||
parser.add_argument("--masks-root", type=Path, default=None, help="Absolute path to the masks dataset `data_masks`.")
|
||||
parser.add_argument(
|
||||
"--paths-config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional JSON config with `data_root` and `masks_root`.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Recompute all reproducibility blocks even if cached outputs already exist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--with-subject-figures",
|
||||
action="store_true",
|
||||
help="Render subject-oriented H1-H5 figures after the hypothesis blocks.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--subject-figure-variants",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of candidate subject figures to render per hypothesis.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
print_payload(
|
||||
run_all_hypotheses(
|
||||
data_root=args.data_root,
|
||||
masks_root=args.masks_root,
|
||||
config_path=args.paths_config,
|
||||
force=args.force,
|
||||
with_subject_figures=args.with_subject_figures,
|
||||
subject_figure_variants=args.subject_figure_variants,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from reproducibility_blocks import print_payload, run_foundation
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run E01 foundation block (A0-A5).")
|
||||
parser.add_argument("--data-root", type=Path, default=None, help="Absolute path to the original dataset `data`.")
|
||||
parser.add_argument("--masks-root", type=Path, default=None, help="Absolute path to the masks dataset `data_masks`.")
|
||||
parser.add_argument(
|
||||
"--paths-config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional JSON config with `data_root` and `masks_root`.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Recompute foundation artifacts even if cached outputs already exist.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
print_payload(
|
||||
run_foundation(
|
||||
data_root=args.data_root,
|
||||
masks_root=args.masks_root,
|
||||
config_path=args.paths_config,
|
||||
force=args.force,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reproducibility_blocks import print_payload, run_h1_interpretable_features
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_payload(run_h1_interpretable_features())
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reproducibility_blocks import print_payload, run_h2_mask_effect
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_payload(run_h2_mask_effect())
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reproducibility_blocks import print_payload, run_h3_noise_regimes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_payload(run_h3_noise_regimes())
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reproducibility_blocks import print_payload, run_h4_aggregation
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_payload(run_h4_aggregation())
|
||||
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reproducibility_blocks import print_payload, run_h5_deep_vs_handcrafted
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_payload(run_h5_deep_vs_handcrafted())
|
||||
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_ROOT = PROJECT_ROOT / "src"
|
||||
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, VENDOR_ROOT]:
|
||||
candidate_str = str(candidate)
|
||||
if candidate.exists() and candidate_str not in sys.path:
|
||||
sys.path.append(candidate_str)
|
||||
|
||||
from evaluation.subject_figure_rendering import render_subject_figure
|
||||
from evaluation.subject_figure_selection import build_subject_figure_specs, flatten_subject_specs
|
||||
from utils.project import (
|
||||
A01_ARTICLE_SUBJECT_MANIFEST_PATH,
|
||||
A01_FIGURES_ROOT,
|
||||
A01_SUBJECT_FIGURE_CASES_PATH,
|
||||
A02_ARTICLE_SUBJECT_MANIFEST_PATH,
|
||||
A02_FIGURES_ROOT,
|
||||
A02_SUBJECT_FIGURE_CASES_PATH,
|
||||
EXPERIMENT_EXECUTION_LOG_PATH,
|
||||
FIGURES_ROOT,
|
||||
SUBJECT_FIGURE_CASES_PATH,
|
||||
SUBJECT_FIGURE_MANIFEST_PATH,
|
||||
SUBJECT_FIGURES_SUMMARY_JSON,
|
||||
SUBJECT_FIGURES_SUMMARY_MD,
|
||||
ensure_dir,
|
||||
local_date_tag,
|
||||
write_json,
|
||||
write_markdown,
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Render subject-oriented article figures for A01 and A02.")
|
||||
parser.add_argument("--variants", type=int, default=5, help="Number of candidate variants to render for each required figure.")
|
||||
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing output files for the same date tag.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _output_filename(spec: dict[str, object], date_tag: str) -> str:
|
||||
return (
|
||||
f"{spec['figure_basename']}__{spec['hypothesis']}_{spec['hypothesis_slug']}__"
|
||||
f"{date_tag}__v{int(spec['variant_index']):02d}.png"
|
||||
)
|
||||
|
||||
|
||||
def _article_figure_root(article_id: str) -> Path:
|
||||
if article_id == "A01":
|
||||
return A01_FIGURES_ROOT
|
||||
if article_id == "A02":
|
||||
return A02_FIGURES_ROOT
|
||||
raise ValueError(f"Unsupported article id: {article_id}")
|
||||
|
||||
|
||||
def _article_manifest_path(article_id: str) -> Path:
|
||||
if article_id == "A01":
|
||||
return A01_ARTICLE_SUBJECT_MANIFEST_PATH
|
||||
if article_id == "A02":
|
||||
return A02_ARTICLE_SUBJECT_MANIFEST_PATH
|
||||
raise ValueError(f"Unsupported article id: {article_id}")
|
||||
|
||||
|
||||
def _article_cases_path(article_id: str) -> Path:
|
||||
if article_id == "A01":
|
||||
return A01_SUBJECT_FIGURE_CASES_PATH
|
||||
if article_id == "A02":
|
||||
return A02_SUBJECT_FIGURE_CASES_PATH
|
||||
raise ValueError(f"Unsupported article id: {article_id}")
|
||||
|
||||
|
||||
def _save_article_manifests(cases_df: pd.DataFrame, manifest_df: pd.DataFrame) -> dict[str, str]:
|
||||
article_manifest_paths: dict[str, str] = {}
|
||||
merged = cases_df.merge(
|
||||
manifest_df[
|
||||
[
|
||||
"article_id",
|
||||
"figure_id",
|
||||
"figure_variant_id",
|
||||
"figure_basename",
|
||||
"variant_index",
|
||||
"hypothesis",
|
||||
"hypothesis_slug",
|
||||
"output_path",
|
||||
"article_figure_path",
|
||||
]
|
||||
],
|
||||
on=["article_id", "figure_id", "figure_variant_id", "figure_basename", "variant_index", "hypothesis", "hypothesis_slug"],
|
||||
how="left",
|
||||
validate="many_to_one",
|
||||
)
|
||||
ordered_columns = [
|
||||
"figure_id",
|
||||
"panel_id",
|
||||
"patient_id",
|
||||
"image_id",
|
||||
"object_id_within_image",
|
||||
"class_name",
|
||||
"target_binary",
|
||||
"split_name",
|
||||
"source_table",
|
||||
"source_path",
|
||||
"notes",
|
||||
"article_id",
|
||||
"figure_variant_id",
|
||||
"figure_basename",
|
||||
"variant_index",
|
||||
"hypothesis",
|
||||
"hypothesis_slug",
|
||||
"output_path",
|
||||
"article_figure_path",
|
||||
"role",
|
||||
"figure_kind",
|
||||
"title",
|
||||
]
|
||||
for article_id in ["A01", "A02"]:
|
||||
article_df = merged.loc[merged["article_id"].eq(article_id)].copy()
|
||||
article_df = article_df[[column for column in ordered_columns if column in article_df.columns]]
|
||||
manifest_path = _article_manifest_path(article_id)
|
||||
ensure_dir(manifest_path.parent)
|
||||
article_df.to_csv(manifest_path, index=False)
|
||||
article_manifest_paths[article_id] = str(manifest_path)
|
||||
return article_manifest_paths
|
||||
|
||||
|
||||
def run_subject_figures(variants: int = 5, overwrite: bool = False) -> dict[str, object]:
|
||||
specs = build_subject_figure_specs(variant_count=variants)
|
||||
date_tag = local_date_tag()
|
||||
rendered_paths: list[str] = []
|
||||
article_paths: list[str] = []
|
||||
manifest_rows: list[dict[str, object]] = []
|
||||
|
||||
for spec in specs:
|
||||
filename = _output_filename(spec, date_tag)
|
||||
output_path = FIGURES_ROOT / filename
|
||||
article_path = _article_figure_root(str(spec["article_id"])) / filename
|
||||
|
||||
if output_path.exists() and not overwrite:
|
||||
rendered_path = str(output_path)
|
||||
else:
|
||||
rendered_path = render_subject_figure(spec, output_path)
|
||||
ensure_dir(article_path.parent)
|
||||
shutil.copy2(output_path, article_path)
|
||||
|
||||
rendered_paths.append(rendered_path)
|
||||
article_paths.append(str(article_path))
|
||||
manifest_rows.append(
|
||||
{
|
||||
"article_id": spec["article_id"],
|
||||
"figure_id": spec["figure_id"],
|
||||
"figure_variant_id": spec["figure_variant_id"],
|
||||
"figure_basename": spec["figure_basename"],
|
||||
"variant_index": spec["variant_index"],
|
||||
"figure_kind": spec["figure_kind"],
|
||||
"hypothesis": spec["hypothesis"],
|
||||
"hypothesis_slug": spec["hypothesis_slug"],
|
||||
"date_tag": date_tag,
|
||||
"output_path": rendered_path,
|
||||
"article_figure_path": str(article_path),
|
||||
"title": spec["title"],
|
||||
}
|
||||
)
|
||||
|
||||
cases_df = flatten_subject_specs(specs)
|
||||
ensure_dir(SUBJECT_FIGURE_CASES_PATH.parent)
|
||||
cases_df.to_csv(SUBJECT_FIGURE_CASES_PATH, index=False)
|
||||
for article_id in ["A01", "A02"]:
|
||||
cases_df.loc[cases_df["article_id"].eq(article_id)].to_csv(_article_cases_path(article_id), index=False)
|
||||
|
||||
manifest_df = pd.DataFrame(manifest_rows)
|
||||
manifest_df.to_csv(SUBJECT_FIGURE_MANIFEST_PATH, index=False)
|
||||
article_manifest_paths = _save_article_manifests(cases_df, manifest_df)
|
||||
|
||||
counts_by_article = manifest_df.groupby("article_id").size().to_dict()
|
||||
counts_by_figure = manifest_df.groupby("figure_id").size().to_dict()
|
||||
counts_by_hypothesis = manifest_df.groupby("hypothesis").size().to_dict()
|
||||
|
||||
lines = [
|
||||
"# Subject Figures Summary",
|
||||
"",
|
||||
f"Generated at date tag: `{date_tag}`",
|
||||
"",
|
||||
"## Counts by article",
|
||||
"",
|
||||
]
|
||||
for article_id in sorted(counts_by_article):
|
||||
lines.append(f"- `{article_id}`: `{counts_by_article[article_id]}` PNG files.")
|
||||
lines.extend(["", "## Counts by figure", ""])
|
||||
for figure_id in sorted(counts_by_figure):
|
||||
lines.append(f"- `{figure_id}`: `{counts_by_figure[figure_id]}` variants.")
|
||||
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"- Combined cases CSV: `{SUBJECT_FIGURE_CASES_PATH}`",
|
||||
f"- Combined figure manifest CSV: `{SUBJECT_FIGURE_MANIFEST_PATH}`",
|
||||
f"- A01 panel manifest CSV: `{A01_ARTICLE_SUBJECT_MANIFEST_PATH}`",
|
||||
f"- A02 panel manifest CSV: `{A02_ARTICLE_SUBJECT_MANIFEST_PATH}`",
|
||||
f"- Output figure directory: `{FIGURES_ROOT}`",
|
||||
f"- Article figure directories: `{A01_FIGURES_ROOT}`, `{A02_FIGURES_ROOT}`",
|
||||
]
|
||||
)
|
||||
write_markdown(SUBJECT_FIGURES_SUMMARY_MD, lines)
|
||||
|
||||
payload = {
|
||||
"date_tag": date_tag,
|
||||
"variants_per_figure": variants,
|
||||
"figure_count": len(rendered_paths),
|
||||
"counts_by_article": counts_by_article,
|
||||
"counts_by_figure": counts_by_figure,
|
||||
"counts_by_hypothesis": counts_by_hypothesis,
|
||||
"cases_csv": str(SUBJECT_FIGURE_CASES_PATH),
|
||||
"figure_manifest_csv": str(SUBJECT_FIGURE_MANIFEST_PATH),
|
||||
"article_cases_csv": {
|
||||
"A01": str(A01_SUBJECT_FIGURE_CASES_PATH),
|
||||
"A02": str(A02_SUBJECT_FIGURE_CASES_PATH),
|
||||
},
|
||||
"article_manifest_csv": article_manifest_paths,
|
||||
"summary_md": str(SUBJECT_FIGURES_SUMMARY_MD),
|
||||
"rendered_paths": rendered_paths,
|
||||
"article_paths": article_paths,
|
||||
}
|
||||
write_json(SUBJECT_FIGURES_SUMMARY_JSON, payload)
|
||||
return payload
|
||||
|
||||
|
||||
def append_execution_log(payload: dict[str, object]) -> None:
|
||||
ensure_dir(EXPERIMENT_EXECUTION_LOG_PATH.parent)
|
||||
date_header = datetime.now().strftime("%Y-%m-%d")
|
||||
if EXPERIMENT_EXECUTION_LOG_PATH.exists():
|
||||
original = EXPERIMENT_EXECUTION_LOG_PATH.read_text(encoding="utf-8").rstrip() + "\n"
|
||||
else:
|
||||
original = "# Журнал выполнения E01\n"
|
||||
lines = [
|
||||
"",
|
||||
f"## {date_header}",
|
||||
"",
|
||||
"- Доработан воспроизводимый pipeline предметных figure-сюжетов по ТЗ для статей A01 и A02.",
|
||||
f"- Сгенерированы все требуемые сюжетные фигуры: `{payload['figure_count']}` PNG-файлов, по `{payload['variants_per_figure']}` вариантов на каждый figure id.",
|
||||
f"- Обновлен общий manifest фигур: `{SUBJECT_FIGURE_MANIFEST_PATH}`.",
|
||||
f"- Обновлены article-level manifest файлы: `{A01_ARTICLE_SUBJECT_MANIFEST_PATH}` и `{A02_ARTICLE_SUBJECT_MANIFEST_PATH}`.",
|
||||
f"- Обновлена сводка предметных фигур: `{SUBJECT_FIGURES_SUMMARY_MD}`.",
|
||||
]
|
||||
EXPERIMENT_EXECUTION_LOG_PATH.write_text(original + "\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
payload = run_subject_figures(variants=args.variants, overwrite=args.overwrite)
|
||||
append_execution_log(payload)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user