| """Recompute YellowCab's public held-out metrics from sanitized predictions.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| import numpy as np |
| import pandas as pd |
| from sklearn.metrics import ( |
| accuracy_score, |
| balanced_accuracy_score, |
| brier_score_loss, |
| f1_score, |
| log_loss, |
| ) |
|
|
|
|
| CLASSES = ("continue", "slow", "stop", "turn_left", "turn_right") |
| SEED = 20260726 |
| BOOTSTRAP_ITERATIONS = 500 |
|
|
|
|
| def expected_calibration_error( |
| probabilities: np.ndarray, |
| targets: np.ndarray, |
| *, |
| bins: int = 15, |
| ) -> float: |
| confidence = probabilities.max(axis=1) |
| predictions = probabilities.argmax(axis=1) |
| correct = predictions == targets |
| edges = np.linspace(0.0, 1.0, bins + 1) |
| value = 0.0 |
| for index in range(bins): |
| if index == bins - 1: |
| mask = (confidence >= edges[index]) & (confidence <= edges[index + 1]) |
| else: |
| mask = (confidence >= edges[index]) & (confidence < edges[index + 1]) |
| if mask.any(): |
| value += float(mask.mean()) * abs( |
| float(correct[mask].mean()) - float(confidence[mask].mean()) |
| ) |
| return value |
|
|
|
|
| def route_bootstrap_macro_f1( |
| frame: pd.DataFrame, |
| targets: np.ndarray, |
| predictions: np.ndarray, |
| ) -> tuple[float, float]: |
| route_values = frame["route_group"].astype(str).to_numpy() |
| routes = np.unique(route_values) |
| rng = np.random.default_rng(SEED) |
| values: list[float] = [] |
| for _ in range(BOOTSTRAP_ITERATIONS): |
| sampled = rng.choice(routes, size=len(routes), replace=True) |
| indices = np.concatenate( |
| [np.flatnonzero(route_values == route) for route in sampled] |
| ) |
| values.append( |
| float( |
| f1_score( |
| targets[indices], |
| predictions[indices], |
| labels=np.arange(len(CLASSES)), |
| average="macro", |
| zero_division=0, |
| ) |
| ) |
| ) |
| return tuple(float(value) for value in np.quantile(values, [0.025, 0.975])) |
|
|
|
|
| def main() -> None: |
| root = Path(__file__).resolve().parent |
| frame = pd.read_csv(root / "eval_predictions.csv") |
| class_to_index = {name: index for index, name in enumerate(CLASSES)} |
| targets = frame["label"].map(class_to_index).to_numpy(dtype=np.int64) |
| predictions = frame["prediction"].map(class_to_index).to_numpy(dtype=np.int64) |
| probabilities = frame[ |
| [f"prob_{class_name}" for class_name in CLASSES] |
| ].to_numpy(dtype=np.float64) |
| if not np.allclose(probabilities.sum(axis=1), 1.0, atol=1e-6): |
| raise ValueError("Probability rows do not sum to one") |
| one_hot = np.eye(len(CLASSES), dtype=np.float64)[targets] |
| ci_low, ci_high = route_bootstrap_macro_f1(frame, targets, predictions) |
| result = { |
| "samples": int(len(frame)), |
| "routes": int(frame["route_group"].nunique()), |
| "accuracy": float(accuracy_score(targets, predictions)), |
| "balanced_accuracy": float(balanced_accuracy_score(targets, predictions)), |
| "macro_f1": float( |
| f1_score( |
| targets, |
| predictions, |
| labels=np.arange(len(CLASSES)), |
| average="macro", |
| zero_division=0, |
| ) |
| ), |
| "weighted_f1": float( |
| f1_score( |
| targets, |
| predictions, |
| labels=np.arange(len(CLASSES)), |
| average="weighted", |
| zero_division=0, |
| ) |
| ), |
| "log_loss": float(log_loss(targets, probabilities, labels=range(len(CLASSES)))), |
| "brier_score": float(np.mean(np.sum((probabilities - one_hot) ** 2, axis=1))), |
| "ece_15_bin": expected_calibration_error(probabilities, targets), |
| "macro_f1_route_ci_95": [ci_low, ci_high], |
| } |
| print(json.dumps(result, indent=2)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|