Skill Tracer
Skills › Scientific Agent Skills › scikit-learn › model_evaluation.md

model_evaluation.md

620 lines · skills/scikit-learn/references/model_evaluation.md at 5f2bc03

Open on GitHub
1# Model Selection and Evaluation Reference
2
3Targets scikit-learn 1.9.1. Snippets with caller-supplied data/columns are illustrative; fit all learned preprocessing inside the training folds when estimating predictive performance.
4

Model Selection and Evaluation Reference

Targets scikit-learn 1.9.1. Snippets with caller-supplied data/columns are illustrative; fit all learned preprocessing inside the training folds when estimating predictive performance.

Raw lines 1 to 4
5## Overview
6
7Comprehensive guide for evaluating models, tuning hyperparameters, and selecting the best model using scikit-learn's model selection tools.
8

Overview

Comprehensive guide for evaluating models, tuning hyperparameters, and selecting the best model using scikit-learn's model selection tools.

Raw lines 5 to 8
9## Train-Test Split

Train-Test Split

Raw lines 9 to 10
11### Basic Splitting
13```python
14from sklearn.model_selection import train_test_split
16# Basic split (default 75/25)
17X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
19# With stratification (preserves class distribution)
20X_train, X_test, y_train, y_test = train_test_split(
21 X, y, test_size=0.25, stratify=y, random_state=42
24# Three-way split (train/val/test)
25X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42)
26X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
27```
29Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use `.iloc[index]` for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Basic Splitting

from sklearn.model_selection import train_test_split

# Basic split (default 75/25)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# With stratification (preserves class distribution)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, stratify=y, random_state=42
)

# Three-way split (train/val/test)
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)

Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use .iloc[index] for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Raw lines 11 to 30
31## Cross-Validation
33#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use `.iloc[index]` for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Cross-Validation

#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use .iloc[index] for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Raw lines 31 to 34
35## Cross-Validation Strategies
37**KFold**
38- Standard k-fold cross-validation
39- Splits data into k consecutive folds
40```python
41from sklearn.model_selection import KFold
43kf = KFold(n_splits=5, shuffle=True, random_state=42)
44for train_idx, val_idx in kf.split(X):
45 X_train, X_val = X[train_idx], X[val_idx]
46 y_train, y_val = y[train_idx], y[val_idx]
47```
49**StratifiedKFold**
50- Preserves class distribution in each fold
51- Use for imbalanced classification
52```python
53from sklearn.model_selection import StratifiedKFold
55skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
56for train_idx, val_idx in skf.split(X, y):
57 X_train, X_val = X[train_idx], X[val_idx]
58 y_train, y_val = y[train_idx], y[val_idx]
59```
61**TimeSeriesSplit**
62- For time series data
63- Respects temporal order
64```python
65from sklearn.model_selection import TimeSeriesSplit
67tscv = TimeSeriesSplit(n_splits=5)
68for train_idx, val_idx in tscv.split(X):
69 X_train, X_val = X[train_idx], X[val_idx]
70 y_train, y_val = y[train_idx], y[val_idx]
71```
73**GroupKFold**
74- Ensures samples from same group don't appear in both train and validation
75- Use when samples are not independent
76```python
77from sklearn.model_selection import GroupKFold
79gkf = GroupKFold(n_splits=5)
80for train_idx, val_idx in gkf.split(X, y, groups=group_ids):
81 X_train, X_val = X[train_idx], X[val_idx]
82 y_train, y_val = y[train_idx], y[val_idx]
83```
85**LeaveOneOut (LOO)**
86- Each sample used as validation set once
87- Use for very small datasets
88- Computationally expensive
89```python
90from sklearn.model_selection import LeaveOneOut
92loo = LeaveOneOut()
93for train_idx, val_idx in loo.split(X):
94 X_train, X_val = X[train_idx], X[val_idx]
95 y_train, y_val = y[train_idx], y[val_idx]
96```
98#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use `.iloc[index]` for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Cross-Validation Strategies

KFold

  • Standard k-fold cross-validation
  • Splits data into k consecutive folds
from sklearn.model_selection import KFold

kf = KFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in kf.split(X):
    X_train, X_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

StratifiedKFold

  • Preserves class distribution in each fold
  • Use for imbalanced classification
from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y):
    X_train, X_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

TimeSeriesSplit

  • For time series data
  • Respects temporal order
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_idx, val_idx in tscv.split(X):
    X_train, X_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

GroupKFold

  • Ensures samples from same group don't appear in both train and validation
  • Use when samples are not independent
from sklearn.model_selection import GroupKFold

gkf = GroupKFold(n_splits=5)
for train_idx, val_idx in gkf.split(X, y, groups=group_ids):
    X_train, X_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

LeaveOneOut (LOO)

  • Each sample used as validation set once
  • Use for very small datasets
  • Computationally expensive
from sklearn.model_selection import LeaveOneOut

loo = LeaveOneOut()
for train_idx, val_idx in loo.split(X):
    X_train, X_val = X[train_idx], X[val_idx]
    y_train, y_val = y[train_idx], y[val_idx]

#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use .iloc[index] for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Raw lines 35 to 99
100## Cross-Validation Functions
102**cross_val_score**
103- Evaluate model using cross-validation
104- Returns array of scores
105```python
106from sklearn.model_selection import cross_val_score
107from sklearn.ensemble import RandomForestClassifier
109model = RandomForestClassifier(n_estimators=100, random_state=42)
110scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
112print(f"Scores: {scores}")
113print(f"Mean: {scores.mean():.3f} (fold SD {scores.std():.3f})")
114```
116**cross_validate**
117- More comprehensive than cross_val_score
118- Can return multiple metrics and fit times
119```python
120from sklearn.model_selection import cross_validate
122model = RandomForestClassifier(n_estimators=100, random_state=42)
123cv_results = cross_validate(
124 model, X, y, cv=5,
125 scoring=['accuracy', 'precision_macro', 'recall_macro', 'f1_macro'],
126 return_train_score=True,
127 return_estimator=True # Returns fitted estimators
130print(f"Test accuracy: {cv_results['test_accuracy'].mean():.3f}")
131print(f"Test precision: {cv_results['test_precision_macro'].mean():.3f}")
132print(f"Fit time: {cv_results['fit_time'].mean():.3f}s")
133```
135**cross_val_predict**
136- Get predictions for each sample when it was in validation set
137- Useful for analyzing errors; requires each sample to appear in exactly one test fold. Pooled prediction metrics need not equal average fold scores
138```python
139from sklearn.model_selection import cross_val_predict
141model = RandomForestClassifier(n_estimators=100, random_state=42)
142y_pred = cross_val_predict(model, X, y, cv=5)
144# Now can analyze predictions vs actual
145from sklearn.metrics import confusion_matrix
146cm = confusion_matrix(y, y_pred)
147```

Cross-Validation Functions

cross_val_score

  • Evaluate model using cross-validation
  • Returns array of scores
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier(n_estimators=100, random_state=42)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print(f"Scores: {scores}")
print(f"Mean: {scores.mean():.3f} (fold SD {scores.std():.3f})")

cross_validate

  • More comprehensive than cross_val_score
  • Can return multiple metrics and fit times
from sklearn.model_selection import cross_validate

model = RandomForestClassifier(n_estimators=100, random_state=42)
cv_results = cross_validate(
    model, X, y, cv=5,
    scoring=['accuracy', 'precision_macro', 'recall_macro', 'f1_macro'],
    return_train_score=True,
    return_estimator=True  # Returns fitted estimators
)

print(f"Test accuracy: {cv_results['test_accuracy'].mean():.3f}")
print(f"Test precision: {cv_results['test_precision_macro'].mean():.3f}")
print(f"Fit time: {cv_results['fit_time'].mean():.3f}s")

cross_val_predict

  • Get predictions for each sample when it was in validation set
  • Useful for analyzing errors; requires each sample to appear in exactly one test fold. Pooled prediction metrics need not equal average fold scores
from sklearn.model_selection import cross_val_predict

model = RandomForestClassifier(n_estimators=100, random_state=42)
y_pred = cross_val_predict(model, X, y, cv=5)

# Now can analyze predictions vs actual
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y, y_pred)
Raw lines 100 to 148
149## Hyperparameter Tuning

Hyperparameter Tuning

Raw lines 149 to 150
151### Grid Search
153**GridSearchCV**
154- Exhaustive search over parameter grid
155- Tests all combinations
156```python
157from sklearn.model_selection import GridSearchCV
158from sklearn.ensemble import RandomForestClassifier
160param_grid = {
161 'n_estimators': [50, 100, 200],
162 'max_depth': [5, 10, 15, None],
163 'min_samples_split': [2, 5, 10],
164 'min_samples_leaf': [1, 2, 4]
167model = RandomForestClassifier(random_state=42)
168grid_search = GridSearchCV(
169 model, param_grid,
170 cv=5,
171 scoring='accuracy',
172 n_jobs=-1, # Use all CPU cores
173 verbose=1
176grid_search.fit(X_train, y_train)
178print(f"Best parameters: {grid_search.best_params_}")
179print(f"Best cross-validation score: {grid_search.best_score_:.3f}")
180print(f"Test score: {grid_search.score(X_test, y_test):.3f}")
182# Access best model
183best_model = grid_search.best_estimator_
185# View all results
186import pandas as pd
187results_df = pd.DataFrame(grid_search.cv_results_)
188```

Grid Search

GridSearchCV

  • Exhaustive search over parameter grid
  • Tests all combinations
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, 15, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

model = RandomForestClassifier(random_state=42)
grid_search = GridSearchCV(
    model, param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,  # Use all CPU cores
    verbose=1
)

grid_search.fit(X_train, y_train)

print(f"Best parameters: {grid_search.best_params_}")
print(f"Best cross-validation score: {grid_search.best_score_:.3f}")
print(f"Test score: {grid_search.score(X_test, y_test):.3f}")

# Access best model
best_model = grid_search.best_estimator_

# View all results
import pandas as pd
results_df = pd.DataFrame(grid_search.cv_results_)
Raw lines 151 to 189
190### Randomized Search
192**RandomizedSearchCV**
193- Samples random combinations from parameter distributions
194- More efficient for large search spaces
195```python
196from sklearn.model_selection import RandomizedSearchCV
197from scipy.stats import randint, uniform
199param_distributions = {
200 'n_estimators': randint(50, 300),
201 'max_depth': [5, 10, 15, 20, None],
202 'min_samples_split': randint(2, 20),
203 'min_samples_leaf': randint(1, 10),
204 'max_features': uniform(0.1, 0.9) # Continuous distribution
207model = RandomForestClassifier(random_state=42)
208random_search = RandomizedSearchCV(
209 model, param_distributions,
210 n_iter=100, # Number of parameter settings sampled
211 cv=5,
212 scoring='accuracy',
213 n_jobs=-1,
214 verbose=1,
215 random_state=42
218random_search.fit(X_train, y_train)
220print(f"Best parameters: {random_search.best_params_}")
221print(f"Best score: {random_search.best_score_:.3f}")
222```

Randomized Search

RandomizedSearchCV

  • Samples random combinations from parameter distributions
  • More efficient for large search spaces
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

param_distributions = {
    'n_estimators': randint(50, 300),
    'max_depth': [5, 10, 15, 20, None],
    'min_samples_split': randint(2, 20),
    'min_samples_leaf': randint(1, 10),
    'max_features': uniform(0.1, 0.9)  # Continuous distribution
}

model = RandomForestClassifier(random_state=42)
random_search = RandomizedSearchCV(
    model, param_distributions,
    n_iter=100,  # Number of parameter settings sampled
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1,
    random_state=42
)

random_search.fit(X_train, y_train)

print(f"Best parameters: {random_search.best_params_}")
print(f"Best score: {random_search.best_score_:.3f}")
Raw lines 190 to 223
224### Successive Halving
226**HalvingGridSearchCV / HalvingRandomSearchCV**
227- Iteratively selects best candidates using successive halving
228- More efficient than exhaustive search
229```python
230from sklearn.experimental import enable_halving_search_cv
231from sklearn.model_selection import HalvingGridSearchCV
233param_grid = {
234 'n_estimators': [50, 100, 200, 300],
235 'max_depth': [5, 10, 15, 20, None],
236 'min_samples_split': [2, 5, 10, 20]
239model = RandomForestClassifier(random_state=42)
240halving_search = HalvingGridSearchCV(
241 model, param_grid,
242 cv=5,
243 factor=3, # Roughly one third of candidates survive each iteration
244 resource='n_samples', # For n_estimators, remove it from the grid and set max_resources
245 max_resources='auto',
246 random_state=42
249halving_search.fit(X_train, y_train)
250print(f"Best parameters: {halving_search.best_params_}")
251```

Successive Halving

HalvingGridSearchCV / HalvingRandomSearchCV

  • Iteratively selects best candidates using successive halving
  • More efficient than exhaustive search
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV

param_grid = {
    'n_estimators': [50, 100, 200, 300],
    'max_depth': [5, 10, 15, 20, None],
    'min_samples_split': [2, 5, 10, 20]
}

model = RandomForestClassifier(random_state=42)
halving_search = HalvingGridSearchCV(
    model, param_grid,
    cv=5,
    factor=3,  # Roughly one third of candidates survive each iteration
    resource='n_samples',  # For n_estimators, remove it from the grid and set max_resources
    max_resources='auto',
    random_state=42
)

halving_search.fit(X_train, y_train)
print(f"Best parameters: {halving_search.best_params_}")
Raw lines 224 to 252
253## Classification Metrics

Classification Metrics

Raw lines 253 to 254
255### Basic Metrics
257```python
258from sklearn.metrics import (
259 accuracy_score, precision_score, recall_score, f1_score,
260 balanced_accuracy_score, matthews_corrcoef
263y_pred = model.predict(X_test)
265accuracy = accuracy_score(y_test, y_pred)
266precision = precision_score(y_test, y_pred, average='weighted') # For multiclass
267recall = recall_score(y_test, y_pred, average='weighted')
268f1 = f1_score(y_test, y_pred, average='weighted')
269balanced_acc = balanced_accuracy_score(y_test, y_pred) # Good for imbalanced data
270mcc = matthews_corrcoef(y_test, y_pred) # Matthews correlation coefficient
272print(f"Accuracy: {accuracy:.3f}")
273print(f"Precision: {precision:.3f}")
274print(f"Recall: {recall:.3f}")
275print(f"F1-score: {f1:.3f}")
276print(f"Balanced Accuracy: {balanced_acc:.3f}")
277print(f"MCC: {mcc:.3f}")
278```

Basic Metrics

from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    balanced_accuracy_score, matthews_corrcoef
)

y_pred = model.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred, average='weighted')  # For multiclass
recall = recall_score(y_test, y_pred, average='weighted')
f1 = f1_score(y_test, y_pred, average='weighted')
balanced_acc = balanced_accuracy_score(y_test, y_pred)  # Good for imbalanced data
mcc = matthews_corrcoef(y_test, y_pred)  # Matthews correlation coefficient

print(f"Accuracy: {accuracy:.3f}")
print(f"Precision: {precision:.3f}")
print(f"Recall: {recall:.3f}")
print(f"F1-score: {f1:.3f}")
print(f"Balanced Accuracy: {balanced_acc:.3f}")
print(f"MCC: {mcc:.3f}")
Raw lines 255 to 279
280### Classification Report
282```python
283from sklearn.metrics import classification_report
285print(classification_report(y_test, y_pred, target_names=class_names))
286```

Classification Report

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred, target_names=class_names))
Raw lines 280 to 287
288### Confusion Matrix
290```python
291from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
292import matplotlib.pyplot as plt
294cm = confusion_matrix(y_test, y_pred)
295disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
296disp.plot(cmap='Blues')
297plt.show()
298```

Confusion Matrix

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
disp.plot(cmap='Blues')
plt.show()
Raw lines 288 to 299
300### ROC and AUC
302Binary `predict_proba[:, 1]` corresponds to `model.classes_[1]`; explicitly identify the scientific positive class. Multiclass probability columns must follow the class order. ROC AUC is threshold-free and does not establish calibrated probabilities or clinical utility.
304```python
305from sklearn.metrics import roc_auc_score, roc_curve, RocCurveDisplay
307# Binary classification
308y_proba = model.predict_proba(X_test)[:, 1]
309auc = roc_auc_score(y_test, y_proba)
310print(f"ROC AUC: {auc:.3f}")
312# Plot ROC curve
313fpr, tpr, thresholds = roc_curve(y_test, y_proba, pos_label=model.classes_[1])
314RocCurveDisplay(fpr=fpr, tpr=tpr, roc_auc=auc).plot()
316# Multiclass (one-vs-rest)
317y_proba_multi = model.predict_proba(X_test) # Use a multiclass fitted model here
318auc_ovr = roc_auc_score(y_test, y_proba_multi, labels=model.classes_, multi_class='ovr')
319```

ROC and AUC

Binary predict_proba[:, 1] corresponds to model.classes_[1]; explicitly identify the scientific positive class. Multiclass probability columns must follow the class order. ROC AUC is threshold-free and does not establish calibrated probabilities or clinical utility.

from sklearn.metrics import roc_auc_score, roc_curve, RocCurveDisplay

# Binary classification
y_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_proba)
print(f"ROC AUC: {auc:.3f}")

# Plot ROC curve
fpr, tpr, thresholds = roc_curve(y_test, y_proba, pos_label=model.classes_[1])
RocCurveDisplay(fpr=fpr, tpr=tpr, roc_auc=auc).plot()

# Multiclass (one-vs-rest)
y_proba_multi = model.predict_proba(X_test)  # Use a multiclass fitted model here
auc_ovr = roc_auc_score(y_test, y_proba_multi, labels=model.classes_, multi_class='ovr')
Raw lines 300 to 320
321### Precision-Recall Curve
323```python
324from sklearn.metrics import precision_recall_curve, PrecisionRecallDisplay
325from sklearn.metrics import average_precision_score
327precision, recall, thresholds = precision_recall_curve(y_test, y_proba, pos_label=model.classes_[1])
328ap = average_precision_score(y_test, y_proba, pos_label=model.classes_[1])
330disp = PrecisionRecallDisplay(precision=precision, recall=recall, average_precision=ap)
331disp.plot()
332```

Precision-Recall Curve

from sklearn.metrics import precision_recall_curve, PrecisionRecallDisplay
from sklearn.metrics import average_precision_score

precision, recall, thresholds = precision_recall_curve(y_test, y_proba, pos_label=model.classes_[1])
ap = average_precision_score(y_test, y_proba, pos_label=model.classes_[1])

disp = PrecisionRecallDisplay(precision=precision, recall=recall, average_precision=ap)
disp.plot()
Raw lines 321 to 333
334### Log Loss
336```python
337from sklearn.metrics import log_loss
339y_proba = model.predict_proba(X_test)
340logloss = log_loss(y_test, y_proba)
341print(f"Log Loss: {logloss:.3f}")
342```

Log Loss

from sklearn.metrics import log_loss

y_proba = model.predict_proba(X_test)
logloss = log_loss(y_test, y_proba)
print(f"Log Loss: {logloss:.3f}")
Raw lines 334 to 343
344## Regression Metrics
346MAPE is a relative value (0.1 means 10%) and becomes unstable near zero targets. R² may be negative on test data; constant targets require care with its default `force_finite=True` behavior.
348```python
349from sklearn.metrics import (
350 mean_squared_error, root_mean_squared_error, mean_absolute_error, r2_score,
351 mean_absolute_percentage_error, median_absolute_error
354y_pred = model.predict(X_test)
356mse = mean_squared_error(y_test, y_pred)
357rmse = root_mean_squared_error(y_test, y_pred)
358mae = mean_absolute_error(y_test, y_pred)
359r2 = r2_score(y_test, y_pred)
360mape = mean_absolute_percentage_error(y_test, y_pred)
361median_ae = median_absolute_error(y_test, y_pred)
363print(f"MSE: {mse:.3f}")
364print(f"RMSE: {rmse:.3f}")
365print(f"MAE: {mae:.3f}")
366print(f"R² Score: {r2:.3f}")
367print(f"MAPE: {mape:.3f}")
368print(f"Median AE: {median_ae:.3f}")
369```

Regression Metrics

MAPE is a relative value (0.1 means 10%) and becomes unstable near zero targets. R² may be negative on test data; constant targets require care with its default force_finite=True behavior.

from sklearn.metrics import (
    mean_squared_error, root_mean_squared_error, mean_absolute_error, r2_score,
    mean_absolute_percentage_error, median_absolute_error
)

y_pred = model.predict(X_test)

mse = mean_squared_error(y_test, y_pred)
rmse = root_mean_squared_error(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
mape = mean_absolute_percentage_error(y_test, y_pred)
median_ae = median_absolute_error(y_test, y_pred)

print(f"MSE: {mse:.3f}")
print(f"RMSE: {rmse:.3f}")
print(f"MAE: {mae:.3f}")
print(f"R² Score: {r2:.3f}")
print(f"MAPE: {mape:.3f}")
print(f"Median AE: {median_ae:.3f}")
Raw lines 344 to 370
371## Clustering Metrics

Clustering Metrics

Raw lines 371 to 372
373### With Ground Truth Labels
375```python
376from sklearn.metrics import (
377 adjusted_rand_score, normalized_mutual_info_score,
378 adjusted_mutual_info_score, fowlkes_mallows_score,
379 homogeneity_score, completeness_score, v_measure_score
382ari = adjusted_rand_score(y_true, y_pred)
383nmi = normalized_mutual_info_score(y_true, y_pred)
384ami = adjusted_mutual_info_score(y_true, y_pred)
385fmi = fowlkes_mallows_score(y_true, y_pred)
386homogeneity = homogeneity_score(y_true, y_pred)
387completeness = completeness_score(y_true, y_pred)
388v_measure = v_measure_score(y_true, y_pred)
389```

With Ground Truth Labels

from sklearn.metrics import (
    adjusted_rand_score, normalized_mutual_info_score,
    adjusted_mutual_info_score, fowlkes_mallows_score,
    homogeneity_score, completeness_score, v_measure_score
)

ari = adjusted_rand_score(y_true, y_pred)
nmi = normalized_mutual_info_score(y_true, y_pred)
ami = adjusted_mutual_info_score(y_true, y_pred)
fmi = fowlkes_mallows_score(y_true, y_pred)
homogeneity = homogeneity_score(y_true, y_pred)
completeness = completeness_score(y_true, y_pred)
v_measure = v_measure_score(y_true, y_pred)
Raw lines 373 to 390
391### Without Ground Truth
393Require `2 <= n_labels < n_samples`; do not let DBSCAN noise label -1 silently become a cluster. If excluding noise, report coverage and avoid comparing the resulting score directly with all-row scores. These internal indices favor compact/convex geometry and cannot confirm scientific validity.
395```python
396from sklearn.metrics import (
397 silhouette_score, calinski_harabasz_score, davies_bouldin_score
400silhouette = silhouette_score(X, labels) # [-1, 1], higher better
401ch_score = calinski_harabasz_score(X, labels) # Higher better
402db_score = davies_bouldin_score(X, labels) # Lower better
403```

Without Ground Truth

Require 2 <= n_labels < n_samples; do not let DBSCAN noise label -1 silently become a cluster. If excluding noise, report coverage and avoid comparing the resulting score directly with all-row scores. These internal indices favor compact/convex geometry and cannot confirm scientific validity.

from sklearn.metrics import (
    silhouette_score, calinski_harabasz_score, davies_bouldin_score
)

silhouette = silhouette_score(X, labels)  # [-1, 1], higher better
ch_score = calinski_harabasz_score(X, labels)  # Higher better
db_score = davies_bouldin_score(X, labels)  # Lower better
Raw lines 391 to 404
405## Custom Scoring

Custom Scoring

Raw lines 405 to 406
407### Using make_scorer
409```python
410from sklearn.metrics import make_scorer
412def custom_metric(y_true, y_pred):
413 # Illustrative binary cost: a missed positive costs five false positives.
414 import numpy as np
415 y_true, y_pred = np.asarray(y_true), np.asarray(y_pred)
416 return np.mean(5 * ((y_true == 1) & (y_pred == 0)) +
417 ((y_true == 0) & (y_pred == 1)))
419custom_scorer = make_scorer(custom_metric, greater_is_better=False)
421# Use in cross-validation or grid search
422scores = cross_val_score(model, X, y, cv=5, scoring=custom_scorer)
423```

Using make_scorer

from sklearn.metrics import make_scorer

def custom_metric(y_true, y_pred):
    # Illustrative binary cost: a missed positive costs five false positives.
    import numpy as np
    y_true, y_pred = np.asarray(y_true), np.asarray(y_pred)
    return np.mean(5 * ((y_true == 1) & (y_pred == 0)) +
                   ((y_true == 0) & (y_pred == 1)))

custom_scorer = make_scorer(custom_metric, greater_is_better=False)

# Use in cross-validation or grid search
scores = cross_val_score(model, X, y, cv=5, scoring=custom_scorer)
Raw lines 407 to 424
425### Multiple Metrics in Grid Search
427```python
428from sklearn.model_selection import GridSearchCV
430scoring = {
431 'accuracy': 'accuracy',
432 'precision': 'precision_weighted',
433 'recall': 'recall_weighted',
434 'f1': 'f1_weighted'
437grid_search = GridSearchCV(
438 model, param_grid,
439 cv=5,
440 scoring=scoring,
441 refit='f1', # Refit on best f1 score
442 return_train_score=True
445grid_search.fit(X_train, y_train)
446```

Multiple Metrics in Grid Search

from sklearn.model_selection import GridSearchCV

scoring = {
    'accuracy': 'accuracy',
    'precision': 'precision_weighted',
    'recall': 'recall_weighted',
    'f1': 'f1_weighted'
}

grid_search = GridSearchCV(
    model, param_grid,
    cv=5,
    scoring=scoring,
    refit='f1',  # Refit on best f1 score
    return_train_score=True
)

grid_search.fit(X_train, y_train)
Raw lines 425 to 447
448## Validation Curves

Validation Curves

Raw lines 448 to 449
450### Learning Curve
452```python
453from sklearn.model_selection import learning_curve
454import matplotlib.pyplot as plt
455import numpy as np
457train_sizes, train_scores, val_scores = learning_curve(
458 model, X, y,
459 cv=5,
460 train_sizes=np.linspace(0.1, 1.0, 10),
461 scoring='accuracy',
462 n_jobs=-1
465train_mean = train_scores.mean(axis=1)
466train_std = train_scores.std(axis=1)
467val_mean = val_scores.mean(axis=1)
468val_std = val_scores.std(axis=1)
470plt.figure(figsize=(10, 6))
471plt.plot(train_sizes, train_mean, label='Training score')
472plt.plot(train_sizes, val_mean, label='Validation score')
473plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1)
474plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.1)
475plt.xlabel('Training Set Size')
476plt.ylabel('Score')
477plt.title('Learning Curve')
478plt.legend()
479plt.grid(True)
480```

Learning Curve

from sklearn.model_selection import learning_curve
import matplotlib.pyplot as plt
import numpy as np

train_sizes, train_scores, val_scores = learning_curve(
    model, X, y,
    cv=5,
    train_sizes=np.linspace(0.1, 1.0, 10),
    scoring='accuracy',
    n_jobs=-1
)

train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)

plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, label='Training score')
plt.plot(train_sizes, val_mean, label='Validation score')
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1)
plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.1)
plt.xlabel('Training Set Size')
plt.ylabel('Score')
plt.title('Learning Curve')
plt.legend()
plt.grid(True)
Raw lines 450 to 481
482### Validation Curve
484```python
485from sklearn.model_selection import validation_curve
487param_range = [1, 10, 50, 100, 200, 500]
488train_scores, val_scores = validation_curve(
489 model, X, y,
490 param_name='n_estimators',
491 param_range=param_range,
492 cv=5,
493 scoring='accuracy',
494 n_jobs=-1
497train_mean = train_scores.mean(axis=1)
498val_mean = val_scores.mean(axis=1)
500plt.figure(figsize=(10, 6))
501plt.plot(param_range, train_mean, label='Training score')
502plt.plot(param_range, val_mean, label='Validation score')
503plt.xlabel('n_estimators')
504plt.ylabel('Score')
505plt.title('Validation Curve')
506plt.legend()
507plt.grid(True)
508```

Validation Curve

from sklearn.model_selection import validation_curve

param_range = [1, 10, 50, 100, 200, 500]
train_scores, val_scores = validation_curve(
    model, X, y,
    param_name='n_estimators',
    param_range=param_range,
    cv=5,
    scoring='accuracy',
    n_jobs=-1
)

train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)

plt.figure(figsize=(10, 6))
plt.plot(param_range, train_mean, label='Training score')
plt.plot(param_range, val_mean, label='Validation score')
plt.xlabel('n_estimators')
plt.ylabel('Score')
plt.title('Validation Curve')
plt.legend()
plt.grid(True)
Raw lines 482 to 509
510## Model Persistence
512Pickle/joblib loading can execute arbitrary code: load trusted artifacts only. Cross-version scikit-learn loading is unsupported. Save the fitted pipeline, exact package versions, feature schema, and validation provenance together.

Model Persistence

Pickle/joblib loading can execute arbitrary code: load trusted artifacts only. Cross-version scikit-learn loading is unsupported. Save the fitted pipeline, exact package versions, feature schema, and validation provenance together.

Raw lines 510 to 513
514### Save and Load Models
516```python
517import joblib
519# Save model
520joblib.dump(model, 'model.pkl')
522# Load model
523loaded_model = joblib.load('model.pkl')
525# Also works with pipelines
526joblib.dump(pipeline, 'pipeline.pkl')
527```

Save and Load Models

import joblib

# Save model
joblib.dump(model, 'model.pkl')

# Load model
loaded_model = joblib.load('model.pkl')

# Also works with pipelines
joblib.dump(pipeline, 'pipeline.pkl')
Raw lines 514 to 528
529### Using pickle
531```python
532import pickle
534# Save
535with open('model.pkl', 'wb') as f:
536 pickle.dump(model, f)
538# Load
539with open('model.pkl', 'rb') as f:
540 loaded_model = pickle.load(f)
541```

Using pickle

import pickle

# Save
with open('model.pkl', 'wb') as f:
    pickle.dump(model, f)

# Load
with open('model.pkl', 'rb') as f:
    loaded_model = pickle.load(f)
Raw lines 529 to 542
543## Imbalanced Data Strategies

Imbalanced Data Strategies

Raw lines 543 to 544
545### Class Weighting
547```python
548from sklearn.ensemble import RandomForestClassifier
550# Automatically balance classes
551model = RandomForestClassifier(class_weight='balanced', random_state=42)
552model.fit(X_train, y_train)
554# Custom weights
555class_weights = {0: 1, 1: 10} # Give class 1 more weight
556model = RandomForestClassifier(class_weight=class_weights, random_state=42)
557```

Class Weighting

from sklearn.ensemble import RandomForestClassifier

# Automatically balance classes
model = RandomForestClassifier(class_weight='balanced', random_state=42)
model.fit(X_train, y_train)

# Custom weights
class_weights = {0: 1, 1: 10}  # Give class 1 more weight
model = RandomForestClassifier(class_weight=class_weights, random_state=42)
Raw lines 545 to 558
559### Resampling (using imbalanced-learn)
561```python
562# Install: uv pip install imbalanced-learn
563from imblearn.over_sampling import SMOTE
564from imblearn.under_sampling import RandomUnderSampler
565from imblearn.pipeline import Pipeline as ImbPipeline
567# Resampling belongs inside each training fold; pass this pipeline to CV/search.
568# Binary numeric features only here: float ratios are not multiclass strategies.
569# For nominal columns consider SMOTENC; never interpolate arbitrary category codes.
570# Combined approach
571pipeline = ImbPipeline([
572 ('over', SMOTE(sampling_strategy=0.5, random_state=42)),
573 ('under', RandomUnderSampler(sampling_strategy=0.8, random_state=42)),
574 ('model', RandomForestClassifier(random_state=42))
575])
576```

Resampling (using imbalanced-learn)

# Install: uv pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline as ImbPipeline

# Resampling belongs inside each training fold; pass this pipeline to CV/search.
# Binary numeric features only here: float ratios are not multiclass strategies.
# For nominal columns consider SMOTENC; never interpolate arbitrary category codes.
# Combined approach
pipeline = ImbPipeline([
    ('over', SMOTE(sampling_strategy=0.5, random_state=42)),
    ('under', RandomUnderSampler(sampling_strategy=0.8, random_state=42)),
    ('model', RandomForestClassifier(random_state=42))
])
Raw lines 559 to 577
578## Best Practices

Best Practices

Raw lines 578 to 579
580### Stratified Splitting
581Use stratified splitting for classification only when rows are independent and the deployment task permits random splitting; retain group or temporal boundaries when those apply:
582```python
583X_train, X_test, y_train, y_test = train_test_split(
584 X, y, test_size=0.2, stratify=y, random_state=42
586```

Stratified Splitting

Use stratified splitting for classification only when rows are independent and the deployment task permits random splitting; retain group or temporal boundaries when those apply:

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)
Raw lines 580 to 587
588### Appropriate Metrics
589- **Balanced data**: Accuracy, F1-score
590- **Imbalanced data**: Per-class Precision/Recall, Average Precision, macro F1, Balanced Accuracy; weighted averages can hide minority failures
591- **Cost-sensitive**: Define custom scorer with costs
592- **Ranking**: ROC AUC, Average Precision
594#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use `.iloc[index]` for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Appropriate Metrics

  • Balanced data: Accuracy, F1-score
  • Imbalanced data: Per-class Precision/Recall, Average Precision, macro F1, Balanced Accuracy; weighted averages can hide minority failures
  • Cost-sensitive: Define custom scorer with costs
  • Ranking: ROC AUC, Average Precision

#Split by the independent unit: patients/specimens/sites and related molecules must not cross partitions. Future prediction requires temporal boundaries and available-at-prediction features. Integer index examples below assume NumPy arrays; use .iloc[index] for pandas. CV fold SD is descriptive, not a confidence interval, because folds overlap in training data.

Raw lines 588 to 595
596## Cross-Validation
597- Use 5 or 10-fold CV for most cases
598- Use StratifiedKFold for classification
599- Use TimeSeriesSplit for time series
600- Use GroupKFold when samples are grouped

Cross-Validation

  • Use 5 or 10-fold CV for most cases
  • Use StratifiedKFold for classification
  • Use TimeSeriesSplit for time series
  • Use GroupKFold when samples are grouped
Raw lines 596 to 601
602### Nested Cross-Validation
603Estimate the complete selection procedure on untouched outer folds; do not choose algorithms or preprocessing using the outer scores. Both levels must respect groups/time where applicable:
604```python
605from sklearn.model_selection import cross_val_score, GridSearchCV
607# Inner loop: hyperparameter tuning
608grid_search = GridSearchCV(model, param_grid, cv=5)
610# Outer loop: performance estimation
611scores = cross_val_score(grid_search, X, y, cv=5)
612print(f"Nested CV score: {scores.mean():.3f} (fold SD {scores.std():.3f})")
613```

Nested Cross-Validation

Estimate the complete selection procedure on untouched outer folds; do not choose algorithms or preprocessing using the outer scores. Both levels must respect groups/time where applicable:

from sklearn.model_selection import cross_val_score, GridSearchCV

# Inner loop: hyperparameter tuning
grid_search = GridSearchCV(model, param_grid, cv=5)

# Outer loop: performance estimation
scores = cross_val_score(grid_search, X, y, cv=5)
print(f"Nested CV score: {scores.mean():.3f} (fold SD {scores.std():.3f})")
Raw lines 602 to 614
615## Upstream references
617- https://scikit-learn.org/stable/modules/cross_validation.html
618- https://scikit-learn.org/stable/modules/model_evaluation.html
619- https://scikit-learn.org/stable/model_persistence.html
620- https://imbalanced-learn.org/stable/common_pitfalls.html

Upstream references

  • https://scikit-learn.org/stable/modules/cross_validation.html
  • https://scikit-learn.org/stable/modules/model_evaluation.html
  • https://scikit-learn.org/stable/model_persistence.html
  • https://imbalanced-learn.org/stable/common_pitfalls.html
Raw lines 615 to 620