# -*- coding: utf-8 -*-
"""教師ラベル監査（追加課金ゼロ）。

目的: 「同じ観測状態なのに人間スコアが割れている」サンプルを見つけ、
モデルでは原理的に当てられないノイズ床（精度天井）を定量化する。

観測状態シグネチャ = LLM評価(visual_density/water/oil/boiling/photo) ＋ 濃度brix ＋ 時間slot。
同一シグネチャ群内の人間スコアのばらつき（std/range）を集計する。

対象は assessable=1（採点対象）。human_total_score と human_soup_score の両方。

出力(utf-8-sig):
  - 09_label_audit/label_contradictions_total.csv  （矛盾の大きい群）
  - 09_label_audit/label_contradictions_soup.csv
  - 09_label_audit/label_audit_summary.md
"""
from __future__ import annotations
import importlib.util, sys
from pathlib import Path
import numpy as np, pandas as pd

ROOT = Path(__file__).resolve().parents[2]
LEGACY = ROOT / "検証" / "scripts" / "run_downstream_model_comparison.py"
INPUT = ROOT / "local_batch_api" / "output_full" / "training_dataset.csv"
OUTDIR = ROOT / "精度改善_20260619" / "09_label_audit"
SIG = ["visual_density", "water_level", "oil_emulsification", "boiling_heat_state", "photo_quality", "brix", "slot"]


def load_legacy():
    spec = importlib.util.spec_from_file_location("legacy_cmp", LEGACY)
    m = importlib.util.module_from_spec(spec); sys.modules["legacy_cmp"] = m
    spec.loader.exec_module(m); return m


def audit(df, target, grade):
    g = df.groupby(SIG)
    # 群統計
    rows = []
    for key, sub in g:
        if len(sub) < 2:
            continue
        sc = sub[target].to_numpy()
        grades = grade(sc)
        rows.append({
            "n": len(sub),
            "score_min": float(sc.min()), "score_max": float(sc.max()),
            "score_range": float(sc.max() - sc.min()), "score_std": float(sc.std(ddof=0)),
            "grade_unique": len(set(grades.tolist())),
            "sample_ids": ",".join(map(str, sub["sample_id"].tolist())),
            **{k: v for k, v in zip(SIG, key)},
        })
    gr = pd.DataFrame(rows)
    # 全体ノイズ床: 同一シグネチャ群に属するサンプルについて、群平均からの偏差
    in_multi = g[target].transform("count") >= 2
    multi = df[in_multi].copy()
    grp_mean = g[target].transform("mean")
    resid = (df[target] - grp_mean)[in_multi]
    floor_mae = float(resid.abs().mean())          # 群平均予測でも残るMAE = ノイズ床の下限目安
    floor_within15 = float((resid.abs() <= 15).mean()) * 100
    # 群内でグレードが割れる割合
    grade_split = gr[gr["grade_unique"] >= 2]["n"].sum() if len(gr) else 0
    return gr.sort_values("score_range", ascending=False), {
        "対象件数(assessable=1)": int(len(df)),
        "複数サンプル群に属する件数": int(in_multi.sum()),
        "群数(n>=2)": int(len(gr)),
        "ノイズ床MAE(群平均予測の残差)": round(floor_mae, 2),
        "群平均予測でも±15点以内%": round(floor_within15, 1),
        "グレードが群内で割れる件数": int(grade_split),
        "score_range>=20の群数": int((gr["score_range"] >= 20).sum()) if len(gr) else 0,
        "score_range>=15の群数": int((gr["score_range"] >= 15).sum()) if len(gr) else 0,
    }


def main():
    OUTDIR.mkdir(parents=True, exist_ok=True)
    mod = load_legacy()
    f = mod.build_features(INPUT)
    f = f[(f["target_out_of_range"] == 0) & (f["assessable"] == 1)].reset_index(drop=True)
    f["sample_id"] = f["sample_id"].astype(str)

    summ = {}
    for target, name in [("human_total_score", "total"), ("human_soup_score", "soup")]:
        gr, s = audit(f, target, mod.grade)
        gr.to_csv(OUTDIR / f"label_contradictions_{name}.csv", index=False, encoding="utf-8-sig")
        summ[name] = s
        print(f"=== {name} ({target}) ===")
        for k, v in s.items():
            print(f"  {k}: {v}")
        print("  矛盾の大きい群 上位5:")
        for _, r in gr.head(5).iterrows():
            print(f"    range={r.score_range:.0f} (min{r.score_min:.0f}-max{r.score_max:.0f}) n={int(r.n)} brix={r.brix} dens={r.visual_density} ids={r.sample_ids}")
        print()

    lines = ["# 教師ラベル監査 サマリ", "",
             "観測状態（LLM評価＋濃度＋時間）が同一なのに人間スコアが割れている度合いを測定し、",
             "モデルで原理的に当てられない「ノイズ床（精度天井）」を定量化した。対象=assessable=1。", ""]
    for name in ["total", "soup"]:
        s = summ[name]
        lines.append(f"## {name}")
        lines.append("")
        lines.append("| 指標 | 値 |")
        lines.append("|---|---:|")
        for k, v in s.items():
            lines.append(f"| {k} | {v} |")
        lines.append("")
    (OUTDIR / "label_audit_summary.md").write_text("\n".join(lines), encoding="utf-8")


if __name__ == "__main__":
    main()
