Asmitha-28 commited on
Commit
0f96a74
Β·
verified Β·
1 Parent(s): d4f1f3e

Upload src\evaluate.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//evaluate.py +402 -0
src//evaluate.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/evaluate.py
2
+ # Evaluate SupportMind pipeline on validation set
3
+ # Produces comprehensive metrics for the results/ directory
4
+ # SupportMind v1.0 β€” Asmitha
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import time
10
+ import logging
11
+ import numpy as np
12
+ import pandas as pd
13
+ from collections import defaultdict
14
+
15
+ # Disable TF/JAX
16
+ os.environ['USE_TF'] = '0'
17
+ os.environ['USE_JAX'] = '0'
18
+ os.environ['USE_TORCH'] = '1'
19
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
20
+ os.environ['OMP_NUM_THREADS'] = '1'
21
+ os.environ['MKL_NUM_THREADS'] = '1'
22
+
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
26
+ logger = logging.getLogger(__name__)
27
+
28
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
29
+ DATA_DIR = os.path.join(BASE_DIR, 'data', 'processed')
30
+ MODEL_DIR = os.path.join(BASE_DIR, 'models', 'deberta_ultimate')
31
+ if not os.path.exists(os.path.join(MODEL_DIR, 'config.json')):
32
+ MODEL_DIR = os.path.join(BASE_DIR, 'models', 'ticket_classifier')
33
+ RESULTS_DIR = os.path.join(BASE_DIR, 'results')
34
+
35
+
36
+ def evaluate_router(val_df, n_passes=20):
37
+ """Evaluate the confidence-gated router on validation data."""
38
+ from confidence_router import ConfidenceGatedRouter, CATEGORY_MAP
39
+
40
+ model_path = MODEL_DIR if os.path.exists(os.path.join(MODEL_DIR, 'config.json')) else None
41
+ router = ConfidenceGatedRouter(model_path, device='cpu')
42
+
43
+ results = []
44
+ action_counts = defaultdict(int)
45
+ correct_by_action = defaultdict(int)
46
+ total_by_action = defaultdict(int)
47
+ confidences = []
48
+ entropies = []
49
+ latencies = []
50
+
51
+ logger.info(f"Evaluating {len(val_df)} samples with {n_passes} MC passes each...")
52
+
53
+ for i, row in val_df.iterrows():
54
+ text = row['text']
55
+ true_label = int(row['label'])
56
+ true_category = CATEGORY_MAP[true_label]
57
+
58
+ start = time.time()
59
+ result = router.route(text, n_passes=n_passes)
60
+ elapsed_ms = (time.time() - start) * 1000
61
+
62
+ pred_category = result['top_category']
63
+ action = result['action']
64
+ confidence = result['confidence']
65
+ entropy = result['entropy']
66
+
67
+ correct = pred_category == true_category
68
+
69
+ results.append({
70
+ 'true_label': true_label,
71
+ 'true_category': true_category,
72
+ 'pred_category': pred_category,
73
+ 'action': action,
74
+ 'confidence': confidence,
75
+ 'entropy': entropy,
76
+ 'correct': correct,
77
+ 'latency_ms': round(elapsed_ms, 1),
78
+ })
79
+
80
+ action_counts[action] += 1
81
+ total_by_action[action] += 1
82
+ if correct:
83
+ correct_by_action[action] += 1
84
+ confidences.append(confidence)
85
+ entropies.append(entropy)
86
+ latencies.append(elapsed_ms)
87
+
88
+ if (i + 1) % 50 == 0:
89
+ logger.info(f" Evaluated {i+1}/{len(val_df)} samples...")
90
+
91
+ # ── Compute aggregate metrics ──
92
+ total = len(results)
93
+ correct_total = sum(1 for r in results if r['correct'])
94
+ overall_accuracy = correct_total / total if total > 0 else 0
95
+
96
+ # Accuracy by action
97
+ accuracy_by_action = {}
98
+ for action in ['route', 'clarify', 'escalate']:
99
+ t = total_by_action.get(action, 0)
100
+ c = correct_by_action.get(action, 0)
101
+ accuracy_by_action[action] = {
102
+ 'count': t,
103
+ 'correct': c,
104
+ 'accuracy': round(c / t, 4) if t > 0 else 0,
105
+ 'percentage': round(t / total * 100, 1) if total > 0 else 0,
106
+ }
107
+
108
+ # Precision on auto-routed tickets (the key metric)
109
+ routed = [r for r in results if r['action'] == 'route']
110
+ precision_routed = sum(1 for r in routed if r['correct']) / len(routed) if routed else 0
111
+
112
+ # Confusion matrix (category-level)
113
+ categories = list(CATEGORY_MAP.values())
114
+ confusion = {true_cat: {pred_cat: 0 for pred_cat in categories} for true_cat in categories}
115
+ for r in results:
116
+ confusion[r['true_category']][r['pred_category']] += 1
117
+
118
+ # Per-category accuracy
119
+ per_category = {}
120
+ for cat in categories:
121
+ cat_results = [r for r in results if r['true_category'] == cat]
122
+ cat_correct = sum(1 for r in cat_results if r['correct'])
123
+ per_category[cat] = {
124
+ 'total': len(cat_results),
125
+ 'correct': cat_correct,
126
+ 'accuracy': round(cat_correct / len(cat_results), 4) if cat_results else 0,
127
+ }
128
+
129
+ # Confidence calibration (binned)
130
+ conf_bins = np.linspace(0, 1, 11)
131
+ calibration = []
132
+ for i in range(len(conf_bins) - 1):
133
+ low, high = conf_bins[i], conf_bins[i+1]
134
+ bin_results = [r for r in results if low <= r['confidence'] < high]
135
+ if bin_results:
136
+ bin_acc = sum(1 for r in bin_results if r['correct']) / len(bin_results)
137
+ bin_conf = np.mean([r['confidence'] for r in bin_results])
138
+ calibration.append({
139
+ 'bin': f"{low:.1f}-{high:.1f}",
140
+ 'count': len(bin_results),
141
+ 'accuracy': round(bin_acc, 4),
142
+ 'mean_confidence': round(bin_conf, 4),
143
+ })
144
+
145
+ report = {
146
+ 'summary': {
147
+ 'total_samples': total,
148
+ 'overall_accuracy': round(overall_accuracy, 4),
149
+ 'precision_auto_routed': round(precision_routed, 4),
150
+ 'mean_confidence': round(np.mean(confidences), 4),
151
+ 'mean_entropy': round(np.mean(entropies), 4),
152
+ 'mean_latency_ms': round(np.mean(latencies), 1),
153
+ 'p95_latency_ms': round(np.percentile(latencies, 95), 1),
154
+ 'mc_passes': n_passes,
155
+ },
156
+ 'routing_distribution': {
157
+ action: {
158
+ 'count': data['count'],
159
+ 'percentage': data['percentage'],
160
+ 'accuracy': data['accuracy'],
161
+ }
162
+ for action, data in accuracy_by_action.items()
163
+ },
164
+ 'per_category_accuracy': per_category,
165
+ 'confidence_calibration': calibration,
166
+ 'confusion_matrix': confusion,
167
+ }
168
+
169
+ return report, results
170
+
171
+
172
+ def evaluate_sla():
173
+ """Evaluate SLA breach predictor."""
174
+ from sla_predictor import SLABreachPredictor
175
+
176
+ sla_path = os.path.join(BASE_DIR, 'models', 'sla_predictor', 'sla_xgb.json')
177
+ predictor = SLABreachPredictor(sla_path)
178
+
179
+ # Test scenarios
180
+ scenarios = [
181
+ {'name': 'Low Risk', 'features': {
182
+ 'text_complexity_score': 5.0, 'agent_queue_depth': 3, 'customer_tier': 1,
183
+ 'hour_of_day': 10, 'day_of_week': 1, 'similar_ticket_avg_hrs': 1.5,
184
+ 'sentiment_score': 0.8, 'repeat_issue': 0, 'escalated_before': 0}},
185
+ {'name': 'Medium Risk', 'features': {
186
+ 'text_complexity_score': 10.0, 'agent_queue_depth': 15, 'customer_tier': 3,
187
+ 'hour_of_day': 14, 'day_of_week': 2, 'similar_ticket_avg_hrs': 4.5,
188
+ 'sentiment_score': -0.3, 'repeat_issue': 0, 'escalated_before': 0}},
189
+ {'name': 'High Risk', 'features': {
190
+ 'text_complexity_score': 16.0, 'agent_queue_depth': 30, 'customer_tier': 4,
191
+ 'hour_of_day': 23, 'day_of_week': 6, 'similar_ticket_avg_hrs': 12.0,
192
+ 'sentiment_score': -0.9, 'repeat_issue': 1, 'escalated_before': 1}},
193
+ ]
194
+
195
+ sla_results = []
196
+ for scenario in scenarios:
197
+ result = predictor.explain(scenario['features'])
198
+ sla_results.append({
199
+ 'scenario': scenario['name'],
200
+ 'breach_probability': result['breach_probability'],
201
+ 'risk_level': result['risk_level'],
202
+ 'factors': result['contributing_factors'],
203
+ })
204
+ logger.info(f" SLA {scenario['name']}: prob={result['breach_probability']:.3f}, risk={result['risk_level']}")
205
+
206
+ # Verify monotonicity (high risk > medium > low)
207
+ probs = [r['breach_probability'] for r in sla_results]
208
+ monotonic = probs[0] < probs[1] < probs[2]
209
+
210
+ return {
211
+ 'scenarios': sla_results,
212
+ 'monotonicity_check': monotonic,
213
+ 'model_type': 'XGBoost',
214
+ }
215
+
216
+
217
+ def evaluate_clarification():
218
+ """Evaluate clarification engine."""
219
+ from clarification_engine import ClarificationEngine
220
+
221
+ bank_path = os.path.join(BASE_DIR, 'data', 'clarification_bank.json')
222
+ engine = ClarificationEngine(bank_path)
223
+
224
+ # Test with different ambiguity profiles
225
+ test_cases = [
226
+ {'probs': [0.35, 0.30, 0.10, 0.08, 0.05, 0.04, 0.05, 0.03],
227
+ 'top_two': ['billing', 'technical_support'], 'label': 'billing_vs_tech'},
228
+ {'probs': [0.25, 0.10, 0.30, 0.08, 0.05, 0.04, 0.15, 0.03],
229
+ 'top_two': ['account_management', 'billing'], 'label': 'account_vs_billing'},
230
+ {'probs': [0.10, 0.35, 0.05, 0.30, 0.05, 0.05, 0.05, 0.05],
231
+ 'top_two': ['technical_support', 'feature_request'], 'label': 'tech_vs_feature'},
232
+ ]
233
+
234
+ clar_results = []
235
+ for tc in test_cases:
236
+ probs = np.array(tc['probs'])
237
+ result = engine.select_question(probs, tc['top_two'])
238
+ clar_results.append({
239
+ 'scenario': tc['label'],
240
+ 'question_id': result['question_id'],
241
+ 'question_text': result['question_text'],
242
+ 'expected_gain': result['expected_gain'],
243
+ 'fallback': result.get('fallback', False),
244
+ })
245
+ logger.info(f" Clarification [{tc['label']}]: gain={result['expected_gain']:.4f}")
246
+
247
+ return {
248
+ 'total_templates': len(engine.bank),
249
+ 'test_results': clar_results,
250
+ 'all_gains_positive': all(r['expected_gain'] > 0 for r in clar_results),
251
+ }
252
+
253
+
254
+ def evaluate_churn():
255
+ """Evaluate churn signal extractor."""
256
+ from churn_extractor import ChurnSignalExtractor
257
+
258
+ extractor = ChurnSignalExtractor()
259
+
260
+ test_threads = [
261
+ {'label': 'No Risk', 'thread': [
262
+ "Hi, I need help setting up the webhook integration.",
263
+ "Thanks for the quick response! That worked perfectly.",
264
+ ]},
265
+ {'label': 'Medium Risk', 'thread': [
266
+ "The export feature has been broken for two weeks.",
267
+ "This is the second time I've reported this issue.",
268
+ "I'm quite frustrated with the response time.",
269
+ ]},
270
+ {'label': 'Critical Risk', 'thread': [
271
+ "We've been having issues with the API for three weeks now.",
272
+ "This is the third time I'm reporting this. Still not fixed.",
273
+ "I'm very frustrated. We're looking at switching to a competitor.",
274
+ "If this isn't resolved by Friday, we'll cancel our subscription.",
275
+ ]},
276
+ ]
277
+
278
+ churn_results = []
279
+ for tc in test_threads:
280
+ result = extractor.extract(tc['thread'])
281
+ churn_results.append({
282
+ 'scenario': tc['label'],
283
+ 'churn_risk_score': result['churn_risk_score'],
284
+ 'risk_level': result['risk_level'],
285
+ 'competitor_mention': result['competitor_mention'],
286
+ 'cancellation_language': result['cancellation_language'],
287
+ 'recommendation': result['recommendation'],
288
+ })
289
+ logger.info(f" Churn [{tc['label']}]: score={result['churn_risk_score']:.3f}, level={result['risk_level']}")
290
+
291
+ # Verify risk ordering
292
+ scores = [r['churn_risk_score'] for r in churn_results]
293
+ monotonic = scores[0] < scores[1] < scores[2]
294
+
295
+ return {
296
+ 'scenarios': churn_results,
297
+ 'monotonicity_check': monotonic,
298
+ }
299
+
300
+
301
+ def evaluate_features():
302
+ """Evaluate feature extraction pipeline."""
303
+ from feature_extraction import FeatureExtractor
304
+
305
+ extractor = FeatureExtractor()
306
+
307
+ test_texts = [
308
+ "My invoice from last month shows $299 but my plan is $199.",
309
+ "The API endpoint /v2/export returns a 500 error when batch size exceeds 1000. URGENT!",
310
+ "Hey, quick question about the dashboard analytics feature.",
311
+ ]
312
+
313
+ feat_results = []
314
+ for text in test_texts:
315
+ features = extractor.extract(text)
316
+ feat_results.append({
317
+ 'text_preview': text[:60] + '...',
318
+ 'sentiment_score': features['sentiment_score'],
319
+ 'urgency_flags': features['urgency_flags'],
320
+ 'product_entities': features['product_entities'],
321
+ 'text_complexity': features['text_complexity_score'],
322
+ 'token_count': features['token_count'],
323
+ })
324
+
325
+ return {'test_results': feat_results}
326
+
327
+
328
+ def main():
329
+ os.makedirs(RESULTS_DIR, exist_ok=True)
330
+
331
+ logger.info("=" * 70)
332
+ logger.info("SupportMind β€” Comprehensive Evaluation")
333
+ logger.info("=" * 70)
334
+
335
+ full_report = {}
336
+
337
+ # 1. Router evaluation (the big one)
338
+ logger.info("\n[1/5] Evaluating Confidence-Gated Router...")
339
+ val_path = os.path.join(DATA_DIR, 'val.csv')
340
+ if os.path.exists(val_path):
341
+ val_df = pd.read_csv(val_path)
342
+ # Use a subset for faster evaluation (100 samples Γ— 20 MC passes)
343
+ eval_subset = val_df.sample(n=min(20, len(val_df)), random_state=42)
344
+ router_report, raw_results = evaluate_router(eval_subset, n_passes=20)
345
+ full_report['router'] = router_report
346
+
347
+ # Save raw predictions
348
+ raw_path = os.path.join(RESULTS_DIR, 'router_predictions.json')
349
+ with open(raw_path, 'w') as f:
350
+ json.dump(raw_results, f, indent=2)
351
+ logger.info(f" Raw predictions saved to {raw_path}")
352
+ else:
353
+ logger.warning(" Validation data not found, skipping router evaluation")
354
+
355
+ # 2. SLA evaluation
356
+ logger.info("\n[2/5] Evaluating SLA Breach Predictor...")
357
+ full_report['sla'] = evaluate_sla()
358
+
359
+ # 3. Clarification evaluation
360
+ logger.info("\n[3/5] Evaluating Clarification Engine...")
361
+ full_report['clarification'] = evaluate_clarification()
362
+
363
+ # 4. Churn evaluation
364
+ logger.info("\n[4/5] Evaluating Churn Signal Extractor...")
365
+ full_report['churn'] = evaluate_churn()
366
+
367
+ # 5. Feature extraction evaluation
368
+ logger.info("\n[5/5] Evaluating Feature Extraction Pipeline...")
369
+ full_report['features'] = evaluate_features()
370
+
371
+ # ── Save full report ──
372
+ report_path = os.path.join(RESULTS_DIR, 'evaluation_report.json')
373
+ with open(report_path, 'w') as f:
374
+ json.dump(full_report, f, indent=2)
375
+ logger.info(f"\n{'='*70}")
376
+ logger.info(f"Full evaluation report saved to: {report_path}")
377
+ logger.info(f"{'='*70}")
378
+
379
+ # ── Print summary ──
380
+ if 'router' in full_report:
381
+ s = full_report['router']['summary']
382
+ rd = full_report['router']['routing_distribution']
383
+ print(f"\n{'='*60}")
384
+ print(f" SUPPORTMIND EVALUATION SUMMARY")
385
+ print(f"{'='*60}")
386
+ print(f" Overall Accuracy: {s['overall_accuracy']:.1%}")
387
+ print(f" Precision (Auto-Routed): {s['precision_auto_routed']:.1%}")
388
+ print(f" Mean Confidence: {s['mean_confidence']:.4f}")
389
+ print(f" Mean Entropy: {s['mean_entropy']:.4f}")
390
+ print(f" Mean Latency: {s['mean_latency_ms']:.0f}ms")
391
+ print(f" P95 Latency: {s['p95_latency_ms']:.0f}ms")
392
+ print(f"\n Routing Distribution:")
393
+ for action in ['route', 'clarify', 'escalate']:
394
+ if action in rd:
395
+ d = rd[action]
396
+ print(f" {action.upper():10s}: {d['count']:4d} ({d['percentage']:5.1f}%) β€” acc {d['accuracy']:.1%}")
397
+ print(f"{'='*60}\n")
398
+
399
+
400
+ if __name__ == '__main__':
401
+ main()
402
+