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

1""" 

2classifier/validators/performance.py — Classification performance validator. 

3 

4Runs held-out sample validation against configured thresholds: 

5 - Precision ≥ 0.70 

6 - Recall ≥ 0.65 

7 - FPR ≤ 0.20 

8 

9Generates structured ValidationReport for each taxonomy category. 

10Used for Phase 1 end-to-end validation and M1 baseline establishment. 

11""" 

12 

13import logging 

14from datetime import datetime 

15 

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 

19 

20logger = logging.getLogger(__name__) 

21 

22 

23class PerformanceValidator: 

24 

25 def __init__(self, engine: ClassificationEngine, taxonomy: TaxonomyConfig): 

26 self.engine = engine 

27 self.taxonomy = taxonomy 

28 

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}") 

41 

42 tp = fp = tn = fn = 0 

43 

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 

48 

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 ) 

55 

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 

64 

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 

69 

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 

74 

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 ) 

93 

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 ) 

99 

100 return report 

101 

102 

103# --------------------------------------------------------------------------- 

104# Built-in held-out sample set for dehumanization (Phase 1 priority category) 

105# --------------------------------------------------------------------------- 

106 

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), 

117 

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), 

127 

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]