This notebook covers multi-period inventory planning: deciding how much to order in each period so that all demand is met at the lowest total order and holding cost. It is our first multi-period model, in which a decision in one period affects all later periods, and it needs a new kind of variable, the state variable tracked over time. Like the applications of Lecture 9, the problem is introduced in the same steps: a practical motivation, the generic model built up with the four modeling steps, the model for a concrete example, its solution in pulp, and possible extensions.
Learning outcomes
On completion of this notebook, you will be able to:
formulate a multi-period inventory planning problem as an LO model;
use state variables and balance constraints to link consecutive periods;
extend the model with capacities, several products and fixed order costs.
Multi-Period Inventory Planning¶
Practical Motivation¶
A shop sells a product whose demand varies from week to week, and the purchase price varies as well (because of supplier promotions, or seasonal prices). Ordering a lot when the price is low saves money, but products that sit in stock cost money too: storage space, capital, insurance. When should the shop order, and how much, to meet all demand at the lowest total cost? This is an example of a multi-period model: a model in which the state of a system (here, the stock) is tracked over time, and a decision in one period affects all later periods.
Modeling¶
Problem Definition and Example¶
We hold a single product and plan for periods (the time horizon). At the start we have units in stock. For every period we know the demand , the order cost per unit ordered in period , and the holding cost per unit left in stock at the end of period . An order arrives immediately. The problem is to decide how much to order in each period, such that all demand is met from stock (no backorders) at minimal total order and holding cost. The parameters , and typically come from a forecast (predictive analytics).
As an example, take periods with an initial stock of and
| period | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| demand | 7 | 9 | 5 | 8 |
| order cost | 8 | 11 | 7 | 10 |
| holding cost | 1 | 1 | 1 | 1 |
Decision Variables¶
The decisions are the order quantities
We also introduce the stock at the end of each period as a decision variable,
Strictly speaking, is not a free choice: once the orders are fixed, the stock follows from them. We could substitute everywhere, but that makes every constraint and the objective much longer. A variable like , which describes the state of the system in a period, is called a state variable; it keeps a multi-period model short and readable.
Objective¶
In period we pay for ordering and for holding stock, so the total cost is
Constraints¶
The stock at the end of period is the stock at the end of the previous period, plus what is ordered for period (assuming it arrives at start of that period), minus what is sold during that period:
This balance constraint links each period to the previous one; for , is the given initial stock. Orders cannot be negative, . The requirement is what forbids backorders: if demand in period were not met from stock, the balance constraint would give a negative .
Complete LO Model¶
Solving the Example in pulp¶
The balance constraints are equalities, written with == in pulp. Python lists start
at index 0, so period is t + 1 in the variable names, and the first period uses
the initial stock s0 in place of the previous period’s stock variable.
import pulp
demand = [7, 9, 5, 8]
holding = [1, 1, 1, 1]
order_cost = [8, 11, 7, 10]
s0 = 6
periods = range(len(demand))
inventory = pulp.LpProblem(name="inventory", sense=pulp.LpMinimize)
order = [pulp.LpVariable(name=f"x_{t + 1}", lowBound=0) for t in periods]
stock = [pulp.LpVariable(name=f"s_{t + 1}", lowBound=0) for t in periods]
inventory += pulp.lpSum(
order_cost[t] * order[t] + holding[t] * stock[t] for t in periods
)
for t in periods:
prev = s0 if t == 0 else stock[t - 1]
inventory += stock[t] == prev + order[t] - demand[t], f"balance_{t + 1}"
inventory.solve(pulp.PULP_CBC_CMD(msg=False))
print("status:", pulp.LpStatus[inventory.status])
print("orders:", [order[t].value() for t in periods])
print("end-of-period stock:", [stock[t].value() for t in periods])
print("total cost:", inventory.objective.value())status: Optimal
orders: [10.0, 0.0, 13.0, 0.0]
end-of-period stock: [9.0, 0.0, 8.0, 0.0]
total cost: 188.0
The optimal plan orders only in the two cheap periods, 1 and 3. Ordering period 2’s demand in period 1 costs per unit (order plus one period of holding), which is less than the 11 of ordering it in period 2; period 4’s demand is ordered in period 3 for the same reason.
Extensions¶
Capacities¶
A storage capacity limits the stock, , and if the product is made rather than bought, a production capacity limits the amount made in period , . Both are one extra constraint per period, without a new modeling trick.
Several Products¶
With several products, every variable and parameter gets a product index, and each product has its own balance constraints. Constraints for resources that the products share, such as storage space, link the products together.
Fixed Order Costs¶
Fixed order costs, which are paid in every period in which something is ordered, do need a new trick:
References¶
Koole, G. (2019). An Introduction to Business Analytics. §6.3 (multi-period). Spreadsheet material replaced with pulp.