forked from Traqora/astroml
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcalibration_example.py
More file actions
357 lines (277 loc) · 12.3 KB
/
Copy pathcalibration_example.py
File metadata and controls
357 lines (277 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""Example usage of calibration curve visualization for fraud scores.
This example demonstrates how to use the calibration analysis tools
to evaluate fraud detection models in the AstroML framework.
"""
from __future__ import annotations
import sys
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from typing import Dict, Tuple
# Add the parent directory to the path to import astroml
# This allows the example to run from any working directory
script_dir = Path(__file__).parent.resolve()
repo_root = script_dir.parent
sys.path.insert(0, str(repo_root))
from astroml.validation.calibration import (
CalibrationAnalyzer,
create_sample_fraud_data
)
# Use script-relative paths for outputs
OUTPUT_DIR = script_dir
def create_realistic_fraud_models() -> Dict[str, Tuple[np.ndarray, np.ndarray]]:
"""
Create realistic fraud detection model outputs for comparison.
Returns:
Dictionary of model_name -> (y_true, y_prob)
"""
np.random.seed(42)
models = {}
# Model 1: Well-calibrated baseline model
y_true1, y_prob1 = create_sample_fraud_data(n_samples=2000, fraud_rate=0.08)
models['Baseline Model'] = (y_true1, y_prob1)
# Model 2: Overconfident model (common issue)
y_true2, y_prob2 = create_sample_fraud_data(n_samples=2000, fraud_rate=0.08)
# Make predictions more extreme (overconfident)
y_prob2 = np.power(y_prob2, 0.7) # Push probabilities toward 0 and 1
y_prob2 = np.clip(y_prob2, 0.01, 0.99)
models['Overconfident Model'] = (y_true2, y_prob2)
# Model 3: Underconfident model
y_true3, y_prob3 = create_sample_fraud_data(n_samples=2000, fraud_rate=0.08)
# Make predictions more conservative (underconfident)
y_prob3 = np.power(y_prob3, 1.5) # Push probabilities toward 0.5
y_prob3 = np.clip(y_prob3, 0.01, 0.99)
models['Underconfident Model'] = (y_true3, y_prob3)
# Model 4: Poorly calibrated model
y_true4, y_prob4 = create_sample_fraud_data(n_samples=2000, fraud_rate=0.08)
# Add systematic bias
y_prob4 = y_prob4 * 0.7 + 0.15 # Shift predictions upward
y_prob4 = np.clip(y_prob4, 0.01, 0.99)
models['Poorly Calibrated Model'] = (y_true4, y_prob4)
return models
def demonstrate_single_model_calibration():
"""Demonstrate calibration analysis for a single model."""
print("=" * 60)
print("Single Model Calibration Analysis")
print("=" * 60)
# Create sample data
y_true, y_prob = create_sample_fraud_data(n_samples=5000, fraud_rate=0.12)
# Initialize analyzer
analyzer = CalibrationAnalyzer(n_bins=15, strategy='quantile')
# Generate calibration plot
fig = analyzer.plot_calibration_curve(
y_true, y_prob,
model_name="Fraud Detection Model",
figsize=(15, 10)
)
# Generate detailed report
report = analyzer.generate_calibration_report(
y_true, y_prob,
model_name="Fraud Detection Model"
)
print("\nCalibration Report:")
print(report)
# Save the plot
output_path = OUTPUT_DIR / 'single_model_calibration.png'
fig.savefig(output_path, dpi=300, bbox_inches='tight')
print(f"\nPlot saved as '{output_path}'")
plt.show()
def demonstrate_multi_model_comparison():
"""Demonstrate calibration comparison across multiple models."""
print("\n" + "=" * 60)
print("Multi-Model Calibration Comparison")
print("=" * 60)
# Create multiple models
models_data = create_realistic_fraud_models()
# Initialize analyzer
analyzer = CalibrationAnalyzer(n_bins=12, strategy='uniform')
# Generate comparison plot
fig = analyzer.plot_multiple_models(
models_data,
figsize=(16, 12)
)
# Generate individual reports for each model
print("\nIndividual Model Reports:")
print("-" * 40)
for model_name, (y_true, y_prob) in models_data.items():
print(f"\n{model_name}:")
metrics = analyzer.compute_calibration_metrics(y_true, y_prob)
print(f" Brier Score: {metrics['brier_score']:.4f}")
print(f" ECE: {metrics['ece']:.4f}")
print(f" Overconfidence: {metrics['overconfidence']:.4f}")
print(f" Underconfidence: {metrics['underconfidence']:.4f}")
# Quick interpretation
if metrics['overconfidence'] > 0.05:
print(" → Model is OVERCONFIDENT")
elif metrics['underconfidence'] > 0.05:
print(" → Model is UNDERCONFIDENT")
else:
print(" → Model is reasonably calibrated")
# Save the comparison plot
output_path = OUTPUT_DIR / 'multi_model_calibration.png'
fig.savefig(output_path, dpi=300, bbox_inches='tight')
print(f"\nComparison plot saved as '{output_path}'")
plt.show()
def demonstrate_calibration_improvement():
"""Demonstrate calibration improvement techniques."""
print("\n" + "=" * 60)
print("Calibration Improvement Demonstration")
print("=" * 60)
# Create poorly calibrated model
y_true, y_prob_poor = create_sample_fraud_data(n_samples=3000, fraud_rate=0.1)
# Make it poorly calibrated
y_prob_poor = np.clip(y_prob_poor * 0.6 + 0.2, 0.01, 0.99)
# Apply simple temperature scaling (calibration improvement)
temperature = 1.5 # Temperature > 1 makes predictions less extreme
y_prob_calibrated = 1 / (1 + np.exp((np.log(y_prob_poor / (1 - y_prob_poor)) / temperature)))
# Compare before and after calibration
models_data = {
'Before Calibration': (y_true, y_prob_poor),
'After Temperature Scaling': (y_true, y_prob_calibrated)
}
analyzer = CalibrationAnalyzer(n_bins=10)
# Generate comparison
fig = analyzer.plot_multiple_models(models_data, figsize=(14, 10))
# Show improvement metrics
print("\nCalibration Improvement Metrics:")
print("-" * 40)
for model_name, (y_true, y_prob) in models_data.items():
metrics = analyzer.compute_calibration_metrics(y_true, y_prob)
print(f"\n{model_name}:")
print(f" Brier Score: {metrics['brier_score']:.4f}")
print(f" ECE: {metrics['ece']:.4f}")
print(f" Log Loss: {metrics['log_loss']:.4f}")
# Calculate improvement
metrics_before = analyzer.compute_calibration_metrics(y_true, y_prob_poor)
metrics_after = analyzer.compute_calibration_metrics(y_true, y_prob_calibrated)
ece_improvement = (metrics_before['ece'] - metrics_after['ece']) / metrics_before['ece'] * 100
brier_improvement = (metrics_before['brier_score'] - metrics_after['brier_score']) / metrics_before['brier_score'] * 100
print(f"\nImprovement:")
print(f" ECE Improvement: {ece_improvement:.1f}%")
print(f" Brier Score Improvement: {brier_improvement:.1f}%")
# Save the plot
output_path = OUTPUT_DIR / 'calibration_improvement.png'
fig.savefig(output_path, dpi=300, bbox_inches='tight')
print(f"\nImprovement plot saved as '{output_path}'")
plt.show()
def demonstrate_threshold_optimization():
"""Demonstrate threshold optimization based on calibration."""
print("\n" + "=" * 60)
print("Threshold Optimization Based on Calibration")
print("=" * 60)
# Create sample data
y_true, y_prob = create_sample_fraud_data(n_samples=5000, fraud_rate=0.08)
analyzer = CalibrationAnalyzer(n_bins=20)
# Test different thresholds
thresholds = np.arange(0.1, 0.9, 0.05)
results = {
'threshold': [],
'precision': [],
'recall': [],
'f1': [],
'calibration_error': []
}
for threshold in thresholds:
y_pred = (y_prob >= threshold).astype(int)
# Calculate metrics
tp = np.sum((y_pred == 1) & (y_true == 1))
fp = np.sum((y_pred == 1) & (y_true == 0))
fn = np.sum((y_pred == 0) & (y_true == 1))
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
# Calculate calibration error for predictions above threshold
mask = y_prob >= threshold
if np.sum(mask) > 0:
pred_mean = np.mean(y_prob[mask])
true_rate = np.mean(y_true[mask])
calibration_error = abs(pred_mean - true_rate)
else:
calibration_error = 0
results['threshold'].append(threshold)
results['precision'].append(precision)
results['recall'].append(recall)
results['f1'].append(f1)
results['calibration_error'].append(calibration_error)
# Create threshold optimization plot
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Threshold Optimization Analysis', fontsize=16, fontweight='bold')
# Precision-Recall curve
ax1 = axes[0, 0]
ax1.plot(results['recall'], results['precision'], 'b-', linewidth=2, marker='o')
ax1.set_xlabel('Recall')
ax1.set_ylabel('Precision')
ax1.set_title('Precision-Recall Curve')
ax1.grid(True, alpha=0.3)
# F1 Score vs Threshold
ax2 = axes[0, 1]
ax2.plot(results['threshold'], results['f1'], 'g-', linewidth=2, marker='s')
ax2.set_xlabel('Threshold')
ax2.set_ylabel('F1 Score')
ax2.set_title('F1 Score vs Threshold')
ax2.grid(True, alpha=0.3)
# Calibration Error vs Threshold
ax3 = axes[1, 0]
ax3.plot(results['threshold'], results['calibration_error'], 'r-', linewidth=2, marker='^')
ax3.set_xlabel('Threshold')
ax3.set_ylabel('Calibration Error')
ax3.set_title('Calibration Error vs Threshold')
ax3.grid(True, alpha=0.3)
# Combined metrics
ax4 = axes[1, 1]
ax4_twin = ax4.twinx()
line1 = ax4.plot(results['threshold'], results['f1'], 'g-', linewidth=2, label='F1 Score')
line2 = ax4_twin.plot(results['threshold'], results['calibration_error'], 'r-', linewidth=2, label='Calibration Error')
ax4.set_xlabel('Threshold')
ax4.set_ylabel('F1 Score', color='g')
ax4_twin.set_ylabel('Calibration Error', color='r')
ax4.tick_params(axis='y', labelcolor='g')
ax4_twin.tick_params(axis='y', labelcolor='r')
# Combined legend
lines = line1 + line2
labels = [l.get_label() for l in lines]
ax4.legend(lines, labels, loc='upper left')
ax4.set_title('Combined Metrics')
ax4.grid(True, alpha=0.3)
# Find optimal threshold (max F1 with reasonable calibration)
f1_array = np.array(results['f1'])
cal_error_array = np.array(results['calibration_error'])
# Filter for reasonable calibration (error < 0.1)
reasonable_mask = cal_error_array < 0.1
if np.any(reasonable_mask):
optimal_idx = np.argmax(f1_array[reasonable_mask])
optimal_threshold = results['threshold'][reasonable_mask][optimal_idx]
optimal_f1 = results['f1'][reasonable_mask][optimal_idx]
optimal_cal_error = results['calibration_error'][reasonable_mask][optimal_idx]
else:
# Fallback to max F1
optimal_idx = np.argmax(f1_array)
optimal_threshold = results['threshold'][optimal_idx]
optimal_f1 = results['f1'][optimal_idx]
optimal_cal_error = results['calibration_error'][optimal_idx]
print(f"\nOptimal Threshold Analysis:")
print(f" Optimal Threshold: {optimal_threshold:.3f}")
print(f" F1 Score: {optimal_f1:.3f}")
print(f" Calibration Error: {optimal_cal_error:.3f}")
plt.tight_layout()
output_path = OUTPUT_DIR / 'threshold_optimization.png'
fig.savefig(output_path, dpi=300, bbox_inches='tight')
print(f"\nThreshold optimization plot saved as '{output_path}'")
plt.show()
def main():
"""Run all calibration analysis examples."""
print("AstroML Calibration Analysis Examples")
print("=====================================")
# Create output directory
OUTPUT_DIR.mkdir(exist_ok=True)
# Run demonstrations
demonstrate_single_model_calibration()
demonstrate_multi_model_comparison()
demonstrate_calibration_improvement()
demonstrate_threshold_optimization()
print("\n" + "=" * 60)
print("All calibration analysis examples completed!")
print(f"Check the '{OUTPUT_DIR}' directory for generated plots.")
print("=" * 60)
if __name__ == "__main__":
main()