first commit

This commit is contained in:
eeymoo
2026-09-19 12:54:45 +08:00
commit 6fc5b64077
126 changed files with 8601 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
"""Post-experiment analysis: stability, compliance, ANOVA, charts, export.
Usage: python analysis_export.py <experiment_id> <output_dir>
- stability_score: mean pairwise token-Jaccard of the 3 repeat outputs per
(model, level, sample) group, written back to each result row.
- line-number compliance: share of runs whose review reported line numbers.
- ANOVA + paired t-tests on detection_rate across model-level groups.
- Charts (heatmap / boxplot / grouped bar) saved as PNG.
- group_summary.csv + raw run-level export for thesis chapter 5.
"""
import base64
import csv
import json
import re
import sys
import uuid
from collections import defaultdict
from pathlib import Path
from statistics import mean
from app.analysis.service import AnalysisService
from app.db import SessionLocal, init_db
from app.models import ExperimentRun
def token_set(text: str) -> set:
return set(re.findall(r"[a-zA-Z_][a-zA-Z_0-9]*", (text or "").lower()))
def jaccard(a: set, b: set) -> float:
union = a | b
return len(a & b) / len(union) if union else 1.0
def main() -> None:
experiment_id = sys.argv[1]
out_dir = Path(sys.argv[2])
out_dir.mkdir(parents=True, exist_ok=True)
init_db()
db = SessionLocal()
svc = AnalysisService(db)
runs = (
db.query(ExperimentRun)
.filter(ExperimentRun.experiment_id == uuid.UUID(experiment_id), ExperimentRun.status == "done")
.all()
)
# --- stability: token-Jaccard across repeats ---
groups = defaultdict(list)
for run in runs:
level = run.template_version.template.level
groups[(run.model_id, level, str(run.sample_id))].append(run)
for key, group_runs in groups.items():
texts = [r.result.raw_output for r in group_runs if r.result and r.result.raw_output]
if len(texts) < 2:
continue
sets = [token_set(t) for t in texts]
pairs = [(i, j) for i in range(len(sets)) for j in range(i + 1, len(sets))]
score = mean([jaccard(sets[i], sets[j]) for i, j in pairs])
for r in group_runs:
if r.result:
r.result.stability_score = round(score, 4)
db.commit()
# --- per model-level summary ---
summary = defaultdict(lambda: defaultdict(list))
compliance = defaultdict(lambda: [0, 0])
for run in runs:
level = run.template_version.template.level
key = (run.model_id, level)
res = run.result
if not res:
continue
if res.detection_rate is not None:
summary[key]["detection_rate"].append(res.detection_rate)
if res.false_positive_rate is not None:
summary[key]["false_positive_rate"].append(res.false_positive_rate)
if res.coverage_rate is not None:
summary[key]["coverage_rate"].append(res.coverage_rate)
if res.stability_score is not None:
summary[key]["stability"].append(res.stability_score)
pf = res.parsed_findings or {}
verdict = pf.get("judge_verdict") if isinstance(pf, dict) else None
if verdict:
compliance[key][1] += 1
if verdict.get("lines_reported"):
compliance[key][0] += 1
rows = []
for (model, level) in sorted(summary):
g = summary[(model, level)]
c = compliance[(model, level)]
row = {
"model": model,
"level": level,
"n": len(g["detection_rate"]),
"detection_rate": round(mean(g["detection_rate"]), 4) if g["detection_rate"] else None,
"false_positive_rate": round(mean(g["false_positive_rate"]), 4) if g["false_positive_rate"] else None,
"coverage_rate": round(mean(g["coverage_rate"]), 4) if g["coverage_rate"] else None,
"coverage_n": len(g["coverage_rate"]),
"stability": round(mean(g["stability"]), 4) if g["stability"] else None,
"line_compliance": round(c[0] / c[1], 4) if c[1] else None,
}
rows.append(row)
print(row)
with open(out_dir / "group_summary.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
# --- per-language breakdown ---
lang_summary = defaultdict(list)
for run in runs:
if run.result and run.result.detection_rate is not None:
lang_summary[(run.model_id, run.sample.language)].append(run.result.detection_rate)
lang_rows = [
{"model": m, "language": lang, "n": len(v), "detection_rate": round(mean(v), 4)}
for (m, lang), v in sorted(lang_summary.items())
]
with open(out_dir / "language_summary.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["model", "language", "n", "detection_rate"])
writer.writeheader()
writer.writerows(lang_rows)
# --- ANOVA + paired t-tests ---
stats = {"anova": svc.run_anova(experiment_id), "paired_t_tests": {}}
models = sorted({r.model_id for r in runs})
levels = sorted({r.template_version.template.level for r in runs})
for m in models:
for a, b in [("L1", "L2"), ("L2", "L3"), ("L1", "L3")]:
if a in levels and b in levels:
key = f"{m}:{a} vs {b}"
stats["paired_t_tests"][key] = svc.run_paired_t_test(f"{m}-{a}", f"{m}-{b}", experiment_id)
with open(out_dir / "statistics.json", "w", encoding="utf-8") as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
print("ANOVA:", stats["anova"])
# --- charts ---
charts = svc.generate_charts(experiment_id)
for name, b64 in charts.items():
try:
(out_dir / f"{name}.png").write_bytes(base64.b64decode(b64))
print("chart saved:", name)
except Exception as e: # noqa: BLE001
print(f"chart {name} failed: {e}")
# --- raw run-level export ---
with open(out_dir / "runs.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["run_id", "model", "level", "language", "sample_id", "repeat",
"detection_rate", "false_positive_rate", "coverage_rate",
"stability_score", "latency_ms", "total_tokens"])
for run in runs:
res = run.result
writer.writerow([
str(run.id), run.model_id, run.template_version.template.level,
run.sample.language, str(run.sample_id), run.repeat_index,
res.detection_rate, res.false_positive_rate, res.coverage_rate,
res.stability_score, round(res.latency_ms or 0), (res.token_usage or {}).get("total_tokens"),
])
print("exports written to", out_dir)
if __name__ == "__main__":
main()