Skip to content

Inventory turnover

How many times a year does the stock we hold get sold?

Current value (inventory case data): 5.6x / year - ! Watch. Open the case.

Field Value
Definition Annualised cost of goods sold divided by average inventory value.
Formula (sum(cogs_value) * 365 / days) / mean_daily(sum(on_hand_value))
Unit times per year
Granularity SKU x day; reported monthly by SKU class.
Direction Higher is better ↑
Target and thresholds ≥ 8.0x / year (amber ≥ 5.0x / year)
Parameters –
Owner Inventory Controller
Source Synthetic stock simulation on public demand
Input table inventory_daily, columns date, on_hand_value, cogs_value
Recommended chart bar - By SKU class (A/B/C) - the comparison that drives policy.
Related KPIs Days of cover, Obsolete stock share, Fill rate (units)

How to read it

Capital efficiency. Higher is better only up to the point where stockouts start: read it together with fill rate.

Common misreadings

  • Using sales at price instead of cost in the numerator inflates turnover by the margin.
  • Using year-end inventory instead of the average flatters companies that run down stock before closing.

Worked example (from the test suite)

date sku on_hand_value cogs_value
2024-01-01 a 100.0 5.0
2024-01-01 b 100.0 5.0
2024-01-02 a 50.0 5.0
2024-01-02 b 150.0 5.0

Expected result: 18.25x / year. COGS 20 over 2 days, annualised: 20 * 365 / 2 = 3650. Inventory value per date: 100 + 100 = 200 and 50 + 150 = 200, average 200. Turnover = 3650 / 200 = 18.25x / year.

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("inventory_turnover")
def inventory_turnover(df: pd.DataFrame) -> float:
    days = df["date"].nunique()
    if not days:
        return float("nan")
    avg_inventory = df.groupby("date")["on_hand_value"].sum().mean()
    return safe_ratio(df["cogs_value"].sum() * 365 / days, avg_inventory)

Reference SQL (tested on SQLite):

SELECT (SUM(cogs_value) * 365.0 / COUNT(DISTINCT date))
       / (SELECT AVG(v) FROM (SELECT SUM(on_hand_value) AS v FROM inventory_daily GROUP BY date))
FROM inventory_daily