YellowCab / recompute_metrics.py
l0uai
Publish causal YellowCab benchmark and model card
52f55f8
Raw
History Blame Contribute Delete
3.96 kB
"""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()