Coverage for narrative_harm_classifier/classifier/validators/performance.py: 95%
40 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 13:25 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-20 13:25 +0000
1"""
2classifier/validators/performance.py — Classification performance validator.
4Runs held-out sample validation against configured thresholds:
5 - Precision ≥ 0.70
6 - Recall ≥ 0.65
7 - FPR ≤ 0.20
9Generates structured ValidationReport for each taxonomy category.
10Used for Phase 1 end-to-end validation and M1 baseline establishment.
11"""
13import logging
14from datetime import datetime
16from narrative_harm_classifier.core.models import ClassifyRequest, ValidationSample, ValidationReport
17from narrative_harm_classifier.classifier.rules.engine import ClassificationEngine
18from narrative_harm_classifier.classifier.taxonomy.loader import TaxonomyConfig
20logger = logging.getLogger(__name__)
23class PerformanceValidator:
25 def __init__(self, engine: ClassificationEngine, taxonomy: TaxonomyConfig):
26 self.engine = engine
27 self.taxonomy = taxonomy
29 def validate_category(
30 self,
31 category_name: str,
32 samples: list[ValidationSample],
33 ) -> ValidationReport:
34 """
35 Run held-out sample validation for a single taxonomy category.
36 Returns a ValidationReport with precision, recall, FPR, and pass/fail.
37 """
38 cat_spec = self.taxonomy.get_category(category_name)
39 if not cat_spec:
40 raise ValueError(f"Category '{category_name}' not found in taxonomy v{self.taxonomy.version}")
42 tp = fp = tn = fn = 0
44 for sample in samples:
45 result = self.engine.classify(ClassifyRequest(text=sample.text))
46 predicted_harmful = result.is_harmful
47 predicted_category = result.harm_category
49 # Category match: either exact category or any harm (if expected_category not specified)
50 category_match = (
51 sample.expected_category is None
52 or predicted_category == sample.expected_category
53 or (predicted_harmful and sample.expected_category is None)
54 )
56 if sample.expected_is_harmful and predicted_harmful and category_match:
57 tp += 1
58 elif not sample.expected_is_harmful and predicted_harmful:
59 fp += 1
60 elif not sample.expected_is_harmful and not predicted_harmful:
61 tn += 1
62 elif sample.expected_is_harmful and not predicted_harmful:
63 fn += 1
65 precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
66 recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
67 fpr = fp / (fp + tn) if (fp + tn) > 0 else 0.0
68 f1 = (2 * precision * recall / (precision + recall)) if (precision + recall) > 0 else 0.0
70 meets_precision = precision >= cat_spec.precision_min
71 meets_recall = recall >= cat_spec.recall_min
72 meets_fpr = fpr <= cat_spec.fpr_max
73 passes = meets_precision and meets_recall and meets_fpr
75 report = ValidationReport(
76 category=category_name,
77 taxonomy_version=self.taxonomy.version,
78 sample_count=len(samples),
79 true_positives=tp,
80 false_positives=fp,
81 true_negatives=tn,
82 false_negatives=fn,
83 precision=round(precision, 4),
84 recall=round(recall, 4),
85 fpr=round(fpr, 4),
86 f1=round(f1, 4),
87 meets_precision_threshold=meets_precision,
88 meets_recall_threshold=meets_recall,
89 meets_fpr_threshold=meets_fpr,
90 passes=passes,
91 validated_at=datetime.utcnow(),
92 )
94 logger.info(
95 f"Validation [{category_name}] v{self.taxonomy.version}: "
96 f"P={precision:.3f} R={recall:.3f} FPR={fpr:.3f} "
97 f"{'PASS' if passes else 'FAIL'}"
98 )
100 return report
103# ---------------------------------------------------------------------------
104# Built-in held-out sample set for dehumanization (Phase 1 priority category)
105# ---------------------------------------------------------------------------
107DEHUMANIZATION_VALIDATION_SAMPLES: list[ValidationSample] = [
108 # True positives — clear dehumanization (all include explicit group identity)
109 ValidationSample(text="These immigrants are nothing but vermin infesting our cities", expected_is_harmful=True),
110 ValidationSample(text="Black people are subhuman animals that don't deserve rights", expected_is_harmful=True),
111 ValidationSample(text="Muslim followers are demonic servants of evil", expected_is_harmful=True),
112 ValidationSample(text="Women are just objects that exist to serve men", expected_is_harmful=True),
113 ValidationSample(text="All immigrants are criminals and rapists, deport them all", expected_is_harmful=True),
114 ValidationSample(text="Asian people are like cockroaches, infesting everything they touch", expected_is_harmful=True),
115 ValidationSample(text="These African migrants are not even human, they are beasts", expected_is_harmful=True),
116 ValidationSample(text="Jewish people are demonic and evil", expected_is_harmful=True),
118 # True negatives — not harmful
119 ValidationSample(text="The animal rights movement has gained significant momentum this year", expected_is_harmful=False),
120 ValidationSample(text="Crime statistics vary significantly across different demographic groups", expected_is_harmful=False),
121 ValidationSample(text="The documentary featured wildlife in their natural habitat", expected_is_harmful=False),
122 ValidationSample(text="Immigration policy continues to be a divisive political issue", expected_is_harmful=False),
123 ValidationSample(text="The theological debate between scholars was fascinating", expected_is_harmful=False),
124 ValidationSample(text="Feminist scholars have written extensively on objectification theory", expected_is_harmful=False),
125 ValidationSample(text="The pest control company eliminated the cockroach infestation", expected_is_harmful=False),
126 ValidationSample(text="Property ownership laws differ significantly between countries", expected_is_harmful=False),
128 # Edge cases — ambiguous context
129 ValidationSample(text="Politicians often behave like animals at these debates", expected_is_harmful=False),
130 ValidationSample(text="The satanic ritual in the horror movie was very realistic", expected_is_harmful=False),
131 ValidationSample(text="These violent criminals are a plague on society", expected_is_harmful=False),
132]