| """Generate appendix-ready LaTeX tables from benchmark summaries.""" |
| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| from make_tables import EXTRA_FILES, METHOD_LABELS, METHOD_ORDER, REAL_SPECS, extract_summary, load_json |
|
|
| SUMMARY_METHODS = ["global", "partition", "twostage", "fullcp", "jackknife_plus", "oneshot", "trainres", "weighted"] |
| PBMC_RUNTIME_FALLBACK = "pbmc_sensitivity_exp2_1_bulk_deconv_boundary_fixed.json" |
|
|
|
|
| def latex_escape(text: str) -> str: |
| return ( |
| text.replace("\\", "\\textbackslash{}") |
| .replace("_", "\\_") |
| .replace("%", "\\%") |
| .replace("&", "\\&") |
| .replace("#", "\\#") |
| ) |
|
|
|
|
| def merge_summary(label: str, filename: str, results_dir: Path) -> dict: |
| summary = extract_summary(load_json(results_dir / filename)) |
| if label in EXTRA_FILES: |
| for extra_name in EXTRA_FILES[label]: |
| extra_path = results_dir / extra_name |
| if extra_path.exists(): |
| summary = {**summary, **extract_summary(load_json(extra_path))} |
| return summary |
|
|
|
|
| def format_cov(entry: dict | None) -> str: |
| if not entry: |
| return "--" |
| return f"{entry['mean']:.3f}" |
|
|
|
|
| def metric_mean(entry: dict | None, key: str) -> str: |
| if not entry: |
| return "--" |
| value = entry.get(key) |
| if value is None: |
| return "--" |
| if isinstance(value, dict): |
| return f"{value['mean']:.3f}" |
| return f"{float(value):.3f}" |
|
|
|
|
| def worst_cov(entry: dict | None) -> str: |
| if not entry: |
| return "--" |
| if "worst_stratum_coverage" in entry: |
| return metric_mean(entry, "worst_stratum_coverage") |
| stratified = entry.get("stratified_coverage", {}) |
| if not stratified: |
| return "--" |
| vals = [] |
| for value in stratified.values(): |
| if isinstance(value, dict): |
| vals.append(float(value["mean"])) |
| else: |
| vals.append(float(value)) |
| return "--" if not vals else f"{min(vals):.3f}" |
|
|
|
|
| def runtime_mean(summary: dict, method: str, label: str, results_dir: Path) -> str: |
| entry = summary.get(method) |
| if entry and "runtime_sec" in entry: |
| return metric_mean(entry, "runtime_sec") |
| if label == "PBMC": |
| fallback_path = results_dir / PBMC_RUNTIME_FALLBACK |
| if fallback_path.exists(): |
| fallback_summary = extract_summary(load_json(fallback_path)) |
| fallback_entry = fallback_summary.get(method) |
| if fallback_entry and "runtime_sec" in fallback_entry: |
| return metric_mean(fallback_entry, "runtime_sec") |
| return "--" |
|
|
|
|
| def build_real_summary_tables(results_dir: Path) -> list[str]: |
| blocks: list[str] = [] |
| task_groups = [REAL_SPECS[:3], REAL_SPECS[3:]] |
| for table_idx, specs in enumerate(task_groups, start=1): |
| blocks.append("\\begin{table*}[!htbp]") |
| blocks.append("\\centering") |
| blocks.append( |
| "\\caption{Real-task summary metrics. Rows report mean marginal coverage, worst-stratum coverage, max disparity, mean radius, and runtime per repetition for each default real-data benchmark task and method. PBMC runtime entries use the corresponding boundary-based rerun because the original deconvolution summary files predated runtime logging for several split methods.}" |
| if table_idx == 1 else |
| "\\caption[]{Real-task summary metrics (continued).}" |
| ) |
| blocks.append(f"\\label{{tab:real-summary-{table_idx}}}") |
| blocks.append("\\scriptsize") |
| blocks.append("\\setlength{\\tabcolsep}{4pt}") |
| blocks.append("\\begin{tabular}{@{}llccccc@{}}") |
| blocks.append("\\toprule") |
| blocks.append("Task & Method & Coverage & Worst & Disparity & Radius & Runtime (s) \\\\") |
| blocks.append("\\midrule") |
|
|
| first_task = True |
| for filename, label in specs: |
| path = results_dir / filename |
| if not path.exists(): |
| continue |
| summary = merge_summary(label, filename, results_dir) |
| methods_present = [m for m in SUMMARY_METHODS if m in summary] |
| if not methods_present: |
| continue |
| if not first_task: |
| blocks.append("\\midrule") |
| first_task = False |
| for row_idx, method in enumerate(methods_present): |
| entry = summary.get(method) |
| task_cell = ( |
| f"\\multirow{{{len(methods_present)}}}{{*}}{{{latex_escape(label)}}}" |
| if row_idx == 0 else "" |
| ) |
| row = [ |
| task_cell, |
| METHOD_LABELS[method], |
| metric_mean(entry, "marginal_coverage"), |
| worst_cov(entry), |
| metric_mean(entry, "max_disparity"), |
| metric_mean(entry, "mean_radius"), |
| runtime_mean(summary, method, label, results_dir), |
| ] |
| blocks.append(" & ".join(row) + " \\\\") |
| blocks.append("\\bottomrule") |
| blocks.append("\\end{tabular}") |
| blocks.append("\\end{table*}") |
| blocks.append("") |
| return blocks |
|
|
|
|
| def build_per_strata_tables(results_dir: Path) -> list[str]: |
| blocks: list[str] = [] |
| for filename, label in REAL_SPECS: |
| path = results_dir / filename |
| if not path.exists(): |
| continue |
| summary = merge_summary(label, filename, results_dir) |
| ref_method = next((m for m in SUMMARY_METHODS if m in summary), None) |
| if ref_method is None: |
| continue |
| strata_keys = sorted(summary[ref_method]["stratified_coverage"].keys(), key=int) |
| cols = "l" + "c" * len(SUMMARY_METHODS) |
| blocks.append("\\begin{table*}[!htbp]") |
| blocks.append("\\centering") |
| blocks.append( |
| f"\\caption{{Per-stratum coverage for {latex_escape(label)}. Entries report mean empirical coverage across repetitions.}}" |
| ) |
| blocks.append(f"\\label{{tab:strata-{label.lower().replace(' ', '-')}}}") |
| blocks.append("\\small") |
| blocks.append(f"\\begin{{tabular}}{{@{{}}{cols}@{{}}}}") |
| blocks.append("\\toprule") |
| headers = ["Stratum"] + [METHOD_LABELS[m] for m in SUMMARY_METHODS] |
| blocks.append(" & ".join(headers) + " \\\\") |
| blocks.append("\\midrule") |
| for k in strata_keys: |
| row = [f"S{k}"] |
| for method in SUMMARY_METHODS: |
| entry = summary.get(method, {}).get("stratified_coverage", {}).get(k) |
| row.append(format_cov(entry)) |
| blocks.append(" & ".join(row) + " \\\\") |
| blocks.append("\\bottomrule") |
| blocks.append("\\end{tabular}") |
| blocks.append("\\end{table*}") |
| blocks.append("") |
| return blocks |
|
|
|
|
| def build_runtime_table(results_dir: Path) -> list[str]: |
| blocks: list[str] = [] |
| blocks.append("\\begin{table*}[!htbp]") |
| blocks.append("\\centering") |
| blocks.append("\\caption{Mean runtime per repetition in seconds on the real-data benchmark. Entries marked \\texttt{--} were not run for that task.}") |
| blocks.append("\\label{tab:runtime-real}") |
| blocks.append("\\small") |
| cols = "l" + "c" * len(REAL_SPECS) |
| blocks.append(f"\\begin{{tabular}}{{@{{}}{cols}@{{}}}}") |
| blocks.append("\\toprule") |
| blocks.append("Method & " + " & ".join(label for _, label in REAL_SPECS) + " \\\\") |
| blocks.append("\\midrule") |
| for method in SUMMARY_METHODS: |
| row = [METHOD_LABELS[method]] |
| for filename, label in REAL_SPECS: |
| path = results_dir / filename |
| if not path.exists(): |
| row.append("--") |
| continue |
| summary = merge_summary(label, filename, results_dir) |
| runtime = runtime_mean(summary, method, label, results_dir) |
| row.append(runtime if runtime == "--" else f"{float(runtime):.2f}") |
| blocks.append(" & ".join(row) + " \\\\") |
| blocks.append("\\bottomrule") |
| blocks.append("\\end{tabular}") |
| blocks.append("\\end{table*}") |
| blocks.append("") |
| return blocks |
|
|
|
|
| def main() -> None: |
| results_dir = Path("results/tables") |
| out_path = Path("paper/rewrite_2026/latex/generated_appendix_tables.tex") |
| blocks: list[str] = [] |
| blocks.append("% Auto-generated by scripts/make_appendix_tables.py") |
| blocks.append("\\FloatBarrier") |
| blocks.append("\\subsection{Real-task summary tables}") |
| blocks.append("") |
| blocks.extend(build_real_summary_tables(results_dir)) |
| blocks.append("\\clearpage") |
| blocks.append("\\FloatBarrier") |
| blocks.append("\\subsection{Per-strata coverage tables}") |
| blocks.append("") |
| blocks.extend(build_per_strata_tables(results_dir)) |
| blocks.append("\\clearpage") |
| blocks.append("\\FloatBarrier") |
| blocks.append("\\subsection{Runtime comparison}") |
| blocks.append("") |
| blocks.extend(build_runtime_table(results_dir)) |
| out_path.write_text("\n".join(blocks) + "\n") |
| print(f"Saved {out_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|