|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
Parse *_NB_single_dataset.txt reports and emit LaTeX tables. |
|
|
|
|
|
Now supports: |
|
|
• Full "everything" longtable (as before) |
|
|
• Option B: TWO COMPACT portrait tables (RMSE table + MAE table) |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
# Make compact + full tables for all reports in a dir |
|
|
python3 make_nb_tables_from_reports.py --in ./plotting --out ./plotting/tex |
|
|
|
|
|
# Only compact tables |
|
|
python3 make_nb_tables_from_reports.py --in ./plotting --out ./plotting/tex --compact_only |
|
|
|
|
|
# Specific files |
|
|
python3 make_nb_tables_from_reports.py \ |
|
|
--files ./plotting/qm9_NB_single_dataset.txt ./plotting/boilingpoint_NB_single_dataset.txt \ |
|
|
--out ./plotting/tex --compact_only |
|
|
""" |
|
|
|
|
|
import argparse |
|
|
import re |
|
|
from pathlib import Path |
|
|
from typing import Dict, List, Tuple |
|
|
import pandas as pd |
|
|
|
|
|
NUM = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" |
|
|
|
|
|
|
|
|
def esc_tex(s: str) -> str: |
|
|
return s.replace("_", r"\_") |
|
|
|
|
|
|
|
|
def parse_header(lines: List[str]) -> Dict[str, str]: |
|
|
out = {} |
|
|
for ln in lines[:60]: |
|
|
m = re.search(r"^Dataset:\s*(.+?)\s+—", ln) |
|
|
if m: |
|
|
out["dataset"] = m.group(1).strip() |
|
|
m = re.search(r"^Control exp_id:\s*(.+)$", ln) |
|
|
if m: |
|
|
out["control"] = m.group(1).strip() |
|
|
m = re.search(r"^k folds:\s*(\d+),\s*alpha:\s*(" + NUM + r")", ln) |
|
|
if m: |
|
|
out["k"] = int(m.group(1)) |
|
|
out["alpha"] = float(m.group(2)) |
|
|
return out |
|
|
|
|
|
|
|
|
def find_block(lines: List[str], start_pat: str) -> Tuple[int, int]: |
|
|
start = -1 |
|
|
pat = re.compile(start_pat) |
|
|
for i, ln in enumerate(lines): |
|
|
if pat.search(ln): |
|
|
start = i |
|
|
break |
|
|
if start < 0: |
|
|
return (-1, -1) |
|
|
end = len(lines) |
|
|
for j in range(start + 1, len(lines)): |
|
|
if not lines[j].strip(): |
|
|
end = j |
|
|
break |
|
|
return (start, end) |
|
|
|
|
|
|
|
|
def parse_nb_block(lines: List[str]) -> pd.DataFrame: |
|
|
hdr_idx = None |
|
|
for i, ln in enumerate(lines): |
|
|
if re.search(r"^\s*comparison\s+", ln) and "mean_diff_RMSE" in ln: |
|
|
hdr_idx = i |
|
|
break |
|
|
if hdr_idx is None: |
|
|
return pd.DataFrame() |
|
|
|
|
|
data_lines = [] |
|
|
for ln in lines[hdr_idx + 1 :]: |
|
|
s = ln.rstrip() |
|
|
if not s: |
|
|
break |
|
|
if set(s) <= set("- "): |
|
|
continue |
|
|
data_lines.append(s) |
|
|
|
|
|
rows = [] |
|
|
for ln in data_lines: |
|
|
|
|
|
m = re.match(r"^\s*(.+?)\s+(" + NUM + r"(?:\s+" + NUM + r"){9})\s*$", ln) |
|
|
if not m: |
|
|
parts = re.split(r"\s{2,}", ln.strip()) |
|
|
if len(parts) < 11: |
|
|
continue |
|
|
comp, *nums = parts |
|
|
else: |
|
|
comp = m.group(1).strip() |
|
|
nums = re.split(r"\s+", m.group(2).strip()) |
|
|
if len(nums) != 10: |
|
|
continue |
|
|
vals = list(map(float, nums)) |
|
|
rows.append( |
|
|
{ |
|
|
"comparison": comp, |
|
|
"mean_diff_RMSE": vals[0], |
|
|
"t_NB_RMSE": vals[1], |
|
|
"p_raw_RMSE": vals[2], |
|
|
"mean_diff_MAE": vals[3], |
|
|
"t_NB_MAE": vals[4], |
|
|
"p_raw_MAE": vals[5], |
|
|
"CI_RMSE_low": vals[6], |
|
|
"CI_RMSE_high": vals[7], |
|
|
"CI_MAE_low": vals[8], |
|
|
"CI_MAE_high": vals[9], |
|
|
} |
|
|
) |
|
|
return pd.DataFrame(rows) |
|
|
|
|
|
|
|
|
def parse_holm_block(lines: List[str], metric_label: str) -> pd.DataFrame: |
|
|
hdr = None |
|
|
for i, ln in enumerate(lines): |
|
|
if re.search(r"^\s*comparison\s+", ln) and "p_raw" in ln and "p_holm" in ln: |
|
|
hdr = i |
|
|
break |
|
|
if hdr is None: |
|
|
return pd.DataFrame() |
|
|
|
|
|
data_lines = [] |
|
|
for ln in lines[hdr + 1 :]: |
|
|
s = ln.rstrip() |
|
|
if not s: |
|
|
break |
|
|
if set(s) <= set("- "): |
|
|
continue |
|
|
data_lines.append(s) |
|
|
|
|
|
rows = [] |
|
|
for ln in data_lines: |
|
|
m = re.match(r"^\s*(.+?)\s+(" + NUM + r")\s+(" + NUM + r")\s+(\w+)\s*$", ln) |
|
|
if m: |
|
|
comp = m.group(1).strip() |
|
|
p_raw = float(m.group(2)) |
|
|
p_holm = float(m.group(3)) |
|
|
sig = m.group(4).strip().lower().startswith("t") |
|
|
else: |
|
|
parts = re.split(r"\s{2,}", ln.strip()) |
|
|
if len(parts) < 4: |
|
|
continue |
|
|
comp = parts[0].strip() |
|
|
p_holm = float(parts[2]) |
|
|
sig = parts[3].strip().lower().startswith("t") |
|
|
rows.append( |
|
|
{ |
|
|
"comparison": comp, |
|
|
f"p_holm_{metric_label}": p_holm, |
|
|
f"sig_{metric_label}": sig, |
|
|
} |
|
|
) |
|
|
return pd.DataFrame(rows) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_full_longtable(meta: Dict[str, str], df: pd.DataFrame, outpath: Path): |
|
|
dataset = meta.get("dataset", "dataset") |
|
|
control = meta.get("control", "control") |
|
|
k = int(meta.get("k", 5)) |
|
|
alpha = float(meta.get("alpha", 0.05)) |
|
|
|
|
|
header = rf""" |
|
|
% Auto-generated from NB_single_dataset report |
|
|
% Requires: \usepackage{{booktabs,longtable,siunitx}} |
|
|
\begin{{longtable}}{{@{{}}l |
|
|
S S S S S S S |
|
|
S |
|
|
S S S S S S S |
|
|
@{{}}}} |
|
|
\caption{{Full NB-corrected one-sided tests (outer folds, $K={k}$, $\alpha={alpha}$) comparing control \texttt{{{esc_tex(control)}}} against each competitor on dataset \texttt{{{esc_tex(dataset)}}}. |
|
|
Positive $\Delta$ (competitor $-$ control) favors the control (lower loss). |
|
|
One-sided alternative $\mathbb{{E}}[\bar d^{{(L)}}] > 0$. |
|
|
Holm step-down controls FWER per metric family (RMSE and MAE reported separately). |
|
|
CIs are NB-style two-sided.}} |
|
|
\label{{tab:{esc_tex(dataset)}_nb_full}}\\ |
|
|
\toprule |
|
|
& \multicolumn{{7}}{{c}}{{\textbf{{RMSE family}}}} & & |
|
|
\multicolumn{{7}}{{c}}{{\textbf{{MAE family}}}}\\ |
|
|
\cmidrule(lr){{2-8}}\cmidrule(lr){{10-16}} |
|
|
Comparison |
|
|
& {{\(\Delta\)RMSE}} |
|
|
& {{$t_{{\text{{NB}}}}$}} |
|
|
& {{p (raw)}} |
|
|
& {{p\(_{{\text{{Holm}}}}\)}} |
|
|
& {{\(\text{{Sig}}\)}} |
|
|
& {{CI\(_{{\text{{low}}}}\)}} |
|
|
& {{CI\(_{{\text{{high}}}}\)}} |
|
|
& {{}} % spacer |
|
|
& {{\(\Delta\)MAE}} |
|
|
& {{$t_{{\text{{NB}}}}$}} |
|
|
& {{p (raw)}} |
|
|
& {{p\(_{{\text{{Holm}}}}\)}} |
|
|
& {{\(\text{{Sig}}\)}} |
|
|
& {{CI\(_{{\text{{low}}}}\)}} |
|
|
& {{CI\(_{{\text{{high}}}}\)}}\\ |
|
|
\midrule |
|
|
\endfirsthead |
|
|
\toprule |
|
|
& \multicolumn{{7}}{{c}}{{\textbf{{RMSE family}}}} & & |
|
|
\multicolumn{{7}}{{c}}{{\textbf{{MAE family}}}}\\ |
|
|
\cmidrule(lr){{2-8}}\cmidrule(lr){{10-16}} |
|
|
Comparison |
|
|
& {{\(\Delta\)RMSE}} |
|
|
& {{$t_{{\text{{NB}}}}$}} |
|
|
& {{p (raw)}} |
|
|
& {{p\(_{{\text{{Holm}}}}\)}} |
|
|
& {{\(\text{{Sig}}\)}} |
|
|
& {{CI\(_{{\text{{low}}}}\)}} |
|
|
& {{CI\(_{{\text{{high}}}}\)}} |
|
|
& {{}} % spacer |
|
|
& {{\(\Delta\)MAE}} |
|
|
& {{$t_{{\text{{NB}}}}$}} |
|
|
& {{p (raw)}} |
|
|
& {{p\(_{{\text{{Holm}}}}\)}} |
|
|
& {{\(\text{{Sig}}\)}} |
|
|
& {{CI\(_{{\text{{low}}}}\)}} |
|
|
& {{CI\(_{{\text{{high}}}}\)}}\\ |
|
|
\midrule |
|
|
\endhead |
|
|
\midrule |
|
|
\multicolumn{{16}}{{r}}{{\emph{{Continued on next page}}}}\\ |
|
|
\midrule |
|
|
\endfoot |
|
|
\bottomrule |
|
|
\endlastfoot |
|
|
""" |
|
|
lines = [] |
|
|
for _, r in df.iterrows(): |
|
|
lines.append( |
|
|
f"{esc_tex(r['comparison'])} & " |
|
|
f"{r['mean_diff_RMSE']:.6f} & {r['t_NB_RMSE']:.6f} & {r['p_raw_RMSE']:.6f} & {r['p_holm_RMSE']:.6f} & {1 if r['sig_RMSE'] else 0} & {r['CI_RMSE_low']:.6f} & {r['CI_RMSE_high']:.6f} & & " |
|
|
f"{r['mean_diff_MAE']:.6f} & {r['t_NB_MAE']:.6f} & {r['p_raw_MAE']:.6f} & {r['p_holm_MAE']:.6f} & {1 if r['sig_MAE'] else 0} & {r['CI_MAE_low']:.6f} & {r['CI_MAE_high']:.6f} \\\\" |
|
|
) |
|
|
outpath.write_text( |
|
|
header + "\n".join(lines) + "\n\\end{longtable}\n", encoding="utf-8" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_compact_tables(meta: Dict[str, str], df: pd.DataFrame, outdir: Path): |
|
|
""" |
|
|
Produce two small portrait tables: |
|
|
• <dataset>_nb_compact_rmse.tex |
|
|
• <dataset>_nb_compact_mae.tex |
|
|
|
|
|
Columns (per metric): Comparison, Δ, t_NB, CI_low, CI_high, p_Holm |
|
|
""" |
|
|
dataset = meta.get("dataset", "dataset") |
|
|
control = meta.get("control", "control") |
|
|
k = int(meta.get("k", 5)) |
|
|
|
|
|
common_preamble = r""" |
|
|
% Auto-generated compact NB tables |
|
|
% Requires in preamble: \usepackage{booktabs,tabularx,siunitx} |
|
|
% \sisetup{round-mode=places,round-precision=3,scientific-notation=true} |
|
|
""" |
|
|
|
|
|
|
|
|
rmse_header = rf"""{common_preamble} |
|
|
\begin{{table}}[t] |
|
|
\centering |
|
|
\small |
|
|
\setlength{{\tabcolsep}}{{4pt}} |
|
|
\caption{{RMSE: NB-corrected one-sided tests (outer folds, $K={k}$) on dataset \texttt{{{esc_tex(dataset)}}}; control \texttt{{{esc_tex(control)}}}. |
|
|
Positive $\Delta$ (competitor $-$ control) favors control. Holm controls FWER.}} |
|
|
\label{{tab:{esc_tex(dataset)}_nb_compact_rmse}} |
|
|
\begin{{tabularx}}{{\linewidth}}{{@{{}}l S S S S S@{{}}}} |
|
|
\toprule |
|
|
Comparison & {{\(\Delta\)RMSE}} & {{$t_{{\text{{NB}}}}$}} & {{CI\(_{{\text{{low}}}}\)}} & {{CI\(_{{\text{{high}}}}\)}} & {{p\(_{{\text{{Holm}}}}\)}}\\ |
|
|
\midrule |
|
|
""" |
|
|
rmse_rows = [] |
|
|
for _, r in df.sort_values("p_holm_RMSE").iterrows(): |
|
|
rmse_rows.append( |
|
|
f"{esc_tex(r['comparison'])} & " |
|
|
f"{r['mean_diff_RMSE']:.3f} & {r['t_NB_RMSE']:.3f} & " |
|
|
f"{r['CI_RMSE_low']:.3f} & {r['CI_RMSE_high']:.3f} & {r['p_holm_RMSE']:.3g} \\\\" |
|
|
) |
|
|
rmse_tex = ( |
|
|
rmse_header |
|
|
+ "\n".join(rmse_rows) |
|
|
+ "\n\\bottomrule\n\\end{tabularx}\n\\end{table}\n" |
|
|
) |
|
|
(outdir / f"{dataset}_nb_compact_rmse.tex").write_text(rmse_tex, encoding="utf-8") |
|
|
|
|
|
|
|
|
mae_header = rf"""{common_preamble} |
|
|
\begin{{table}}[t] |
|
|
\centering |
|
|
\small |
|
|
\setlength{{\tabcolsep}}{{4pt}} |
|
|
\caption{{MAE: NB-corrected one-sided tests (outer folds, $K={k}$) on dataset \texttt{{{esc_tex(dataset)}}}; control \texttt{{{esc_tex(control)}}}. |
|
|
Positive $\Delta$ (competitor $-$ control) favors control. Holm controls FWER.}} |
|
|
\label{{tab:{esc_tex(dataset)}_nb_compact_mae}} |
|
|
\begin{{tabularx}}{{\linewidth}}{{@{{}}l S S S S S@{{}}}} |
|
|
\toprule |
|
|
Comparison & {{\(\Delta\)MAE}} & {{$t_{{\text{{NB}}}}$}} & {{CI\(_{{\text{{low}}}}\)}} & {{CI\(_{{\text{{high}}}}\)}} & {{p\(_{{\text{{Holm}}}}\)}}\\ |
|
|
\midrule |
|
|
""" |
|
|
mae_rows = [] |
|
|
for _, r in df.sort_values("p_holm_MAE").iterrows(): |
|
|
mae_rows.append( |
|
|
f"{esc_tex(r['comparison'])} & " |
|
|
f"{r['mean_diff_MAE']:.3f} & {r['t_NB_MAE']:.3f} & " |
|
|
f"{r['CI_MAE_low']:.3f} & {r['CI_MAE_high']:.3f} & {r['p_holm_MAE']:.3g} \\\\" |
|
|
) |
|
|
mae_tex = ( |
|
|
mae_header |
|
|
+ "\n".join(mae_rows) |
|
|
+ "\n\\bottomrule\n\\end{tabularx}\n\\end{table}\n" |
|
|
) |
|
|
(outdir / f"{dataset}_nb_compact_mae.tex").write_text(mae_tex, encoding="utf-8") |
|
|
|
|
|
|
|
|
def process_file(path: Path, outdir: Path, make_full: bool, make_compact: bool): |
|
|
txt = path.read_text(encoding="utf-8", errors="ignore").splitlines() |
|
|
meta = parse_header(txt) |
|
|
|
|
|
|
|
|
nb_start, nb_end = find_block( |
|
|
txt, r"^\s*--- NB-corrected t \(outer folds\) per competitor ---" |
|
|
) |
|
|
if nb_start < 0: |
|
|
raise RuntimeError(f"NB block not found in {path}") |
|
|
df_nb = parse_nb_block(txt[nb_start:nb_end]) |
|
|
if df_nb.empty: |
|
|
raise RuntimeError(f"NB table parsing failed in {path}") |
|
|
|
|
|
|
|
|
hr_start, hr_end = find_block( |
|
|
txt, r"^\s*--- Holm-adjusted p-values \(RMSE family\)\s*---" |
|
|
) |
|
|
hm_start, hm_end = find_block( |
|
|
txt, r"^\s*--- Holm-adjusted p-values \(MAE family\)\s*---" |
|
|
) |
|
|
if hr_start < 0 or hm_start < 0: |
|
|
raise RuntimeError(f"Holm blocks not found in {path}") |
|
|
df_hr = parse_holm_block(txt[hr_start:hr_end], "RMSE") |
|
|
df_hm = parse_holm_block(txt[hm_start:hm_end], "MAE") |
|
|
|
|
|
|
|
|
df = df_nb.merge(df_hr, on="comparison", how="left").merge( |
|
|
df_hm, on="comparison", how="left" |
|
|
) |
|
|
|
|
|
|
|
|
dataset = meta.get("dataset", path.stem.replace("_NB_single_dataset", "")) |
|
|
outdir.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
if make_full: |
|
|
full_path = outdir / f"{dataset}_nb_full_summary_from_report.tex" |
|
|
|
|
|
df_full = df.sort_values(["p_raw_RMSE", "p_raw_MAE"]).reset_index(drop=True) |
|
|
write_full_longtable(meta, df_full, full_path) |
|
|
|
|
|
if make_compact: |
|
|
|
|
|
write_compact_tables(meta, df, outdir) |
|
|
|
|
|
print( |
|
|
f"Processed: {path.name} -> {dataset} (full={make_full}, compact={make_compact})" |
|
|
) |
|
|
|
|
|
|
|
|
def main(): |
|
|
ap = argparse.ArgumentParser() |
|
|
ap.add_argument( |
|
|
"--in", |
|
|
dest="indir", |
|
|
type=str, |
|
|
default=None, |
|
|
help="Directory containing *_NB_single_dataset.txt files.", |
|
|
) |
|
|
ap.add_argument( |
|
|
"--files", nargs="*", default=None, help="Explicit list of report files." |
|
|
) |
|
|
ap.add_argument( |
|
|
"--out", |
|
|
dest="outdir", |
|
|
type=str, |
|
|
required=True, |
|
|
help="Output directory for .tex tables.", |
|
|
) |
|
|
ap.add_argument( |
|
|
"--compact_only", |
|
|
action="store_true", |
|
|
help="Only write the compact RMSE/MAE tables (skip the full longtable).", |
|
|
) |
|
|
args = ap.parse_args() |
|
|
|
|
|
outdir = Path(args.outdir) |
|
|
if args.files: |
|
|
files = [Path(f) for f in args.files] |
|
|
elif args.indir: |
|
|
files = sorted(Path(args.indir).glob("*_NB_single_dataset.txt")) |
|
|
else: |
|
|
raise SystemExit("Provide either --in DIR or --files file1 file2 ...") |
|
|
|
|
|
if not files: |
|
|
raise SystemExit("No *_NB_single_dataset.txt files found.") |
|
|
|
|
|
for f in files: |
|
|
process_file(f, outdir, make_full=not args.compact_only, make_compact=True) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|