PDC / backend /tests /eval_harness.py
borndeveloper's picture
Deploy: case-study recommendation engine + ply yarn fix + weighted percentile removal
967454e
Raw
History Blame Contribute Delete
12.1 kB
"""Evaluation harness: run prediction engine against ground truth and produce reports."""
from __future__ import annotations
import json
import sys
import time
from collections import Counter
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.services.data_service import data_service
FIELDS = [
("greige_epi", "Greige EPI"),
("greige_ppi", "Greige PPI"),
("finish_epi", "FINISH EPI"),
("finish_ppi", "FINISH PPI"),
("reed_count", "Reed Count"),
("ends_per_dent", "Ends per dent"),
("reed_space", "Reed space"),
("finish_width", "FINISH WIDTH"),
("target_gsm", "FINISH GSM"),
]
def percent_error(pred_value: float | None, actual_value: float | None) -> float | None:
if pred_value is None or actual_value is None or actual_value == 0:
return None
return abs(pred_value - actual_value) / abs(actual_value) * 100
def run_eval(sample: pd.DataFrame, label: str) -> dict:
"""Run predictions on a sample and return metrics."""
results = []
errors = {fld: [] for fld, _ in FIELDS}
conf_dist = Counter()
case_dist = Counter()
fields_ok = {fld: Counter() for fld, _ in FIELDS if fld != "target_gsm"}
fields_ok["target_gsm"] = Counter()
t0 = time.time()
total = len(sample)
for i, (_, row) in enumerate(sample.iterrows()):
try:
pred = data_service.predict_construction({
"weave": str(row["weave"]),
"blend": str(row["blend"]),
"warp_count": float(row["warp_count"]),
"weft_count": float(row["weft_count"]),
"finish_epi": float(row["FINISH EPI"]),
"finish_ppi": float(row["FINISH PPI"]),
"target_gsm": float(row["FINISH GSM"]) if pd.notna(row.get("FINISH GSM")) else None,
})
except Exception as e:
continue
rec = pred.get("recommendation", {})
sp = pred.get("search_path", {})
dq = pred.get("data_quality", {})
conf_dist[dq.get("confidence", "very_low")] += 1
case_dist[sp.get("case_number", "?")] += 1
row_res = {"article": str(row.get("master_article", "")), "case": sp.get("case_number", "?"), "range": sp.get("range", "?"), "matches": sp.get("matches_found", 0)}
for fld, col in FIELDS:
p = rec.get(fld)
a = None
if col in row.index:
a = float(row[col]) if pd.notna(row.get(col)) else None
err = percent_error(p, a)
row_res[fld + "_pred"] = p
row_res[fld + "_actual"] = a
row_res[fld + "_err"] = err
if err is not None:
errors[fld].append(err)
# Count within-threshold
if err is not None:
if err <= 5:
fields_ok[fld]["within5"] += 1
elif err <= 10:
fields_ok[fld]["within10"] += 1
elif err <= 20:
fields_ok[fld]["within20"] += 1
else:
fields_ok[fld]["beyond20"] += 1
else:
fields_ok[fld]["no_actual"] += 1
results.append(row_res)
elapsed = time.time() - t0
report = {
"label": label,
"total_articles": total,
"predicted_articles": len(results),
"elapsed_seconds": round(elapsed, 1),
"avg_ms_per_article": round(elapsed / max(len(results), 1) * 1000, 1),
}
field_report = {}
for fld, _ in FIELDS:
arr = errors[fld]
if arr:
a = np.array(arr)
field_report[fld] = {
"MAE_pct": round(float(a.mean()), 3),
"Median_pct": round(float(np.median(a)), 3),
"P90_pct": round(float(np.percentile(a, 90)), 3),
"P10_pct": round(float(np.percentile(a, 10)), 3),
"Std_pct": round(float(a.std()), 3),
"Max_pct": round(float(a.max()), 3),
"count": int(len(a)),
"within_5pct": fields_ok[fld].get("within5", 0),
"within_10pct": fields_ok[fld].get("within10", 0),
"within_20pct": fields_ok[fld].get("within20", 0),
"beyond_20pct": fields_ok[fld].get("beyond20", 0),
}
else:
field_report[fld] = {"MAE_pct": None, "count": 0}
report["fields"] = field_report
report["confidence_dist"] = dict(conf_dist)
report["case_dist"] = {f"Case_{k}": v for k, v in case_dist.items()}
# Summary score (exclude finish_epi/finish_ppi: exact-by-design per case study)
scores = []
for fld in ["greige_epi", "greige_ppi", "target_gsm", "reed_count", "reed_space", "finish_width"]:
e = field_report.get(fld, {}).get("MAE_pct")
if e is not None:
scores.append(e)
report["overall_MAE_pct"] = round(float(np.mean(scores)), 3) if scores else None
return report
def get_sample_A() -> pd.DataFrame:
"""Original 25 diverse articles (fixed sample)."""
data_service.load_data()
df = data_service.df
test_candidates = df.dropna(subset=[
"weave", "blend", "warp_count", "weft_count",
"Greige EPI", "Greige PPI", "FINISH EPI", "FINISH PPI",
"Reed Count", "Ends per dent", "Reed space", "FINISH GSM", "FINISH WIDTH",
])
test_set = test_candidates[
test_candidates["weave"].isin([
"PLAIN", "2/1 S TWILL", "3/1 S TWILL", "4/1 S SATIN", "OXFORD", "DOBBY",
])
]
test_set = test_set[test_set["blend"].isin(["100%CO", "65%PES 35%CO", "97%CO 3%EA", "60%CO 40%PES"])]
test_set = test_set.sort_values("FINISH EPI").reset_index(drop=True)
idx = [int(i * len(test_set) / 26) for i in range(1, 26)]
return test_set.iloc[idx].copy()
def get_sample_B(seed: int) -> pd.DataFrame:
"""Random 25 articles (different per seed)."""
data_service.load_data()
df = data_service.df
test_candidates = df.dropna(subset=[
"weave", "blend", "warp_count", "weft_count",
"Greige EPI", "Greige PPI", "FINISH EPI", "FINISH PPI",
"Reed Count", "Ends per dent", "Reed space", "FINISH GSM", "FINISH WIDTH",
])
test_set = test_candidates[
test_candidates["weave"].isin([
"PLAIN", "2/1 S TWILL", "3/1 S TWILL", "4/1 S SATIN", "OXFORD", "DOBBY",
])
]
test_set = test_set[test_set["blend"].isin(["100%CO", "65%PES 35%CO", "97%CO 3%EA", "60%CO 40%PES"])]
sample = test_set.sample(n=25, random_state=seed)
return sample.copy()
def get_sample_C() -> dict:
"""Full validation report (200 articles)."""
return data_service.get_validation_report(sample_size=200, seed=42)
def compare_reports(baseline: dict, current: dict, label: str) -> str:
"""Generate comparison table between two reports."""
lines = [f"\n{'='*100}", f" COMPARISON: {label}", f"{'='*100}\n"]
lines.append(f"{'Field':18s} {'Baseline MAE%':>15s} {'Current MAE%':>15s} {'Delta':>12s} {'P90 Base':>10s} {'P90 Curr':>10s} {'Δ P90':>10s}")
lines.append("-" * 100)
for fld, _ in FIELDS:
b = baseline.get("fields", {}).get(fld, {})
c = current.get("fields", {}).get(fld, {})
b_mae = b.get("MAE_pct")
c_mae = c.get("MAE_pct")
b_p90 = b.get("P90_pct")
c_p90 = c.get("P90_pct")
if b_mae is not None and c_mae is not None:
delta = c_mae - b_mae
sign = "+" if delta > 0 else ""
color = "IMPROVED" if delta < 0 else ("REGRESSED" if delta > 0 else "SAME")
lines.append(f"{fld:18s} {b_mae:>14.3f}% {c_mae:>14.3f}% {sign}{delta:>10.3f}% {b_p90:>9.3f}% {c_p90:>9.3f}% {c_p90-b_p90 if b_p90 and c_p90 else 0:>+9.3f}%")
else:
lines.append(f"{fld:18s} {'N/A':>15s} {'N/A':>15s}")
# Confidence distribution
b_conf = baseline.get("confidence_dist", {})
c_conf = current.get("confidence_dist", {})
lines.append(f"\n{'Confidence':18s} {'Baseline':>15s} {'Current':>15s} {'Delta':>12s}")
lines.append("-" * 60)
for level in ["high", "medium", "low", "very_low"]:
bv = b_conf.get(level, 0)
cv = c_conf.get(level, 0)
d = cv - bv
lines.append(f"{level:18s} {bv:>15d} {cv:>15d} {d:+12d}")
# Overall
b_ov = baseline.get('overall_MAE_pct','-')
c_ov = current.get('overall_MAE_pct','-')
lines.append(f"\n{'Overall MAE%':18s} {str(b_ov):>15s} {str(c_ov):>15s}")
return "\n".join(lines)
def print_report(report: dict, title: str = "REPORT") -> None:
"""Print a readable report."""
print(f"\n{'='*100}")
print(f" {title}: {report['label']}")
print(f"{'='*100}")
print(f" Articles: {report['total_articles']} | Predicted: {report['predicted_articles']} | Time: {report['elapsed_seconds']}s ({report['avg_ms_per_article']}ms/art)")
print(f" Overall MAE: {report.get('overall_MAE_pct', 'N/A')}%")
print()
print(f" {'Field':18s} {'MAE%':>8s} {'Median%':>8s} {'P90%':>8s} {'P10%':>8s} {'≤5%':>6s} {'≤10%':>6s} {'≤20%':>6s} {'>20%':>6s}")
print(f" {'-'*80}")
for fld, _ in FIELDS:
f = report["fields"].get(fld, {})
if f.get("MAE_pct") is not None:
print(f" {fld:18s} {f['MAE_pct']:>7.2f}% {f['Median_pct']:>7.2f}% {f['P90_pct']:>7.2f}% {f['P10_pct']:>7.2f}% {f['within_5pct']:>5d} {f['within_10pct']:>5d} {f['within_20pct']:>5d} {f['beyond_20pct']:>5d}")
else:
print(f" {fld:18s} {'N/A':>8s}")
print()
print(f" Confidence: {report.get('confidence_dist', {})}")
print(f" Cases: {report.get('case_dist', {})}")
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--mode", choices=["baseline", "compare"], required=True)
ap.add_argument("--seed_b", type=int, default=42)
ap.add_argument("--baseline_file", type=str, default=None)
ap.add_argument("--report_file", type=str, default=None)
args = ap.parse_args()
data_service.load_data()
if args.mode == "baseline":
# Test Set A: original 25 fixed articles
sample_a = get_sample_A()
report_a = run_eval(sample_a, "Test Set A — 25 fixed diverse articles")
print_report(report_a, "BASELINE")
# Test Set B: random 25 new articles
sample_b = get_sample_B(args.seed_b)
report_b = run_eval(sample_b, f"Test Set B — 25 random articles (seed={args.seed_b})")
print_report(report_b, "BASELINE")
# Test Set C: full 200 validation
print(f"\n{'='*100}")
print(f" Test Set C — 200-sample validation report")
print(f"{'='*100}")
report_c = get_sample_C()
print(json.dumps(report_c, indent=2))
# Combined
combined = {
"set_a": report_a,
"set_b": report_b,
"set_c": report_c,
}
elif args.mode == "compare":
with open(args.baseline_file) as f:
baseline = json.load(f)
# Re-evaluate with current code
sample_a = get_sample_A()
report_a = run_eval(sample_a, "Test Set A — 25 fixed diverse articles")
print_report(report_a, "CURRENT")
sample_b = get_sample_B(args.seed_b)
report_b = run_eval(sample_b, f"Test Set B — 25 random articles (seed={args.seed_b})")
print_report(report_b, "CURRENT")
report_c = get_sample_C()
# Comparison
print(compare_reports(baseline["set_a"], report_a, "TEST SET A (Fixed 25)"))
print(compare_reports(baseline["set_b"], report_b, "TEST SET B (Random 25)"))
print(f"\n{'='*100}")
print(f" Test Set C — 200-sample validation report")
print(f"{'='*100}")
print(json.dumps(report_c, indent=2))
combined = {
"set_a": report_a,
"set_b": report_b,
"set_c": report_c,
}
if args.report_file:
with open(args.report_file, "w") as f:
json.dump(combined, f, indent=2)
print(f"\nReport saved to {args.report_file}")