Skip to main content

benchgap

Measure how much of a machine-learning benchmark score survives deployment.

A model scores 0.99 on a held-out test set. What does it score on real data six months later? The gap between those two numbers is usually reported — if at all — as a single figure, which names an effect without explaining it. benchgap helps you break it into pieces you can act on, with confidence intervals that aren't quietly wrong.

pip install benchgap

Depends on numpy and nothing else.


The part most people get wrong

Confidence intervals on model evaluations are routinely several times too narrow, because the rows being resampled aren't independent:

Domain The trap Group by
Phishing detection 200 URLs on one hacked website look like 200 observations; they're closer to one website
Air-quality forecasting 17 stations recording the same smog event look like 17; they're one weather event day
Solar flare forecasting 300 snapshots of one sunspot group look like 300; the group is one thing region

Resample rows independently in any of those and you'll report an interval that looks convincing and isn't. benchgap takes a groups argument everywhere it computes uncertainty, and resamples whole groups.

import benchgap as bg

lo, hi = bg.cluster_bootstrap_ci(y, prob, threshold, groups=website, n_resamples=1000)

This isn't optional politeness. It's the difference between an interval that means something and one that doesn't.


Quick start

Score a model honestly

import benchgap as bg

# Pick the decision threshold on VALIDATION data, then freeze it.
# Choosing it on the data you're about to report is the most common way an
# evaluation quietly becomes optimistic.
threshold = bg.select_threshold(y_val, prob_val, objective="tss")

scores = bg.score_at(y_test, prob_test, threshold)
print(scores.tss, scores.recall, scores.false_alarm_rate)

TSS (True Skill Statistic) = recall − false-alarm rate. It is 0 for any constant forecast, which makes the zero line a real no-skill baseline. Accuracy can't do that job: when one outcome is rare, a model that always predicts the common case scores 98% and has no skill at all.

Build a chain of comparisons

A "cell" is one model scored on one dataset. Line several up so each step changes exactly one thing, and the total drop becomes attributable instead of merely observed.

cells = {
    "shuffled":    bg.evaluate_cell("shuffled", "random split — the common protocol",
                                    y1, p1, thresholds, groups=g1),
    "honest":      bg.evaluate_cell("honest", "grouped split — no leakage",
                                    y2, p2, thresholds, groups=g2),
    "operational": bg.evaluate_cell("operational", "live data, model frozen",
                                    y3, p3, thresholds, groups=g3),
}

steps = [
    ("leakage", "shuffled", "honest", "Same data; only the split changes."),
    ("drift",   "honest",   "operational", "Same protocol; later data."),
]

print(bg.attribute(cells, steps)["tss"])
# {'leakage': 0.215, 'drift': -0.077, 'total': 0.138}

A step whose endpoints are missing is skipped rather than guessed at, and total is computed from the first and last cell directly — so it stays correct even when an intermediate cell doesn't exist.

Ask whether retraining helps

result = bg.recovery(gap_tss, retrained_tss, ci)
# {'delta': -0.136, 'ci95': [-0.255, -0.009],
#  'direction': 'harms', 'significant': True}

The interval can exclude zero in either direction. A retrained model that's measurably worse is a finding, not a null — testing only for improvement would report it as "not significant" and throw it away.


What's in it

score_at, tss_from_confusion, brier, brier_skill, confusion metrics, with TSS as the headline
select_threshold, peak_tss pick an operating point on validation; peak_tss is a hindsight upper bound, never deployable
cluster_bootstrap_ci, paired_difference_ci uncertainty that respects grouping
Cell, evaluate_cell, attribute, step_descriptions chains of comparisons and their decomposition
recovery interpret a retraining comparison in either direction

The interface is arrays in, dicts out. It never sees a DataFrame, a model object, or a file path — the three studies that drove its design disagree about all of those, and anything wider would have forced a data model onto domains that don't want one.

Where it came from

Extracted unchanged from three independent studies that had each arrived at the same code:

Study What it measures
phish-drift phishing-URL datasets, two of which a regex solves better than published models
ozone-drift Houston smog forecasting — the control case, where honest testing holds up
SolarFlarePredictor solar flare forecasting from satellite magnetic-field data

Those studies make a claim that only works if every number is comparable: how far a benchmark score falls in deployment depends on what kind of change the model faces. Three copies of the scoring code kept in sync by a hash check would drift eventually — and when they did, the comparison would silently stop being a comparison. Hence one package.

License

MIT.

Metadata

Release files for benchgap 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for benchgap 0.1.0
File Size Uploaded
benchgap-0.1.0.tar.gz 11.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for benchgap 0.1.0
File Interpreter ABI Platform
benchgap-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 22.4 kB

Release files / benchgap-0.1.0.tar.gz

Download URL benchgap-0.1.0.tar.gz
Size 11.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a4b989fdd27cfb42d85ef6989a9a374d28196f1cb49ba5766440da4ece80eb08
BLAKE2b-256 checksum
How to use checksums
b3e763a209ea2d3469fa969ae3eebd8be05befa414d28e7d931c4c87893248c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / benchgap-0.1.0-py3-none-any.whl

Download URL benchgap-0.1.0-py3-none-any.whl
Size 10.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2e78f83bf3c3c513da55fc7448fd41cf7f9627af3af6de6d8ca795933a03fad0
BLAKE2b-256 checksum
How to use checksums
52b59d5f03027b3c7ddba3c15541d169654ee27ad45b4853f70f386f9c3f9ea0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page