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