Inventory, money lying idle
Inventory exists to meet demand without waiting for the supplier. But every item on a shelf or in a warehouse is tied-up money: it has been paid for, it takes up space, it can expire or become obsolete. Inventory management seeks a balance between two risks: stockout (losing sales) and overstock (needlessly tying up money).
Value, coverage and turnover
items["stock_value"] = items["stock_units"] * items["unit_cost"]
items["daily_usage"] = items["annual_usage"] / 365
items["coverage_days"] = items["stock_units"] / items["daily_usage"]
items["usage_value"] = items["annual_usage"] * items["unit_cost"]
items["rotation"] = items["usage_value"] / items["stock_value"]
- Coverage answers the question: “if nothing more arrives, how many days can I hold out?”. P1: 400 units at 13.7 units per day, i.e. about 29 days.
- Turnover says how many times the stock “turns over” in a year. P1: 50,000 euros consumed for 4,000 euros of stock, i.e. a turnover of 12.5.
The two indicators are linked: coverage in days ≈ 365 / turnover. Overall, you compute turnover on the totals (total consumption / total stock), like any ratio. Ideally, the denominator is an average stock over the period (average of monthly readings) rather than a snapshot at one date.
Spotting overstock
A simple threshold, set with purchasing, flags items that are over-covered:
overstock = items.index[items["coverage_days"] > 120].tolist()
An item covered for six months while the supplier delivers in two weeks ties up money for no reason. Conversely, a coverage shorter than the supplier lead time signals an upcoming stockout (lesson 2 and lesson 4).
ABC classification
Not all items deserve the same attention. The Pareto principle is often observed: a small share of items accounts for most of the value consumed. ABC classification exploits it:
abc = items.sort_values("usage_value", ascending=False)
abc["cum_share"] = abc["usage_value"].cumsum() / abc["usage_value"].sum()
abc["abc_class"] = "C"
abc.loc[abc["cum_share"] <= 0.95, "abc_class"] = "B"
abc.loc[abc["cum_share"] <= 0.80, "abc_class"] = "A"
- Sort the items by decreasing consumption value.
- Compute the cumulative share.
- Classify: A up to 80%, B up to 95%, C beyond.
Here, P1 and P2 (2 items out of 8) make up 75% of consumption: they are the A items. The 80 / 95 thresholds are a common convention, not a rule: some companies use 70 / 90, or classify on margin rather than consumption. Write down your choice.
Crossing ABC and stock
The classification becomes useful when you cross it with stock:
summary = abc.groupby("abc_class").agg(skus=("abc_class", "size"),
usage_value=("usage_value", "sum"),
stock_value=("stock_value", "sum"))
C items account for only 6% of consumption, but 21% of stock value (3,000 euros out of 14,000): that is where the money sleeps. Typical policy:
- A: careful forecasts, frequent review of parameters, close supplier follow-up;
- B: standard rules;
- C: grouped orders, reduction of the number of references, clearance of dormant items.
Key takeaways
- Coverage = stock / daily consumption; turnover = consumption / average stock.
- ABC classification sorts by value and classifies by cumulative share, with written thresholds.
- Cross ABC and stock value to know where to act.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Coverage and turnover
Add the columns
stock_value,coverage_daysandrotation(annual consumption in value / stock value) toitems. Computeglobal_rotationon the totals andoverstock, the list of items covered for more than 120 days.import pandas as pd items = pd.DataFrame({ "sku": ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8"], "stock_units": [400, 300, 200, 100, 150, 300, 50, 100], "unit_cost": [10.0, 10.0, 10.0, 5.0, 10.0, 5.0, 20.0, 5.0], "annual_usage": [5000, 2500, 800, 1400, 400, 600, 100, 200], # unités consommées par an }).set_index("sku") # global_rotation = ... # overstock = ...ABC classification
Build
abc:itemssorted by decreasing consumption value, withusage_value,cum_shareandabc_class(A if cumulative share ≤ 0.80, B if ≤ 0.95, otherwise C). Buildsummary(index = class) withskus,usage_valueandstock_value, thenc_stock_share: the share of stock value held by C items.import pandas as pd items = pd.DataFrame({ "sku": ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8"], "stock_units": [400, 300, 200, 100, 150, 300, 50, 100], "unit_cost": [10.0, 10.0, 10.0, 5.0, 10.0, 5.0, 20.0, 5.0], "annual_usage": [5000, 2500, 800, 1400, 400, 600, 100, 200], # unités consommées par an }).set_index("sku") # abc = ... # summary = ... # c_stock_share = ...Reading turnover
An item has an annual turnover of 4. What is its approximate coverage?
- 4 days
- About 91 days (three months)
- About 365 days
- 4 months
End-of-lesson quiz
Stock of 600 units, consumption of 20 units per day. Coverage?
- 12 days
- 30 days
- 620 days
In an ABC classification, A items are:
- The most numerous
- Those that weigh the most in the value consumed
- Those with the most stock
Why classify on value rather than quantities?
- Because quantities are unknown
- Because one unit of an expensive item weighs more than one unit of a cheap item
- It makes no difference
The rest of the course
- Stockouts and Service Level15 min
- Forecasting Demand: Naive, Moving Average and Exponential Smoothing18 min
- Supplier Lead Times, Safety Stock and Reorder Point18 min
- Simple Optimization: Economic Order Quantity and Replenishment Policy18 min