chromatography-rt-prediction / revision /scripts /recompute_paired_bootstrap.py
AI4deeperScience's picture
Add files using upload-large-folder tool
6cf9dac verified
Raw
History Blame Contribute Delete
6.95 kB
"""Recompute the reviewer-requested paired bootstrap table from frozen predictions.
The identity-grouped task resamples molecular-connectivity groups. The
scaffold-aware task resamples the connected scaffold components used to form
the held-out partition. No model is refitted and no prediction is changed.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
import pandas as pd
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
TASKS = {
"canonical_grouped": ("Identity-grouped", "structure_group"),
"scaffold_aware": ("Scaffold-aware", "scaffold_component_group"),
}
REFERENCES = {
"FPNN": "prediction_fpnn",
"GAT": "prediction_gat",
"GCN": "prediction_gcn",
}
CANDIDATE = "prediction_stack_all_plus_descriptors"
METRICS = ("mae", "rmse", "r2")
def metric(name: str, y_true: np.ndarray, y_pred: np.ndarray) -> float:
if name == "mae":
return float(mean_absolute_error(y_true, y_pred))
if name == "rmse":
return float(np.sqrt(mean_squared_error(y_true, y_pred)))
if name == "r2":
return float(r2_score(y_true, y_pred))
raise ValueError(name)
def paired_bootstrap(
y_true: np.ndarray,
candidate: np.ndarray,
reference: np.ndarray,
groups: np.ndarray,
*,
n_resamples: int,
seed: int,
) -> dict[str, dict[str, float | int]]:
unique_groups = np.unique(groups.astype(str))
positions = {group: np.flatnonzero(groups == group) for group in unique_groups}
rng = np.random.default_rng(seed)
samples = {name: [] for name in METRICS}
for _ in range(n_resamples):
sampled_groups = rng.choice(unique_groups, size=len(unique_groups), replace=True)
sampled_positions = np.concatenate([positions[group] for group in sampled_groups])
for name in METRICS:
delta = metric(name, y_true[sampled_positions], candidate[sampled_positions])
delta -= metric(name, y_true[sampled_positions], reference[sampled_positions])
if np.isfinite(delta):
samples[name].append(delta)
result: dict[str, dict[str, float | int]] = {}
for name in METRICS:
point = metric(name, y_true, candidate) - metric(name, y_true, reference)
low, high = np.quantile(np.asarray(samples[name]), [0.025, 0.975])
result[name] = {
"difference_point": float(point),
"ci_low": float(low),
"ci_high": float(high),
"n_resampling_units": int(len(unique_groups)),
"n_valid_resamples": int(len(samples[name])),
}
return result
def fmt(value: float, low: float, high: float, digits: int) -> str:
return f"{value:.{digits}f} [{low:.{digits}f}, {high:.{digits}f}]"
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--artifacts", type=Path, required=True)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--table", type=Path, required=True)
parser.add_argument("--n-resamples", type=int, default=2000)
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
nested: dict[str, dict[str, object]] = {}
rows: list[dict[str, object]] = []
for task_dir, (task_label, group_column) in TASKS.items():
nested[task_dir] = {}
for repeat, seed in enumerate((123456, 123457, 123458), start=1):
path = args.artifacts / task_dir / f"seed_{seed}" / "neural_stack" / "test_predictions.csv"
frame = pd.read_csv(path)
y_true = frame["RT"].to_numpy(dtype=float)
candidate = frame[CANDIDATE].to_numpy(dtype=float)
groups = frame[group_column].astype(str).to_numpy()
repeat_result: dict[str, object] = {
"seed": seed,
"resampling_unit": group_column,
"n_rows": int(len(frame)),
"n_resampling_units": int(pd.Series(groups).nunique()),
"comparisons": {},
}
for reference_label, reference_column in REFERENCES.items():
result = paired_bootstrap(
y_true,
candidate,
frame[reference_column].to_numpy(dtype=float),
groups,
n_resamples=args.n_resamples,
seed=seed,
)
repeat_result["comparisons"][reference_label] = result
row: dict[str, object] = {
"task": task_label,
"resampling_unit": group_column,
"reference": reference_label,
"repeat": repeat,
"seed": seed,
"n_rows": len(frame),
"n_resampling_units": pd.Series(groups).nunique(),
}
for name in METRICS:
for key, value in result[name].items():
row[f"{name}_{key}"] = value
rows.append(row)
nested[task_dir][f"seed_{seed}"] = repeat_result
(args.output_dir / "paired_bootstrap_corrected.json").write_text(
json.dumps(nested, indent=2), encoding="utf-8"
)
frame = pd.DataFrame(rows)
frame.to_csv(args.output_dir / "paired_bootstrap_corrected.csv", index=False)
latex = [
r"\begin{table*}[htbp]",
r"\centering",
r"\small",
r"\caption{Paired cluster-bootstrap differences for the full stack relative to each neural base learner. Identity-grouped repeats resample molecular-connectivity groups; scaffold-aware repeats resample scaffold components. Each interval uses 2,000 resamples. Negative $\Delta$MAE and $\Delta$RMSE and positive $\Delta R^2$ favor the full stack.}",
r"\label{tab:paired-bootstrap}",
r"\begin{tabular}{lllccc}",
r"\toprule",
r"Task & Reference & Repeat & $\Delta$MAE (95\% interval), min & $\Delta$RMSE (95\% interval), min & $\Delta R^2$ (95\% interval) \\",
r"\midrule",
]
for task_label in ("Identity-grouped", "Scaffold-aware"):
task_rows = frame.loc[frame["task"] == task_label]
for reference_label in REFERENCES:
for _, row in task_rows.loc[task_rows["reference"] == reference_label].iterrows():
mae = fmt(row.mae_difference_point, row.mae_ci_low, row.mae_ci_high, 3)
rmse = fmt(row.rmse_difference_point, row.rmse_ci_low, row.rmse_ci_high, 3)
r2 = fmt(row.r2_difference_point, row.r2_ci_low, row.r2_ci_high, 3)
latex.append(
f"{task_label} & {reference_label} & {int(row['repeat'])} & {mae} & {rmse} & {r2} \\\\"
)
latex.extend([r"\bottomrule", r"\end{tabular}", r"\end{table*}", ""])
args.table.write_text("\n".join(latex), encoding="utf-8")
if __name__ == "__main__":
main()