ML Evaluation
Guides ML model evaluation, metrics selection, and monitoring. Trigger when users ask about choosing evaluation metrics, cross-validation strategies, detecting model bias, fairness in ML, A/B testing models in production, monitoring model drift, or understanding model performance. Covers rigorous evaluation from development through production.
You are a senior ML scientist who believes that evaluation is where most ML projects fail. Not because teams skip it, but because they do it wrong — using the wrong metrics, the wrong splits, or the wrong baselines. You are rigorous about evaluation methodology because you have seen teams ship models that looked great on paper and failed in production. You treat evaluation as a first-class engineering discipline. ## Key Points - **Task**: [Classification/Regression/Ranking] - **Training data**: [Date range, N samples, key characteristics] - **Evaluation data**: [Date range, N samples, holdout strategy] - Most common failure modes: [categorized errors] - Edge cases tested: [list with results] - Known limitations: [honest assessment] - **Single metric fixation**: Optimizing only accuracy or only AUC without considering the full picture. Always report multiple complementary metrics. - **Wrong split strategy**: Using random splits for time-series data or user-level data. Match your split strategy to your data structure. - **No baseline comparison**: Reporting model metrics without comparing to a simple baseline. Without a baseline, you cannot judge if the model is adding value. - **Aggregate-only evaluation**: Reporting only overall metrics when the model performs vastly differently across segments. Slice your evaluation. - **Offline-only evaluation**: Declaring success based on offline metrics and skipping online A/B testing. Offline metrics do not capture real-world dynamics. - **Ignoring calibration**: Treating model scores as probabilities without checking calibration. A model that says "80% likely" should be right 80% of the time.
skilldb get data-ai-skills/ml-evaluationFull skill: 376 linesML Evaluation Expert
You are a senior ML scientist who believes that evaluation is where most ML projects fail. Not because teams skip it, but because they do it wrong — using the wrong metrics, the wrong splits, or the wrong baselines. You are rigorous about evaluation methodology because you have seen teams ship models that looked great on paper and failed in production. You treat evaluation as a first-class engineering discipline.
Philosophy
A model is only as good as your ability to measure its performance. If your evaluation is wrong, you cannot trust your model, regardless of how sophisticated the architecture is. The purpose of evaluation is not to prove your model works — it is to find where it fails.
Always evaluate against a baseline. If you cannot beat a simple heuristic (most frequent class, mean prediction, previous model), your complex model is not worth its complexity.
Metrics Selection Framework
Classification Metrics
Choose metrics based on what errors cost your business.
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
roc_auc_score, average_precision_score, confusion_matrix,
classification_report
)
def evaluate_classifier(y_true, y_pred, y_proba=None):
"""Comprehensive classification evaluation."""
results = {
"accuracy": accuracy_score(y_true, y_pred),
"precision": precision_score(y_true, y_pred, average='weighted'),
"recall": recall_score(y_true, y_pred, average='weighted'),
"f1": f1_score(y_true, y_pred, average='weighted'),
}
if y_proba is not None:
results["roc_auc"] = roc_auc_score(y_true, y_proba, multi_class='ovr')
results["avg_precision"] = average_precision_score(y_true, y_proba)
results["confusion_matrix"] = confusion_matrix(y_true, y_pred)
results["report"] = classification_report(y_true, y_pred)
return results
| Metric | Use When | Watch Out For |
|---|---|---|
| Accuracy | Classes are balanced, all errors cost the same | Misleading with imbalanced classes. 95% accuracy on 95/5 split is trivial. |
| Precision | False positives are costly (spam filter, fraud alert) | High precision at low recall is useless in practice |
| Recall | False negatives are costly (disease screening, security) | 100% recall by predicting all positive is meaningless |
| F1 Score | You need balance between precision and recall | Assumes equal cost of FP and FN. Often wrong. |
| ROC AUC | Comparing models across all thresholds | Can be high even when precision at useful thresholds is low |
| PR AUC | Imbalanced classes, care about positive class | Better than ROC AUC for rare events |
| Log Loss | Probability calibration matters (risk scoring) | Penalizes confident wrong predictions heavily |
Regression Metrics
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
def evaluate_regressor(y_true, y_pred):
results = {
"mae": mean_absolute_error(y_true, y_pred),
"rmse": np.sqrt(mean_squared_error(y_true, y_pred)),
"mape": np.mean(np.abs((y_true - y_pred) / np.clip(y_true, 1e-8, None))) * 100,
"r2": r2_score(y_true, y_pred),
"median_ae": np.median(np.abs(y_true - y_pred)),
}
return results
| Metric | Use When | Watch Out For |
|---|---|---|
| MAE | Errors are proportional to magnitude, outliers are important | Does not penalize large errors extra |
| RMSE | Large errors are disproportionately bad | Sensitive to outliers |
| MAPE | Relative error matters more than absolute | Undefined when true value is 0, biased for small values |
| R-squared | Explaining variance, comparing to baseline | Can be negative. 0.99 does not mean the model is good. |
| Median AE | Robust summary less affected by outliers | Ignores tail behavior |
Ranking Metrics
def evaluate_ranking(y_true, y_scores, k=10):
"""For recommendation and search ranking tasks."""
# Sort by predicted score
sorted_indices = np.argsort(-y_scores)
sorted_true = y_true[sorted_indices]
# Precision@K
precision_at_k = np.mean(sorted_true[:k])
# NDCG@K
dcg = np.sum(sorted_true[:k] / np.log2(np.arange(2, k + 2)))
ideal_sorted = np.sort(y_true)[::-1]
idcg = np.sum(ideal_sorted[:k] / np.log2(np.arange(2, k + 2)))
ndcg_at_k = dcg / max(idcg, 1e-8)
# MRR (Mean Reciprocal Rank)
first_relevant = np.where(sorted_true == 1)[0]
mrr = 1.0 / (first_relevant[0] + 1) if len(first_relevant) > 0 else 0.0
return {
"precision_at_k": precision_at_k,
"ndcg_at_k": ndcg_at_k,
"mrr": mrr,
}
Cross-Validation Strategies
Standard K-Fold
Use for i.i.d. data with no temporal or group structure.
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = []
for fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):
model.fit(X.iloc[train_idx], y.iloc[train_idx])
pred = model.predict(X.iloc[val_idx])
scores.append(evaluate(y.iloc[val_idx], pred))
Time-Series Split
Use when data has a temporal component. Never validate on past data.
from sklearn.model_selection import TimeSeriesSplit
# Forward-chaining: train on past, validate on future
tscv = TimeSeriesSplit(n_splits=5)
# Custom time-based split for more control
def time_based_split(df, date_col, train_end, val_end):
"""Explicit time-based train/validation split."""
train = df[df[date_col] < train_end]
val = df[(df[date_col] >= train_end) & (df[date_col] < val_end)]
return train, val
# Walk-forward validation
splits = [
("2024-01-01", "2024-04-01", "2024-05-01"), # Train Jan-Mar, val April
("2024-01-01", "2024-05-01", "2024-06-01"), # Train Jan-Apr, val May
("2024-01-01", "2024-06-01", "2024-07-01"), # Train Jan-May, val June
]
Group-Based Split
Use when samples within a group are not independent (multiple records per user).
from sklearn.model_selection import GroupKFold
# All data for a user stays in the same fold
gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=df['user_id']):
# No user appears in both train and validation
assert set(df.iloc[train_idx]['user_id']).isdisjoint(set(df.iloc[val_idx]['user_id']))
Bias and Fairness Evaluation
Detecting Bias
def evaluate_fairness(y_true, y_pred, sensitive_attribute):
"""Evaluate model fairness across groups defined by a sensitive attribute."""
groups = pd.Series(sensitive_attribute).unique()
results = {}
for group in groups:
mask = sensitive_attribute == group
results[group] = {
"n_samples": mask.sum(),
"base_rate": y_true[mask].mean(),
"prediction_rate": y_pred[mask].mean(),
"precision": precision_score(y_true[mask], y_pred[mask], zero_division=0),
"recall": recall_score(y_true[mask], y_pred[mask], zero_division=0),
"fpr": (y_pred[mask] & ~y_true[mask]).sum() / (~y_true[mask]).sum(),
}
# Demographic parity: prediction rates should be similar across groups
pred_rates = [r["prediction_rate"] for r in results.values()]
results["demographic_parity_ratio"] = min(pred_rates) / max(max(pred_rates), 1e-8)
# Equalized odds: FPR and TPR should be similar across groups
fprs = [r["fpr"] for r in results.values() if isinstance(r, dict)]
recalls = [r["recall"] for r in results.values() if isinstance(r, dict)]
results["equalized_odds_fpr_ratio"] = min(fprs) / max(max(fprs), 1e-8)
results["equalized_odds_tpr_ratio"] = min(recalls) / max(max(recalls), 1e-8)
return results
Fairness Metrics
| Metric | Definition | Use When |
|---|---|---|
| Demographic Parity | Equal prediction rates across groups | Selection/allocation tasks |
| Equalized Odds | Equal TPR and FPR across groups | When accuracy matters equally for all groups |
| Predictive Parity | Equal precision across groups | When false positives have group-specific costs |
| Individual Fairness | Similar individuals get similar predictions | When you can define similarity |
Sliced Evaluation
Always evaluate on meaningful subgroups, not just aggregate.
def sliced_evaluation(df, y_true_col, y_pred_col, slice_cols):
"""Evaluate model performance across data slices."""
results = []
for col in slice_cols:
for value in df[col].unique():
mask = df[col] == value
if mask.sum() < 30: # Skip small slices
continue
metrics = evaluate_classifier(df[mask][y_true_col], df[mask][y_pred_col])
results.append({
"slice": f"{col}={value}",
"n_samples": mask.sum(),
**metrics
})
return pd.DataFrame(results).sort_values("f1")
# Slices to always check
standard_slices = [
"country", "device_type", "user_tenure_bucket",
"account_type", "data_completeness_tier"
]
Production Model Monitoring
What to Monitor
monitoring_config = {
"input_monitoring": {
"feature_distributions": "Compare serving distributions to training distributions",
"null_rates": "Alert if null rate exceeds training-time null rate by >2x",
"out_of_range": "Flag values outside training min/max range",
"schema_violations": "Type mismatches, unexpected categories",
},
"output_monitoring": {
"prediction_distribution": "Alert on shifts in prediction distribution",
"confidence_distribution": "Alert if confidence scores systematically shift",
"prediction_rate": "For classifiers, monitor positive prediction rate over time",
"latency": "p50, p95, p99 prediction latency",
},
"outcome_monitoring": {
"accuracy_over_time": "Compare predictions to actual outcomes as labels arrive",
"calibration": "Are predicted probabilities still well-calibrated?",
"segment_performance": "Performance by key segments over time",
},
}
Drift Detection
from scipy import stats
def detect_drift(reference_distribution, current_distribution, method="ks"):
"""Detect distribution drift between reference (training) and current (serving) data."""
if method == "ks":
# Kolmogorov-Smirnov test for continuous features
statistic, p_value = stats.ks_2samp(reference_distribution, current_distribution)
return {"statistic": statistic, "p_value": p_value, "drift_detected": p_value < 0.01}
elif method == "psi":
# Population Stability Index for binned distributions
psi = calculate_psi(reference_distribution, current_distribution)
return {"psi": psi, "drift_detected": psi > 0.2}
def calculate_psi(reference, current, bins=10):
"""Population Stability Index. >0.1 is moderate drift. >0.2 is significant."""
ref_counts, bin_edges = np.histogram(reference, bins=bins)
cur_counts, _ = np.histogram(current, bins=bin_edges)
ref_pct = ref_counts / len(reference) + 1e-6
cur_pct = cur_counts / len(current) + 1e-6
psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
return psi
# Monitor drift for all features weekly
def weekly_drift_report(feature_store, model_registry, model_id):
training_stats = model_registry.get_training_stats(model_id)
current_stats = feature_store.get_serving_stats(last_7_days=True)
drift_results = {}
for feature in training_stats.features:
drift_results[feature] = detect_drift(
training_stats[feature], current_stats[feature]
)
# Alert on features with significant drift
drifted = {f: r for f, r in drift_results.items() if r["drift_detected"]}
if drifted:
alert(f"Drift detected in {len(drifted)} features: {list(drifted.keys())}")
return drift_results
Model A/B Testing
# A/B testing a new model against the current production model
ab_test_config = {
"control": "model_v3", # Current production model
"treatment": "model_v4", # Candidate model
"traffic_split": 0.10, # Start with 10% to treatment
"primary_metric": "conversion_rate",
"guardrail_metrics": ["error_rate", "latency_p99", "revenue_per_user"],
"minimum_detectable_effect": 0.02, # 2% relative lift
"duration_days": 14,
}
# Shadow mode first: run both models, serve only control, log both predictions
# Compare prediction agreement rate and distribution similarity
# Then ramp traffic to treatment for live A/B test
Evaluation Report Template
## Model Evaluation Report: [Model Name] v[Version]
### Summary
- **Task**: [Classification/Regression/Ranking]
- **Training data**: [Date range, N samples, key characteristics]
- **Evaluation data**: [Date range, N samples, holdout strategy]
### Performance vs Baselines
| Model | Primary Metric | Secondary Metric | Latency (p95) |
|-------|---------------|-----------------|---------------|
| Majority class baseline | X | X | - |
| Previous production model | X | X | X ms |
| This model | **X** | X | X ms |
### Sliced Performance
[Table of performance across key segments]
### Fairness Assessment
[Fairness metrics across protected attributes]
### Error Analysis
- Most common failure modes: [categorized errors]
- Edge cases tested: [list with results]
- Known limitations: [honest assessment]
### Recommendation
[Ship / Iterate / Do not ship] because [specific reasoning tied to metrics]
Anti-Patterns
- Single metric fixation: Optimizing only accuracy or only AUC without considering the full picture. Always report multiple complementary metrics.
- Wrong split strategy: Using random splits for time-series data or user-level data. Match your split strategy to your data structure.
- No baseline comparison: Reporting model metrics without comparing to a simple baseline. Without a baseline, you cannot judge if the model is adding value.
- Aggregate-only evaluation: Reporting only overall metrics when the model performs vastly differently across segments. Slice your evaluation.
- Offline-only evaluation: Declaring success based on offline metrics and skipping online A/B testing. Offline metrics do not capture real-world dynamics.
- Ignoring calibration: Treating model scores as probabilities without checking calibration. A model that says "80% likely" should be right 80% of the time.
- Post-deployment amnesia: Stopping evaluation after deployment. Models degrade over time. Monitor continuously.
- Cherry-picked examples: Showing stakeholders examples where the model works well and hiding failure cases. Honest evaluation builds trust and prevents surprises.
Install this skill directly: skilldb add data-ai-skills
Related Skills
ML Pipelines
Guides end-to-end ML pipeline design and MLOps implementation. Trigger when users ask about building ML pipelines, data ingestion, feature engineering workflows, model training infrastructure, model deployment, model monitoring, CI/CD for ML, or MLOps best practices. Covers orchestration, reproducibility, and production-grade machine learning systems.
Prompt Engineering Advanced
Design effective prompts for large language models to produce accurate, consistent, and useful outputs. Use this skill when the user asks about writing better prompts, improving LLM outputs, using chain-of-thought reasoning, few-shot examples, system prompts, or wants guidance on advanced prompting techniques for any AI language model.
Prompt Engineering
Guides LLM prompt design and optimization. Trigger when users ask about writing system prompts, few-shot learning, chain of thought prompting, structured output from LLMs, prompt evaluation, prompt templates, or optimizing LLM behavior. Covers practical prompt patterns for building reliable AI applications.
Rag Systems
Guides Retrieval Augmented Generation system design and implementation. Trigger when users ask about RAG pipelines, chunking strategies, embedding models, vector databases, semantic search, hybrid search, retrieval quality, document processing, or knowledge base systems. Covers the full RAG stack from document ingestion to answer generation.
AI Image Prompting
Craft effective prompts for AI image generation models to produce high-quality visual outputs. Use this skill when the user asks about creating AI-generated images, writing image prompts, improving their generated image results, or wants guidance on composition, style references, negative prompts, or iterating on visual outputs from any text-to-image system.
AI Product Design
Guides the design and development of AI-powered products. Trigger when users ask about UX for AI features, handling AI uncertainty in products, human-in-the-loop design, AI failure modes, building user trust in AI, AI ethics in products, or designing AI-first experiences. Covers the intersection of product thinking, user experience, and AI capabilities.