Skip to content

Stockout rate (SKU-days)

How often is an item simply not available?

Current value (inventory case data): 5.9% - ! Watch. Open the case.

Field Value
Definition SKU-days of the active assortment with zero on-hand units at end of day, divided by all active SKU-days. A SKU is active between its first and last demand.
Formula count(active AND on_hand_units <= 0) / count(active sku_days)
Unit %
Granularity SKU x day; reported weekly by SKU class.
Direction Lower is better ↓
Target and thresholds ≤ 3.0% (amber ≤ 6.0%)
Parameters –
Owner Supply Planner
Source Synthetic stock simulation on public demand
Input table inventory_daily, columns on_hand_units, active
Recommended chart heatmap - SKU x week grid reveals clusters of stockouts in time.
Related KPIs Fill rate (units), Days of cover

How to read it

Unweighted by volume, so it shows breadth: many small SKUs out of stock raise it even when fill rate stays high.

Common misreadings

  • End-of-day stock misses intraday stockouts; for fast movers it understates the problem.
  • Including discontinued or not-yet-launched SKUs inflates it - this definition counts the active assortment only.

Worked example (from the test suite)

on_hand_units active
0 True
5 True
3 True
0 True
10 True
2 True
0 True
1 True
0 False
0 False

Expected result: 37.50%. Active SKU-days: 8 (the last two rows are a discontinued SKU and are excluded). At zero: 3 of 8 = 0.375. Counting the inactive rows would give 5 / 10 = 0.5.

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("stockout_rate")
def stockout_rate(df: pd.DataFrame) -> float:
    active = df[df["active"].astype(bool)]
    return safe_ratio((active["on_hand_units"] <= 0).sum(), len(active))

Reference SQL (tested on SQLite):

SELECT CAST(SUM(CASE WHEN on_hand_units <= 0 THEN 1 ELSE 0 END) AS REAL) / COUNT(*)
FROM inventory_daily WHERE active = 1