In [38]:
from IPython.display import HTML


HTML('''<script>

code_show=true; 

function code_toggle() {

 if (code_show){

 $('div.input').hide();

 } else {

 $('div.input').show();

 }

 code_show = !code_show

} 

$( document ).ready(code_toggle);

</script>

<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code."></form>''')
Out[38]:
In [13]:
from gurobipy import *
m = Model("Westvaco")
from IPython.core.display import Image
Image(filename='logo.png', width=500)
Out[13]:

Optimized Motor Carrier Selection at Westvaco

                                                                                Analyst: Jiao Jiang
                                                                                Date: 2019/11/10

1. Introduction

Company Background: Westvaco is the second largest American packaging company. It sells more than $2 billion worht of manufactured paper, paperboard, and speciality chemcials annually.
Distribution Problem: how to assign truckloads to carriers and transport the products at supply location to destinations where they are demanded at a minimum cost.

2. Model

2.1 Data:
Trips: the number of truckloads required for each destinations.
Stops: the number of stops required for each destinations.
Miles: the total number of miles for the trip to final destination.
Carriers: there are 6 carriers ABCT,IRST,LAST,MRST,NEST,PSST offers delivery services. The rates are different to various destinations. * indicates carrier do not travel to this destination. rates in dollars/mile.
minCharge: minimum truckload charged by each carrier.
stopoffCharge: $ charged each time the truck stoped.

In [15]:
#from gurobipy import *
m = Model("Westvaco")
import pandas as pd
destination = ['Atlanta','Everett','Ephrata','Riverview','Carson','Chamblee','Roseville','Hanover','Sparks','Parsippany','Effingham','Kearny']
states = ['GA','MA','PA','MI','CA','GA','MN','PA','NV','NJ','IL','NJ']
trips = [4,1,3,5,1,1,1,1,2,1,5,7]
stops = [0,3,0,0,2,0,3,0,0,1,0,0]
miles = [612, 612, 190, 383, 3063, 429, 600, 136, 2439, 355, 570, 324]
minCharge = [350,400,350,300,350,300]
stopoffCharge = [50,75,50,35,50,50]
ABCT = ['*','*','*',0.79,'*','*',1.24,'*','*','*',0.87,'*']
IRST = [0.88,1.18,3.42,1.01,0.8,1.23,1.13,4.78,1.45,1.62,0.87,2.01]
LAST = [1.15,1.27,1.73,1.25,0.87,1.61,1.89,2.23,'*',1.36,1.25,1.54]
MRST = [0.87,1.39,1.71,0.96,'*',1.22,1.32,2.39,1.2,1.39,0.87,1.53]
NEST = [0.95,1.35,1.82,0.95,1.00,1.33,1.41,2.26,'*',1.03,0.9,1.28]
PSST = [1.05,1.28,2,1.11,'*',1.47,1.41,2.57,'*',1.76,1.31,1.95]
given=[ABCT,IRST,LAST,MRST,NEST,PSST]
dataframe1 = pd.DataFrame.from_dict({
    'Destination':destination,
    'State':states,
    'Trips':trips,
    'Stops':stops,
    'Miles':miles,
    'ABCT':ABCT,
    'IRST':IRST,
    'LAST':LAST,
    'MRST':MRST,
    'NEST':NEST,
    'PSST':PSST
})
dataframe1
Out[15]:
Destination State Trips Stops Miles ABCT IRST LAST MRST NEST PSST
0 Atlanta GA 4 0 612 * 0.88 1.15 0.87 0.95 1.05
1 Everett MA 1 3 612 * 1.18 1.27 1.39 1.35 1.28
2 Ephrata PA 3 0 190 * 3.42 1.73 1.71 1.82 2
3 Riverview MI 5 0 383 0.79 1.01 1.25 0.96 0.95 1.11
4 Carson CA 1 2 3063 * 0.80 0.87 * 1 *
5 Chamblee GA 1 0 429 * 1.23 1.61 1.22 1.33 1.47
6 Roseville MN 1 3 600 1.24 1.13 1.89 1.32 1.41 1.41
7 Hanover PA 1 0 136 * 4.78 2.23 2.39 2.26 2.57
8 Sparks NV 2 0 2439 * 1.45 * 1.2 * *
9 Parsippany NJ 1 1 355 * 1.62 1.36 1.39 1.03 1.76
10 Effingham IL 5 0 570 0.87 0.87 1.25 0.87 0.9 1.31
11 Kearny NJ 7 0 324 * 2.01 1.54 1.53 1.28 1.95
In [18]:
print("Minimum charge per truckload for each carrier is", minCharge)
print("Stop-off charge for each carrier is", stopoffCharge)
Minimum charge per truckload for each carrier is [350, 400, 350, 300, 350, 300]
Stop-off charge for each carrier is [50, 75, 50, 35, 50, 50]
In [19]:
from IPython.core.display import Image
Image(filename='map.png', width=500)
Out[19]:

2.2 Cost Table:
First, calculate the cost for each carrier using fomular: cost = rate miles + stops stopoffCharge.
If the cost is less than minCharge, we should pay at least the minCharge.
If rate is not specified, assign a very large cost. Then create a cost table:

In [23]:
cost = []
for j in range(len(minCharge)):
    for i in range(len(trips)):
        if given[j][i] =='*':
            cost.append("-")
        if given[j][i] != '*':
            if given[j][i]*miles[i]+stops[i]*stopoffCharge[j] <= minCharge[j]:
                cost.append(minCharge[j])
            else:
                cost.append(given[j][i]*miles[i]+stops[i]*stopoffCharge[j])
ABCTcost = cost[:12]
IRSTcost = cost[12:24]
LASTcost = cost[24:36]
MRSTcost = cost[36:48]
NESTcost = cost[48:60]
PSSTcost = cost[60:]
cost = [ABCTcost,IRSTcost,LASTcost,MRSTcost,NESTcost,PSSTcost]
dataframe2 = pd.DataFrame.from_dict({
    'Destination':destination,
    'State':states,
    'ABCTcost':ABCTcost,
    'IRSTcost':IRSTcost,
    'LASTcost':LASTcost,
    'MRSTcost':MRSTcost,
    'NESTcost':NESTcost,
    'PSSTcost':PSSTcost
})
dataframe2
Out[23]:
Destination State ABCTcost IRSTcost LASTcost MRSTcost NESTcost PSSTcost
0 Atlanta GA - 538.56 703.8 532.44 581.4 642.6
1 Everett MA - 947.16 927.24 955.68 976.2 933.36
2 Ephrata PA - 649.80 350 324.9 350 380
3 Riverview MI 350 400.00 478.75 367.68 363.85 425.13
4 Carson CA - 2600.40 2764.81 - 3163 -
5 Chamblee GA - 527.67 690.69 523.38 570.57 630.63
6 Roseville MN 894 903.00 1284 897 996 996
7 Hanover PA - 650.08 350 325.04 350 349.52
8 Sparks NV - 3536.55 - 2926.8 - -
9 Parsippany NJ - 650.10 532.8 528.45 415.65 674.8
10 Effingham IL 495.9 495.90 712.5 495.9 513 746.7
11 Kearny NJ - 651.24 498.96 495.72 414.72 631.8

2.3 Decision Variables: there are 72 decision variables in total: 12 cities and 6 carriers. Westvaco is deciding the allocation of truckloads.

In [30]:
N=12
list1 = [[m.addVar(vtype=GRB.INTEGER, name="ABCT"+str(i+1)) for i in range(N)],
         [m.addVar(vtype=GRB.INTEGER, name="IRST"+str(i+1)) for i in range(N)],
         [m.addVar(vtype=GRB.INTEGER, name="LAST"+str(i+1)) for i in range(N)],
         [m.addVar(vtype=GRB.INTEGER, name="MRST"+str(i+1)) for i in range(N)],
         [m.addVar(vtype=GRB.INTEGER, name="NEST"+str(i+1)) for i in range(N)],
         [m.addVar(vtype=GRB.INTEGER, name="PSST"+str(i+1)) for i in range(N)]]
m.update()
dataframe3 = pd.DataFrame.from_dict({
    'Destination':['Atlanta','Everett','Ephrata','Riverview','Carson','Chamblee','Roseville','Hanover','Sparks','Parsippany','Effingham','Kearny','Constrain2','Constrain3'],
    'State':['GA','MA','PA','MI','CA','GA','MN','PA','NV','NJ','IL','NJ','Available pulls','Commitment'],
    'ABCTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',4,1],
    'IRSTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',8,7],
    'LASTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',7,6],
    'MRSTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',7,0],
    'NESTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',3,0],
    'PSSTvariable':['-','-','-','-','-','-','-','-','-','-','-','-',4,4],
    'Constrain1':[4,1,3,5,1,1,1,1,2,1,5,7,'','']
})
dataframe3
Out[30]:
Destination State ABCTvariable IRSTvariable LASTvariable MRSTvariable NESTvariable PSSTvariable Constrain1
0 Atlanta GA - - - - - - 4
1 Everett MA - - - - - - 1
2 Ephrata PA - - - - - - 3
3 Riverview MI - - - - - - 5
4 Carson CA - - - - - - 1
5 Chamblee GA - - - - - - 1
6 Roseville MN - - - - - - 1
7 Hanover PA - - - - - - 1
8 Sparks NV - - - - - - 2
9 Parsippany NJ - - - - - - 1
10 Effingham IL - - - - - - 5
11 Kearny NJ - - - - - - 7
12 Constrain2 Available pulls 4 8 7 7 3 4
13 Constrain3 Commitment 1 7 6 0 0 4

2.4 Objectives: the objective function is minimize the total cost. Total cost is the sum of multiplication of decision variables and cost table for each carrier: sum(cost for each carrier*decison variables)

In [59]:
sum1=[]
for i in range(len(list1)):
    for j in range(len(list1[i])):
        sum1.append(list1[i][j]*cost[i][j])
m.setObjective(sum(sum1), GRB.MINIMIZE)
m.update()

2.5 Constrains:
Constrain1: there are required number of trips to each city,
Constrain2: available pulls, upper limit for each carrier,
Constrain3: Westvaco has commitment to some of the carries, lower limit.

In [60]:
constr1 = ['Trip1','Trip2','Trip3','Trip4','Trip5','Trip6','Trip7','Trip8','Trip9','Trip10','Trip11','Trip12']
for j in range(N):
    m.addConstr(list1[0][j]+list1[1][j]+list1[2][j]+list1[3][j]+list1[4][j]+list1[5][j], GRB.EQUAL, trips[j], constr1[j])
m.update()
ub = [4,8,7,7,3,4]
lb = [1,7,6,0,0,4]
constr2 = ['ub1','ub2','ub3','ub4','ub5','ub6']
constr3 = ['lb1','lb2','lb3','lb4','lb5','lb6']
for i in range(len(ub)):
    m.addConstr(sum(list1[i]), GRB.LESS_EQUAL, ub[i], constr2[i])
    m.addConstr(sum(list1[i]), GRB.GREATER_EQUAL, lb[i], constr3[i])
m.update()

2.6 Optimization and results: use m.optimize() function will return the lowest cost feasible. In this case is $22394; the result table shows the least cost assignmnent of truckloads to carriers that meets the necessary requirements

In [62]:
m.optimize()
a = []
for v in m.getVars():
    if '%g' % (v.x) == -0:
        a.append(0)
    else:
        a.append(int('%g' % (v.x)))
        
results = pd.DataFrame.from_dict({
    'Destination': destination,
    'ABCT': a[0:12],
    'IRST': a[12:24],
    'LAST': a[24:36],
    'MRST': a[36:48],
    'NEST': a[48:60],
    'PSST': a[60:],
    'TRIPS':trips
})
results
Optimize a model with 24 rows, 72 columns and 216 nonzeros
Variable types: 0 continuous, 72 integer (0 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [3e+02, 1e+05]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 8e+00]
Presolved: 21 rows, 72 columns, 180 nonzeros

Continuing optimization...


Explored 0 nodes (26 simplex iterations) in 0.01 seconds
Thread count was 12 (of 12 available processors)

Solution count 2: 22394.4 422994 

Optimal solution found (tolerance 1.00e-04)
Best objective 2.239438000000e+04, best bound 2.239438000000e+04, gap 0.0000%
Out[62]:
Destination ABCT IRST LAST MRST NEST PSST TRIPS
0 Atlanta 0 0 0 4 0 0 4
1 Everett 0 0 0 0 0 1 1
2 Ephrata 0 0 1 0 0 2 3
3 Riverview 4 0 0 1 0 0 5
4 Carson 0 1 0 0 0 0 1
5 Chamblee 0 1 0 0 0 0 1
6 Roseville 0 1 0 0 0 0 1
7 Hanover 0 0 0 0 0 1 1
8 Sparks 0 0 0 2 0 0 2
9 Parsippany 0 0 0 0 1 0 1
10 Effingham 0 5 0 0 0 0 5
11 Kearny 0 0 5 0 2 0 7

Recommendation

The result table above shows how to distribute truckload for each carrier:
ABCT, Westvaco should assign 4 truckloads in total and all towards Riverview.
IRST, 1 truckload to Carson, 1 truckload to Chamblee, 1 truckload to Roseville, and 5 truckload to Effingham.
LAST, 1 truckload to Ephrata, 5 truckload to Kearny.
MRST, 4 truckloads to Atlanta, 1 truckload to Riverview and 2 truckloads to Sparks.
NEST, 1 truckload to Parsippany, and 2 truckloads to Kearny.
PSST, 1 truckload to Evertt, 2 truckloads to Ephrata and 1 truckload to Hanover.

The cost of distribution plan is $22394. From the article we know that the annual trucking costs is about 15 million dollar per year, which is 41095 dollar per day. By implementing LP solution, it only cost 22394 dollar per day. This model can save Westvaco 18702 per day, 6826230 per year. The model can successfully reduce the cost by 45.5%

In [32]:
#15000000/365
#(22394-41096)/41096
#18702*365
Out[32]:
6826230
In [36]:
from IPython.core.display import Image
Image(filename='costdecrease.png', width=350)
Out[36]:
In [ ]: