| """Gabungkan lembar anotasi manusia -> ground_truth_human.csv + Kappa asli. |
| |
| Input: 1-3 file CSV hasil isi annotation_sheet (kolom relevance terisi 0/1/2). |
| Output: |
| - eval/ground_truth_human.csv (konsensus majority vote; 1 annotator = |
| labelnya langsung dipakai) |
| - eval/kappa_report_human.md (Cohen's Kappa antar-manusia, kalau >= 2) |
| |
| Lalu jalankan ulang evaluasi terhadap GT manusia: |
| python -m app.evaluation.runner --queries ../eval/queries.json \ |
| --ground-truth ../eval/ground_truth_human.csv \ |
| --indexes-dir ../data/indexes --output ../eval/results_human.csv |
| python -m scripts.eval_smart --ground-truth ../eval/ground_truth_human.csv |
| |
| Usage: |
| python -m scripts.ingest_human_annotations --sheets ../eval/annotation_A.csv ../eval/annotation_B.csv |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import sys |
| from collections import Counter |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
|
|
| from app.evaluation import cohen_kappa, weighted_kappa |
| from app.evaluation.kappa import interpret_kappa |
|
|
| ROOT = Path(__file__).resolve().parents[2] |
|
|
|
|
| def load_sheet(path: Path) -> dict[tuple[str, str], int]: |
| labels: dict[tuple[str, str], int] = {} |
| skipped = 0 |
| with open(path, encoding="utf-8-sig") as f: |
| for row in csv.DictReader(f): |
| raw = (row.get("relevance") or "").strip() |
| if raw not in {"0", "1", "2"}: |
| skipped += 1 |
| continue |
| labels[(row["query_id"], row["doc_id"])] = int(raw) |
| if skipped: |
| print(f"[warn] {path.name}: {skipped} baris tanpa label valid di-skip") |
| return labels |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description="Ingest anotasi manusia") |
| parser.add_argument("--sheets", nargs="+", type=Path, required=True) |
| parser.add_argument( |
| "--output", type=Path, default=ROOT / "eval" / "ground_truth_human.csv") |
| args = parser.parse_args() |
|
|
| annotators = [load_sheet(p) for p in args.sheets] |
| for p, a in zip(args.sheets, annotators): |
| print(f"[load] {p.name}: {len(a)} label") |
| if not any(annotators): |
| print("[error] tidak ada label valid"); return 1 |
|
|
| all_keys = sorted(set().union(*[set(a) for a in annotators])) |
| consensus: dict[tuple[str, str], int] = {} |
| for key in all_keys: |
| votes = [a[key] for a in annotators if key in a] |
| top, top_count = Counter(votes).most_common(1)[0] |
| |
| consensus[key] = sorted(votes)[len(votes) // 2] if top_count == 1 else top |
|
|
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| with open(args.output, "w", encoding="utf-8", newline="") as f: |
| w = csv.writer(f) |
| w.writerow(["query_id", "doc_id", "relevance"]) |
| for (qid, did), rel in consensus.items(): |
| w.writerow([qid, did, rel]) |
| dist = Counter(consensus.values()) |
| print(f"[consensus] {len(consensus)} label -> {args.output}") |
| print(f"[distribusi] rel0={dist.get(0,0)} rel1={dist.get(1,0)} rel2={dist.get(2,0)}") |
|
|
| if len(annotators) >= 2: |
| shared = sorted(set.intersection(*[set(a) for a in annotators])) |
| lines = [ |
| "# Inter-Annotator Agreement (HUMAN)", "", |
| f"Annotator: {len(annotators)} manusia, shared items: {len(shared)}", "", |
| "| Pair | Cohen's Kappa | Interpretasi | Weighted Kappa |", |
| "|------|---------------|--------------|----------------|", |
| ] |
| for i in range(len(annotators)): |
| for j in range(i + 1, len(annotators)): |
| a = [annotators[i][k] for k in shared] |
| b = [annotators[j][k] for k in shared] |
| k = cohen_kappa(a, b, labels=[0, 1, 2]) |
| kw = weighted_kappa(a, b, labels=[0, 1, 2], weight_type="linear") |
| lines.append( |
| f"| {args.sheets[i].stem} vs {args.sheets[j].stem} " |
| f"| {k:.3f} | {interpret_kappa(k)} | {kw:.3f} |") |
| print(f"[kappa] {args.sheets[i].stem} vs {args.sheets[j].stem}: " |
| f"{k:.3f} ({interpret_kappa(k)})") |
| report = ROOT / "eval" / "kappa_report_human.md" |
| report.write_text("\n".join(lines) + "\n", encoding="utf-8") |
| print(f"[saved] {report}") |
| else: |
| print("[info] 1 annotator: Kappa butuh >= 2; label dipakai langsung " |
| "(disclose sebagai single-annotator di laporan)") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|