Make Your Inventory Simulation in Python | Towards Data Science

This article is an extract from my book Inventory Optimization: Models and Simulations. You can read my other articles here and subscribe to my blog here. I am also active on LinkedIn.
Before jumping in Python and coding our inventory simulation, let’s take the time to define the inventory policy we will simulate: a periodic replenishment inventory policy with an up-to level.
Products following a periodic replenishment policy are ordered periodically, based on a fixed schedule and an up-to level S.
At the beginning of any review period, we need to order enough products to bring our net inventory up to level S. As you can see in the figure above, the order quantity depends on how much stock we have when we make the order and is therefore variable. On the other hand, the orders are made following a fixed schedule: the elapsed time between two consecutive orders will always be the same.
This periodic replenishment policy is actually the most common in supply chains because it is often forced onto supply chains by the widespread use of MRP/DRP. These tools follow a predefined schedule — often daily or weekly — resulting in the implicit use of a periodic review policy.
The fixed review period policy is often noted (R, S), with R being the fixed review period and S the up-to level.
Periodic replenishments allow businesses to group their orders to each of their suppliers. This helps both clients and suppliers to streamline their operations as they can plan orders and workload in advance.
🛡️ Risks. This periodic policy is riskier than a continuous one (where you can make an order at any point in time) due to the blind spot it creates: you cannot order in-between two review periods. If you make an order every Friday with your supplier but are out-of-stock on Monday evening, you will have to wait four more days before making a new order. You will possibly suffer lost sales due to being out-of-stock in the meantime. That is riskier than the fixed reorder point policy, where you would have made a new order directly on Monday evening.
️📦 Variable Order Quantity. Another issue is that the order quantity will vary at each order. This might disrupt a smooth operational flow. For example, if you have a palletized product, you might not want to remove the packaging around an entire pallet to ship a single unit.
In a continuous policy, we order our products based on a fixed threshold. As soon as the net inventory reaches the threshold (or goes below), we request a predetermined number of units from our supplier (or launch a production batch). This threshold is called the reorder point or ROP.
Continuous policies assume that a client can make an order with its supplier at any time. In reality, this might not be the case. For example, a supplier might only accept orders once a month (or only send one shipment a month — which is the same). In such a case, it isn’t reasonable to think that you would follow a fixed reorder point policy, as the supplier is actually following its own calendar. In practice, a real continuous policy is therefore exceptional (some fully automated production processes, or internal processes, could follow these assumptions).
Before jumping in your first inventory simulation, you might want to read my previous article, Inventory Optimization, which lays the foundations for optimizing policies.
towardsdatascience.com
Let’s start by defining the variable time — it will set the duration of our simulation. We can then populate a demand array that follows a normal distribution.
import numpy as nptime = 200d_mu = 100d_std = 25d = np.maximum(np.random.normal(d_mu, d_std, time).round(0).astype(int),0)
In this simulation, we assume that our demand is normally distributed and strictly positive. Feel free to change this assumption by changing the way you set it up.
We can then define the remaining inputs of our policy and compute the various parameters.
from scipy.stats import normL, R, alpha = 4, 1, 0.95 z = norm.ppf(alpha) x_std = np.sqrt(L+R)*d_std
Note that we compute x_std as the demand deviation over the risk-horizon, which I coined in my book.
Risk-horizonMaximum period of time that you need to wait to receive an order. During this period your inventory is at risk of being depleted.
nicolas-vandeput.medium.com
You can see in the figure below how our policy (and simulation) would behave over the first timesteps. The order made at the end of timestep 0 will be available in period timestep 5.
Let’s set up our different inventory levels using the usual safety stock formula.
📦 Cycle stock Cs. Stock needed to fulfill the expected demand (or forecast) over a replenishment cycle.
🛡️ Safety stock Ss. Stock protecting the supply chain against demand variability (or forecast error) and supply lack of reliability.
🚚 In-transit stock Is. Inventory in transit in between different locations. Usually, these are goods ordered from a supplier but not yet available in our warehouse for our clients to buy.
Ss = np.round(x_std*z).astype(int) Cs = 1/2 * d_mu * R Is = d_mu * L S = Ss + 2*Cs + Is
nicolas-vandeput.medium.com
As our incoming orders can stay in transit for multiple periods (in our example, 4 periods), we will define transit as a 2-dimensional array.
hand = np.zeros(time, dtype=int) transit = np.zeros((time,L+1), dtype=int)
As you can see in the table below, a new order will start at the end of the 2nd dimension (transit[time,-1]) and then make its way through the 2nd dimension as time passes by (one layer per period) until it reaches the last slot (transit[time,0]). The last slot means that this in-transit inventory will be received at the end of the current period (in other words, be available as of the next period).
We also have to create two objects to track the service level of our policy:
stock-out_period will contain a Boolean that flags if there is a shortage during a period.
stock-out_cycle will contain a Boolean for each order cycle (i.e., review period) that flags if we had a shortage at any time during the last cycle.
stock−out_period = np.full(time, False, dtype=bool)stock−out_cycle = []
We now have to initialize these arrays for the first timestep. The initial on-hand and net inventories are S minus the demand during the first period. The in-transit stock is initialized for the second period as the demand in the first period.
hand[0] = S − d[0]transit[1,−1] = d[0]
We can now start our simulation. These are the main steps performed at each timestep t:
Check whether we received an order at the beginning of the period (transit[ t-1,0] > 0). If so, we need to compute the cycle service level by checking if there was a stock-out last period.Update the on-hand inventory by subtracting the demand d[t] and adding the received inventory transit[t-1,0]Indicate in stockout_period[t] whether we had a shortage.Update the net inventory position net[t].Remember that it is the total intransit inventory transit[t].sum() plus the on-hand inventory hand[t]Update the in-transit array by offsetting the values of the previous timestep by 1: transit[t,:-1] = transit[t-1,1:]. This represents the fact that the orders move through the supply pipeline.If we are at the review period (t%R==0), we make an order based on the current net inventory position net[t] and the up-to level S. The order is then stored at the extreme of the in-transit array transit[t,L].
Check whether we received an order at the beginning of the period (transit[ t-1,0] > 0). If so, we need to compute the cycle service level by checking if there was a stock-out last period.
Update the on-hand inventory by subtracting the demand d[t] and adding the received inventory transit[t-1,0]
Indicate in stockout_period[t] whether we had a shortage.
Update the net inventory position net[t].Remember that it is the total intransit inventory transit[t].sum() plus the on-hand inventory hand[t]
Update the in-transit array by offsetting the values of the previous timestep by 1: transit[t,:-1] = transit[t-1,1:]. This represents the fact that the orders move through the supply pipeline.
If we are at the review period (t%R==0), we make an order based on the current net inventory position net[t] and the up-to level S. The order is then stored at the extreme of the in-transit array transit[t,L].
for t in range(1,time):   if transit[t−1,0]>0:     stockout_cycle.append(stockout_period[t−1])   hand[t] = hand[t−1] − d[t] + transit[t−1,0]   stockout_period[t] = hand[t] < 0  transit[t,:−1] = transit[t−1,1:]  if 0==t%R:     net = hand[t] + transit[t].sum()     transit[t,L] = S − net
Finally, we can create a DataFrame df to hold our simulation results and plot them on a graph similar to the figure below.
df = pd.DataFrame(data= {'Demand':d, 'On−hand':hand, 'In−transit':list(transit)}) df = df.iloc[R+L:,:] #Remove initialization periods print(df)df['On−hand'].plot(title='Inventory Policy (%d,%d)' %(R,S), ylim=(0,S), legend=True)
We can also print cycle and period service levels.
print('Alpha:',alpha∗100)SL_alpha = 1−sum(stockout_cycle)/len(stockout_cycle)print('Cycle Service Level:', round(SL_alpha∗100,1))SL_period = 1−sum(stockout_period)/timeprint('Period Service Level:', round(SL_period∗100,1))
Simulations are compelling to optimize policies that do not fit the usual assumptions (normality, fixed lead times, no lost sales, etc.). The code above is only a first step towards simulation-driven inventory optimization.
If you want to play with this new tool,
See how much stock variation and service level you can expect from different safety stock levels and lead times.
Try out different demand probabilities functions (such as Poisson or Gamma) to see how they would differ from the normal assumption. You can also use a custom distribution.