Eval Metrics — Python source
The standard LLM-eval numbers with exact math — unbiased pass@k, precision/recall/F1 from confusion counts, exact-match and label-set micro-F1. 100% client-side.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""Eval Metrics — the standard LLM-eval metrics with exact, testable formulas.
Language: Python (3.10+, standard library only)
Port of src/lib/evalMetrics.ts (the canonical TypeScript implementation).
Tool page: https://dev.cosmolabs.org/tools/eval-metrics
pass@k is the unbiased estimator from the Codex paper (Chen et al. 2021),
identical to the HumanEval implementation's combinatorial form. Precision,
recall, and F1 run over confusion counts and over label sets; exact-match
rate runs over paired strings (case-sensitive).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, Sequence
@dataclass(frozen=True)
class PRF1:
"""Precision / recall / F1 triple."""
precision: float
recall: float
f1: float
@dataclass(frozen=True)
class ConfusionCounts:
"""Confusion counts; tn is accepted but unused by P/R/F1."""
tp: int
fp: int
fn: int
tn: int | None = None
def pass_at_k(n: int, c: int, k: int) -> float:
"""Unbiased pass@k: probability that at least one of k samples drawn
without replacement from n (of which c are correct) passes.
1 when n - c < k (a wrong draw is impossible)
1 - Π_{i=0..k-1} (n - c - i) / (n - i) otherwise
Raises ValueError on impossible inputs (the TS RangeError contract).
"""
if n <= 0:
raise ValueError("n must be > 0")
if c < 0 or c > n:
raise ValueError("c must be in [0, n]")
if k <= 0 or k > n:
raise ValueError("k must be in [1, n]")
if n - c < k:
return 1.0
product = 1.0
for i in range(k):
product *= (n - c - i) / (n - i)
return 1.0 - product
def precision_recall(counts: ConfusionCounts) -> PRF1:
"""Precision/recall/F1 over confusion counts. Zero denominators score 0.
Raises ValueError on negative counts.
"""
tp, fp, fn = counts.tp, counts.fp, counts.fn
if any(v < 0 for v in (tp, fp, fn)):
raise ValueError("counts must be >= 0")
precision = tp / (tp + fp) if tp + fp > 0 else 0.0
recall = tp / (tp + fn) if tp + fn > 0 else 0.0
f1 = (2 * precision * recall) / (precision + recall) if precision + recall > 0 else 0.0
return PRF1(precision, recall, f1)
def micro_average(per_class: Sequence[ConfusionCounts]) -> PRF1:
"""Micro-averaged P/R/F1 across per-class confusion counts."""
sums = ConfusionCounts(
tp=sum(c.tp for c in per_class),
fp=sum(c.fp for c in per_class),
fn=sum(c.fn for c in per_class),
)
return precision_recall(sums)
def exact_match_rate(predictions: Sequence[str], references: Sequence[str]) -> float:
"""Exact-match rate over paired predictions/references (case-sensitive).
Empty input scores 0; length mismatch raises ValueError.
"""
if len(predictions) != len(references):
raise ValueError("predictions and references must have the same length")
if not predictions:
return 0.0
hits = sum(1 for p, r in zip(predictions, references) if p == r)
return hits / len(predictions)
def set_match(prediction: Iterable[str], reference: Iterable[str]) -> PRF1:
"""P/R/F1 over label SETS — the standard multi-label / extraction metric."""
p = set(prediction)
r = set(reference)
tp = sum(1 for label in r if label in p)
fp = len(p - r)
fn = len(r - p)
return precision_recall(ConfusionCounts(tp=tp, fp=fp, fn=fn))
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →