"""Phase 3 label audit. Samples questions, hides the generated label, asks the user to label each one, then reports agreement between human and generated labels. Usage: python scripts/audit_labels.py python scripts/audit_labels.py --n 50 --source data/raw/questions.jsonl """ import argparse import json import os import random ORDERED_LABELS = ["Surface", "Mechanistic", "Critical"] INPUT_MAP = { "s": "Surface", "surface": "Surface", "m": "Mechanistic", "mechanistic": "Mechanistic", "c": "Critical", "critical": "Critical", } DEFINITIONS = { "Surface": "recall a fact/definition, or explain/interpret a single concept", "Mechanistic": "apply a method, or analyse/compare/trace mechanism", "Critical": "evaluate, critique, weigh trade-offs, or design something new", } def load_records(source): """Load all records from a JSONL file.""" records = [] with open(source, "r", encoding="utf-8") as handle: for line in handle: line = line.strip() if line: records.append(json.loads(line)) return records def stratified_sample(records, n, seed): """Sample n records with roughly equal counts per class.""" random.seed(seed) by_class = {label: [] for label in ORDERED_LABELS} for record in records: label = record.get("bloom_class") if label in by_class: by_class[label].append(record) per_class = max(1, n // len(ORDERED_LABELS)) sample = [] for label in ORDERED_LABELS: pool = by_class[label] random.shuffle(pool) sample.extend(pool[:per_class]) random.shuffle(sample) return sample def prompt_one(question, index, total): """Show a question and collect a blind label, or None to quit.""" print(f"\n[{index}/{total}] {question}") print(" Which cognitive level?") for label in ORDERED_LABELS: print(f" {label[0].lower()} = {label:12s} ({DEFINITIONS[label]})") while True: choice = input(" your label [s/m/c, or q to stop]: ").strip().lower() if choice == "q": return None if choice in INPUT_MAP: return INPUT_MAP[choice] print(" please enter s, m, c, or q") def summarise(annotations): """Print and return agreement stats.""" if not annotations: print("\nNo items labelled.") return None total = len(annotations) agree = sum(1 for a in annotations if a["human_label"] == a["true_label"]) overall = agree / total print("\n" + "=" * 46) print(f"Label fidelity audit (n = {total})") print(f" overall agreement : {overall:.1%}") print(f"\n {'class':12s} {'agree':>6s} {'n':>4s}") per_class = {} for label in ORDERED_LABELS: subset = [a for a in annotations if a["true_label"] == label] if subset: hits = sum(1 for a in subset if a["human_label"] == label) rate = hits / len(subset) per_class[label] = {"agreement": rate, "n": len(subset)} print(f" {label:12s} {rate:6.1%} {len(subset):4d}") else: per_class[label] = {"agreement": None, "n": 0} print("=" * 46) return {"overall_agreement": overall, "per_class": per_class, "n": total} def main(): """Run the interactive audit and save results.""" parser = argparse.ArgumentParser(description="Label-fidelity audit") parser.add_argument("--source", default="data/raw/questions.jsonl", help="JSONL to sample from") parser.add_argument("--n", type=int, default=50, help="sample size") parser.add_argument("--seed", type=int, default=7, help="sampling seed") parser.add_argument("--output", default="data/outputs/label_fidelity.json") args = parser.parse_args() if not os.path.exists(args.source): fallback = "data/processed/train.jsonl" print(f"{args.source} not found, falling back to {fallback}") args.source = fallback records = load_records(args.source) sample = stratified_sample(records, args.n, args.seed) print(f"Sampled {len(sample)} questions from {args.source}.") print("Label each by cognitive intent. The generated label is hidden.\n") annotations = [] for i, record in enumerate(sample, start=1): human = prompt_one(record["question"], i, len(sample)) if human is None: print("Stopping early.") break annotations.append({ "question": record["question"], "true_label": record["bloom_class"], "human_label": human, "agree": human == record["bloom_class"], }) summary = summarise(annotations) if summary is not None: os.makedirs(os.path.dirname(args.output), exist_ok=True) with open(args.output, "w", encoding="utf-8") as handle: json.dump({"summary": summary, "annotations": annotations}, handle, indent=2, ensure_ascii=False) print(f"\nSaved -> {args.output}") if __name__ == "__main__": main()