In part 1, we built a crucial foundation to understand problem of inventory imbalance. Here, in part 2, we will dig deeper into the solution to the presented problem and how we can implement the presented solution in practice with a set of business constraints.
Linear programming is one of the most widely used optimization tools in every industry which operates under conflicting constraints. Here, the problem of inventory imbalance in the retail industry exists with a set of business constraints like transfer can’t happen across regions resulting in intra-region transfer constraint.
Let’s discuss various business constraints in detail to bring a sense of complexity of problem and to understand requirements of proposed solutions.
Below described constraints will be building blocks of linear programming models to ensure solution alignment with business constraints.
Above constraints can be seen in the majority of the retail industry, but there are some constraints unique to specific businesses which have a unique strategy under which they operate.
For example, some retail chains, specifically in fashion retail, want to have stock of every size of given article. This, as a constraint, suggests transfer which keeps the minimum stock of each size of given article.
How can a described situation be modeled as a linear programming (LP) problem?
At this point, we have a fair understanding of inventory imbalance problem, under which constraints, design of solution is considered and why linear programming model, as a solution makes sense. This offers a nice opportunity to make our hands dirty now and see the solution in practice.
Like linear programming, Python is a widely used programming language in the field of optimisation and data science. In python, there is a dedicated package/library for solving optimization problems.
Here, we try to demonstrate usage of the PuLp package with a sample dataset. Below implementation can be very useful for applying this solution with any similar real world optimization problems.
Dataset 1 : Demand and Stock Format: (store, stock, demand)
Above table represents stock and demand in respective stores. Like store B has demand of 6 units but stock of 3 units only. We can observe that, some stores can give stock and some need to receive stock in order to meet demand. For simplicity, we assume the above dataset is considered for any single hypothetical item.
Now, let’s consider a dataset for various costs like transportation cost and holding costs. Before moving any further, it is necessary to understand the purpose of these costs in design of solution.
Transportation costs define the cost of transfer between any given 2 stores, and holding costs define the cost for holding a single unit of inventory in a given store for a certain duration. Intuitively, holding cost can be seen as a good representative of the DSI metric. High holding cost suggests longer DSI and lower holding cost suggests lower DSI.
Above table, shows store to store transfer cost. 0 cost shows transfer between same stores and in reality such transfer will not take place. But such every single constraint needs to be part of the solution.
Note: We will present important code blocks of solutions designed in python programming language.
Define decision variables
transfer_variables = pulp.LpVariable.dicts(name='X', indexs=indexes, lowBound=0, cat='Integer') binary_transfer_variables = pulp.LpVariable.dicts(name='Y', indexs=indexes, lowBound=0, upBound = 1, cat='Integer') ending_inventory_variables = pulp.LpVariable.dicts(name='EI', indexs=u_stores, lowBound=0, cat='Integer') satisfied_demand = pulp.LpVariable.dicts(name='SD', indexs=u_stores, lowBound=0, cat='Integer')
u_stores holds a list as
indexes holds list as ['A_B', 'A_C', 'B_A', 'B_C', 'C_A', 'C_B']
transfer_variables define transferred quantity between given stores.
binary_transfer_variables define whether transfer happened between given stores or not.
ending_inventory_variables define remaining stock at store at the end of period given that sale happened according to demand.
satisfied_demand defines the number of units sold at a given store according to new stock (after transfer) and given demand.
# Define and initialize model model = pulp.LpProblem(name='Inventory_Redistribution', sense=pulp.LpMinimize)
Define objective function
# Objective function total_logistic_cost = pulp.lpSum([transportation_cost.loc[tv.split('_')][tv.split('_')]*transfer_variables[tv] for tv in transfer_variables.keys()]) total_holding_cost = pulp.lpSum([holding_cost[s]*ending_inventory_variables[s] for s in u_stores]) objective = total_holding_cost + total_logistic_cost model.setObjective(objective)
Below, mathematical representation is also defined alongside the code equivalent. There, X stands for source store and Y stands for destination store, M stands for arbitrary large number and MTQ stands for threshold for minimum transfer quantity for given item.
# Setting up binary variable, if transfer happens # Mathematical representation Y[X,Y] - X[X,Y] <= 0 X[X,Y] - M*Y[X,Y] <= 0 for key, value in transfer_variables.items(): model.addConstraint(pulp.LpConstraint( e = binary_transfer_variables[key] - transfer_variables[key], sense = pulp.LpConstraintLE, name = 'Y_'+key+'_1', rhs = 0 )) model.addConstraint(pulp.LpConstraint( e = transfer_variables[key] - M*binary_transfer_variables[key], sense = pulp.LpConstraintLE, name = 'Y_'+key+'_2', rhs = 0 )) # Minimum transfer quantity # Mathematical representation X[X,Y] - MTQ*Y[X,Y] >= 0 for key, value in transfer_variables.items(): model.addConstraint(pulp.LpConstraint( e = transfer_variables[key]-MTQ * binary_transfer_variables[key], sense = pulp.LpConstraintGE, name = 'MTQ_'+key, rhs = 0 )) # Transfer quantity and destination demand # Mathematical representation X[X,Y] - Demand[Y] <= 0 for key, value in transfer_variables.items(): model.addConstraint(pulp.LpConstraint( e = transfer_variables[key] - demand_dic[key.split('_')], sense = pulp.LpConstraintLE, name = 'TQ_DEST_DE_'+key, rhs = 0 )) # satisfied demand # Mathematical representation SD[store] - Demand[store] <= 0 SD[store] - Stock[store] - TO[store] + TI[store] <=0 (TO stands for Transfer Out from store, and TI stands for Transfer In) for s in u_stores: model.addConstraint(pulp.LpConstraint( e = satisfied_demand[s] - demand_dic[s], sense = pulp.LpConstraintLE, name = 'SD_DE_'+s, rhs = 0 )) model.addConstraint(pulp.LpConstraint( e = satisfied_demand[s] - closing_dic[s] - pulp.lpSum([transfer_variables[v] for v in transfer_variables.keys() if s in v.split('_')]) + pulp.lpSum([transfer_variables[v] for v in transfer_variables.keys() if s in v.split('_')]) , sense = pulp.LpConstraintLE, name = 'SD_INV_'+s, rhs = 0 )) # ending inventory level # Mathematical representation EI[store] - Stock[store] - TO[store] + TI[store] + SD[store] = 0 for s in u_stores: model.addConstraint(pulp.LpConstraint( e = ending_inventory_variables[s] - closing_dic[s] - pulp.lpSum([transfer_variables[v] for v in transfer_variables.keys() if s in v.split('_')]) + pulp.lpSum([transfer_variables[v] for v in transfer_variables.keys() if s in v.split('_')]) + satisfied_demand[s], sense = pulp.LpConstraintEQ, name = 'EI_'+s, rhs = 0 ))
Solving model and extracting suggested transfers
# solve model model.solve()