opencode tests and his result

This commit is contained in:
folen
2026-10-26 05:44:25 +03:00
parent 2c6bf4653a
commit ac7c6f0b92
12 changed files with 3032 additions and 0 deletions

336
lab1/scripts/analyze_followup.py Executable file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
"""Сводка follow-up экспериментов и парные сравнения по повторам.
Скрипт читает каталоги results/followup/<label>/, созданные
scripts/run_followup_experiments.sh, и формирует:
- summary.csv / summary.md — среднее macro-F1 по повторам, а не только repeat 0;
- comparisons.csv / comparisons.md — парные сравнения с McNemar и bootstrap;
- comparisons/<A>__vs__<B>.json — детали по каждому повтору.
Пример:
python scripts/analyze_followup.py \
--followup-dir results/followup \
--pairs final_rf:ref_logreg final_rf:ref_no_lsa
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from stylometry.evaluation import classification_metrics # noqa: E402
from stylometry.stats import bootstrap_difference, mcnemar_exact # noqa: E402
def _json_load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _read_run(run_dir: Path) -> tuple[dict[str, Any], pd.DataFrame]:
metadata = _json_load(run_dir / "run_metadata.json")
pooled_path = run_dir / "pooled_metrics.csv"
if pooled_path.exists():
pooled = pd.read_csv(pooled_path)
else:
pooled = pd.DataFrame([metadata.get("metrics", {})])
return metadata, pooled
def _feature_summary(config: dict[str, Any]) -> str:
keys = (
"groups",
"lsa_variants",
"lsa_components",
"min_df",
"char_max_features",
"pos_max_features",
"char_ngram_range",
"pos_ngram_range",
"block_weights",
)
features = config.get("feature_config", {})
return json.dumps({key: features.get(key) for key in keys if key in features}, ensure_ascii=False)
def build_summary(followup_dir: Path) -> pd.DataFrame:
rows: list[dict[str, Any]] = []
for run_dir in sorted(followup_dir.iterdir()):
if not run_dir.is_dir() or not (run_dir / "run_metadata.json").exists():
continue
metadata, pooled = _read_run(run_dir)
config = metadata.get("config", {})
bootstrap = metadata.get("bootstrap", {}).get("macro_f1", {})
macro = pd.to_numeric(pooled.get("macro_f1"), errors="coerce")
accuracy = pd.to_numeric(pooled.get("accuracy"), errors="coerce")
balanced = pd.to_numeric(pooled.get("balanced_accuracy"), errors="coerce")
top3 = pd.to_numeric(pooled.get("top3_accuracy"), errors="coerce")
command_path = run_dir / "command.txt"
rows.append(
{
"label": run_dir.name,
"preset": config.get("preset"),
"model": config.get("model"),
"folds": config.get("folds"),
"n_repeats": int(len(pooled)),
"n_samples": config.get("n_samples"),
"mean_macro_f1": float(macro.mean()),
"std_macro_f1": float(macro.std(ddof=1)) if len(macro) > 1 else 0.0,
"min_macro_f1": float(macro.min()),
"max_macro_f1": float(macro.max()),
"repeat0_macro_f1": float(macro.iloc[0]),
"mean_accuracy": float(accuracy.mean()),
"mean_balanced_accuracy": float(balanced.mean()),
"mean_top3_accuracy": float(top3.mean()),
"ci_lower_repeat0": bootstrap.get("lower"),
"ci_upper_repeat0": bootstrap.get("upper"),
"model_params": json.dumps(config.get("model_params", {}), ensure_ascii=False),
"feature_config": _feature_summary(config),
"command": command_path.read_text(encoding="utf-8").strip()
if command_path.exists()
else "",
}
)
summary = pd.DataFrame(rows)
if summary.empty:
return summary
reference = summary.loc[summary["label"] == "ref_logreg", "mean_macro_f1"]
if not reference.empty:
summary["delta_vs_ref_logreg"] = summary["mean_macro_f1"] - float(reference.iloc[0])
else:
summary["delta_vs_ref_logreg"] = np.nan
summary = summary.sort_values("mean_macro_f1", ascending=False).reset_index(drop=True)
summary.insert(0, "rank", np.arange(1, len(summary) + 1))
return summary
def _compare_pair(
followup_dir: Path,
label_a: str,
label_b: str,
n_boot: int,
seed: int,
) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
dir_a = followup_dir / label_a
dir_b = followup_dir / label_b
if not (dir_a / "oof_predictions.csv").exists() or not (dir_b / "oof_predictions.csv").exists():
print(f"[compare] пропуск {label_a} vs {label_b}: нет oof_predictions.csv")
return None, []
oof_a = pd.read_csv(dir_a / "oof_predictions.csv")
oof_b = pd.read_csv(dir_b / "oof_predictions.csv")
common_repeats = sorted(set(oof_a["repeat"].unique()) & set(oof_b["repeat"].unique()))
if not common_repeats:
print(f"[compare] пропуск {label_a} vs {label_b}: нет общих повторов")
return None, []
details: list[dict[str, Any]] = []
for repeat in common_repeats:
first = oof_a.loc[oof_a["repeat"] == repeat, ["line_id", "y_true", "y_pred"]]
second = oof_b.loc[oof_b["repeat"] == repeat, ["line_id", "y_true", "y_pred"]]
merged = first.merge(
second,
on=["line_id", "y_true"],
suffixes=("_a", "_b"),
how="inner",
)
if merged.empty:
continue
labels = sorted(merged["y_true"].unique())
metrics_a = classification_metrics(merged["y_true"], merged["y_pred_a"], labels=labels)
metrics_b = classification_metrics(merged["y_true"], merged["y_pred_b"], labels=labels)
mcnemar = mcnemar_exact(merged["y_true"], merged["y_pred_a"], merged["y_pred_b"])
difference = bootstrap_difference(
merged["y_true"],
merged["y_pred_a"],
merged["y_pred_b"],
metric="macro_f1",
labels=labels,
n_boot=n_boot,
seed=seed + int(repeat),
)
details.append(
{
"repeat": int(repeat),
"n_samples": int(len(merged)),
"macro_f1_a": metrics_a["macro_f1"],
"macro_f1_b": metrics_b["macro_f1"],
"macro_f1_diff": metrics_a["macro_f1"] - metrics_b["macro_f1"],
"accuracy_a": metrics_a["accuracy"],
"accuracy_b": metrics_b["accuracy"],
"accuracy_diff": metrics_a["accuracy"] - metrics_b["accuracy"],
"mcnemar_p": mcnemar["p_value"],
"mcnemar_b": mcnemar["b"],
"mcnemar_c": mcnemar["c"],
"bootstrap_mean_diff": difference["mean_difference"],
"bootstrap_lower": difference["lower"],
"bootstrap_upper": difference["upper"],
}
)
if not details:
print(f"[compare] пропуск {label_a} vs {label_b}: не удалось сопоставить строки")
return None, []
diffs = np.asarray([row["macro_f1_diff"] for row in details], dtype=float)
p_values = np.asarray([row["mcnemar_p"] for row in details], dtype=float)
ci_lower = np.asarray([row["bootstrap_lower"] for row in details], dtype=float)
ci_upper = np.asarray([row["bootstrap_upper"] for row in details], dtype=float)
summary = {
"pair": f"{label_a} vs {label_b}",
"label_a": label_a,
"label_b": label_b,
"n_repeats": len(details),
"mean_macro_f1_a": float(np.mean([row["macro_f1_a"] for row in details])),
"mean_macro_f1_b": float(np.mean([row["macro_f1_b"] for row in details])),
"mean_diff": float(diffs.mean()),
"std_diff": float(diffs.std(ddof=1)) if len(diffs) > 1 else 0.0,
"min_diff": float(diffs.min()),
"max_diff": float(diffs.max()),
"n_positive": int((diffs > 0).sum()),
"n_negative": int((diffs < 0).sum()),
"mean_mcnemar_p": float(p_values.mean()),
"min_mcnemar_p": float(p_values.min()),
"mean_ci_lower": float(ci_lower.mean()),
"mean_ci_upper": float(ci_upper.mean()),
"n_ci_excludes_zero": int(((ci_lower > 0) | (ci_upper < 0)).sum()),
}
return summary, details
def _format_markdown(summary: pd.DataFrame) -> str:
columns = [
"rank",
"label",
"model",
"preset",
"n_repeats",
"mean_macro_f1",
"std_macro_f1",
"mean_accuracy",
"mean_top3_accuracy",
"delta_vs_ref_logreg",
]
table = summary[columns].copy()
return table.to_markdown(index=False, floatfmt=".4f")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--followup-dir",
default=str(PROJECT_ROOT / "results" / "followup"),
help="Каталог с подкаталогами запусков.",
)
parser.add_argument(
"--pairs",
nargs="*",
default=[],
help="Пары для сравнения в формате label_a:label_b.",
)
parser.add_argument("--n-boot", type=int, default=1000)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args(argv)
followup_dir = Path(args.followup_dir)
if not followup_dir.exists():
raise FileNotFoundError(f"Нет каталога: {followup_dir}")
summary = build_summary(followup_dir)
if summary.empty:
print(f"[summary] в {followup_dir} нет запусков с run_metadata.json")
return 1
summary_path = followup_dir / "summary.csv"
summary.to_csv(summary_path, index=False, encoding="utf-8-sig")
markdown = [
"# Follow-up: сводка запусков",
"",
"Среднее и стандартное отклонение посчитаны по pooled-метрикам каждого",
"повтора (`pooled_metrics.csv`), а не только по repeat 0.",
"",
_format_markdown(summary),
"",
]
(followup_dir / "summary.md").write_text("\n".join(markdown), encoding="utf-8")
print(f"[summary] {summary_path}")
print(
summary[
["rank", "label", "model", "n_repeats", "mean_macro_f1", "std_macro_f1"]
].to_string(index=False)
)
comparison_rows: list[dict[str, Any]] = []
comparisons_dir = followup_dir / "comparisons"
comparisons_dir.mkdir(exist_ok=True)
for pair in args.pairs:
if ":" not in pair:
print(f"[compare] некорректная пара: {pair}")
continue
label_a, label_b = pair.split(":", 1)
summary_row, details = _compare_pair(
followup_dir,
label_a.strip(),
label_b.strip(),
n_boot=args.n_boot,
seed=args.seed,
)
if summary_row is None:
continue
comparison_rows.append(summary_row)
payload = {"summary": summary_row, "details": details}
output = comparisons_dir / f"{label_a.strip()}__vs__{label_b.strip()}.json"
output.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(
f"[compare] {summary_row['pair']}: "
f"diff={summary_row['mean_diff']:+.4f}, "
f"{summary_row['n_positive']}/{summary_row['n_repeats']} повторов в плюс, "
f"mean McNemar p={summary_row['mean_mcnemar_p']:.4f}"
)
if comparison_rows:
comparisons = pd.DataFrame(comparison_rows).sort_values("mean_diff", ascending=False)
comparisons.to_csv(followup_dir / "comparisons.csv", index=False, encoding="utf-8-sig")
columns = [
"pair",
"n_repeats",
"mean_macro_f1_a",
"mean_macro_f1_b",
"mean_diff",
"std_diff",
"n_positive",
"mean_mcnemar_p",
"mean_ci_lower",
"mean_ci_upper",
"n_ci_excludes_zero",
]
comparison_markdown = [
"# Follow-up: парные сравнения",
"",
"`mean_diff` — средняя разница macro-F1 (A − B) по общим повторам.",
"`n_positive` — сколько повторов дали положительную разницу.",
"`n_ci_excludes_zero` — сколько повторов имеют bootstrap-интервал,",
"не содержащий ноль (описательно, без поправки на множественность).",
"",
comparisons[columns].to_markdown(index=False, floatfmt=".4f"),
"",
]
(followup_dir / "comparisons.md").write_text(
"\n".join(comparison_markdown), encoding="utf-8"
)
print(f"[compare] {followup_dir / 'comparisons.csv'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())