Forecast bias¶
Do we systematically forecast too much or too little?
Current value (inventory case data): -2.1% - ✓ On target. Open the case.
| Field | Value |
|---|---|
| Definition | (Sum of forecast - sum of actuals) / sum of actuals. Positive = over-forecast. |
| Formula | (sum(F) - sum(A)) / sum(A) |
| Unit | % |
| Granularity | SKU x week; rolled up by SKU class and month. |
| Direction | Inside a band ↔ (too high and too low are both bad) |
| Target and thresholds | -5.0% to 5.0% (amber -10.0% to 10.0%) |
| Parameters | – |
| Owner | Demand Planner |
| Source | Demand (UCI Online Retail II) + baseline forecast computed in this repo |
| Input | table forecast, columns actual_units, forecast_units |
| Recommended chart | bar - Diverging bars by month around zero with the green band shaded. |
| Related KPIs | Forecast error (WAPE), Obsolete stock share, Stockout rate (SKU-days) |
How to read it¶
Error you can fix. Accuracy (WAPE) says how big the miss is; bias says in which direction. Persistent positive bias builds excess and obsolete stock; negative bias builds stockouts.
Common misreadings¶
- A bias near zero can hide large errors that cancel out; never read it without WAPE.
Worked example (from the test suite)¶
| actual_units | forecast_units |
|---|---|
| 100 | 90 |
| 10 | 20 |
| 0 | 5 |
Expected result: 4.55%. (115 - 110) / 110 = +0.04545 -> 4.5% over-forecast, inside the +/-5% band.
This example is asserted in tests/test_formulas.py against the Python function and in tests/test_sql.py against the SQL below.
Implementations¶
Python function (the single implementation used by the site):
@kpi("forecast_bias")
def forecast_bias(df: pd.DataFrame) -> float:
actual = df["actual_units"].sum()
return safe_ratio(df["forecast_units"].sum() - actual, actual)
Reference SQL (tested on SQLite):
SELECT (SUM(forecast_units) - SUM(actual_units)) * 1.0 / SUM(actual_units) FROM forecast