Skip to main content

forelint

PyPI DOI

Lint your forecast backtests before you trust a score.

forelint checks the things a backtest score hides: leakage between related series, the run-to-run noise floor, whether model A is really better than model B, and which items carry the bias and the error. It works on the long-format tables that statsforecast, mlforecast and most forecasting code already produce, and on any scikit-learn estimator.

pip install forelint

What it checks

Question Function
Does my validation leak through related stores, products or regions? split_optimism, GroupTimeSplit
Is this improvement bigger than seed-to-seed noise? noise_floor
Is model A really better than B across many series? dm_test, diebold_mariano, model_confidence_set
Which items are always over or under forecast? Does the model beat a naive forecast? Where does the error live? audit
Does the model win on steady items and lose on intermittent ones? classify_demand, audit(segments="demand_class")
What is the scaled error (MASE, RMSSE) per item? audit(train=history)
How do I score without counting stockout days as low demand? audit(exclude="stockout")

Quickstart

import forelint as fl

# df: one row per item, date and forecast origin ("cutoff"), the actual in "y",
# one column per model, e.g. the output of statsforecast's cross_validation()
report = fl.audit(df, models=["LightGBM", "ETS"], baseline="SeasonalNaive",
                  train=history,              # adds MASE and RMSSE
                  segments="demand_class",    # smooth / erratic / intermittent / lumpy
                  exclude="stockout")         # optional: skip stockout periods
print(report)

# Is LightGBM really better than the seasonal naive, or is it noise?
# (uses the cutoff column, one point per forecast origin)
print(fl.dm_test(df, "LightGBM", "SeasonalNaive"))
# How much does a leaky split flatter the model, against an honest split on
# the same test rows and training size?
fl.split_optimism(model, X, y, groups=store_ids, time=dates)

# What difference between two runs is just noise?
nf = fl.noise_floor(lambda seed: train_and_score(seed), seeds=range(10))
nf.is_meaningful(new_mae - old_mae)

With statsforecast

examples/statsforecast_cv.py runs StatsForecast.cross_validation with AutoETS, Croston, seasonal naive and naive on 60 daily series at 12 forecast origins whose windows do not overlap, then audits the output as it comes out, using the cutoff column so each origin counts once. AutoETS is the only model left in the confidence set, yet on the intermittent series it is still worse than the naive forecast on about half of them.

On real data: one Walmart store in M5

benchmarks/m5_benchmark.py runs every check on the public M5 data: store CA_1, department FOODS_3, 823 items, a direct 28-day-ahead gradient-boosting model from scikit-learn, backtested at 10 forecast origins 28 days apart (no overlapping windows) against a 28-day moving average. Measured, not assumed:

  • On absolute error the model is worse. Forecast value added is −3.9% on MAE, and the moving average is the better forecast for 58% of items.
  • The loss is all in the slow sellers. By demand class, the model adds +5.0% on the 152 smooth items and +1.4% on the 33 erratic ones, but loses −10.5% on the 463 intermittent items and −9.2% on the 171 lumpy ones, being worse on about two thirds of each.
  • On squared error the evidence is weak. The model's RMSE is lower, 3.44 against 3.51, but Diebold-Mariano across the 10 origins gives p = 0.20; the model confidence set at 10% keeps the model and drops the moving average only narrowly (p = 0.057).
  • Bias is item by item. Overall bias is small (−0.6% of sales), but the model's mean error is significantly non-zero on 24% of items, against under 1% for the moving average.
  • 10% of items hold 74% of the squared error.
  • The noise floor is small: over 10 seeds the MAE sd is 0.0027, so a difference under 0.0087 between two runs is noise.
  • Leakage is small for this data: plain K-fold is 3.3% optimistic against holding out the items (95% interval 0.8% to 5.7%); for random and group splits the interval includes zero. Leakage depends on the features; forelint measures it rather than assuming it.

The model is deliberately plain (six features, no tuning). The point is the audit, not the model.

Why

In an international machine-learning competition on well-geology prediction, validation ranked five builds against the hidden leaderboard with a correlation of −0.745: the build it liked most scored worst, and identical code scored 6.241, 6.259 and 6.368 on three runs. Both failures were measurable before the deadline with the right checks. forelint is those checks, packaged.

Background: My validation ranked five models backwards.

Conventions

  • Error is forecast − actual. Positive bias means over-forecasting.
  • With a cutoff column, dm_test and the audit's bias test use one value per forecast origin, because the steps of one forecast share an origin and are correlated. When windows overlap (origins closer together than the horizon, such as statsforecast's default step_size=1), each series keeps only windows that do not overlap, counted back from its latest, and windows are lined up across series by that position, so series that end on different dates still count fully. At least 3 windows are needed. Backtest with step_size at least the horizon and ten or more windows. Without a cutoff column, every time stamp is treated as a separate one-step forecast.
  • dm_test applies Diebold-Mariano with the Harvey-Leybourne-Newbold correction, falling back to Newey-West weights if the long-run variance comes out negative.
  • model_confidence_set follows Hansen, Lunde and Nason (2011) with the T_max statistic and a circular block bootstrap. Its p-values match arch.bootstrap.MCS when given the same bootstrap draws.
  • MASE and RMSSE are scaled by each item's in-sample naive error from its first positive value onward, as M5 does; RMSSE is the per-series part of M5's weighted WRMSSE. Gaps in the history are not counted as one-step changes.
  • Demand classes use ADI from each item's first demand and CV² of the positive demand sizes, with the Syntetos-Boylan cut-offs 1.32 and 0.49.
  • split_optimism compares each leaky split with its honest counterpart on the same test rows and the same number of training rows per group: random vs time (leak through time), group vs group_time (through other entities' future) and kfold vs group (through the entity itself, what plain K-fold does). Each optimism figure comes with a 95% interval over folds; a negative value means no detectable leak, not a pessimistic split.
  • noise_floor reports the critical difference, t · √2 · sd: the smallest difference between two single runs that is significant.

Cite

If forelint helps your work, please cite it:

Mashiah, D. (2026). forelint: lint your forecast backtests before you trust a score. Zenodo. https://doi.org/10.5281/zenodo.24044927

Status

Alpha. The API may change before 1.0. Issues and pull requests are welcome.

Author

David Mashiah. MIT licensed.

Metadata

Release files for forelint 0.2.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 forelint 0.2.0
File Size Uploaded Provenance Actions
forelint-0.2.0.tar.gz 29.5 kB Without provenance Details for forelint-0.2.0.tar.gz

Built distribution (wheel)

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

Total release size: 53.1 kB

Release files / forelint-0.2.0.tar.gz

Download URL forelint-0.2.0.tar.gz
Size 29.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6c6d534eba5e9c4ca148fd1c35f7f1df764edc488bdb8d2efec6a0c0f91893a7
BLAKE2b-256 checksum
How to use checksums
2e2939b8b27031a74ed2fd979d848c4ae1d27e5a68865f4c943c44d2454c4b02
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / forelint-0.2.0-py3-none-any.whl

Download URL forelint-0.2.0-py3-none-any.whl
Size 23.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c6aa9498a94173a70baa396f27ae0c04e4be42ab6ff002b2cd7da90ec35698cc
BLAKE2b-256 checksum
How to use checksums
0dd4128c5a0d4e16ca3321752d91007f22f42b27006c17e566471e37c22859ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Security for forelint 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

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