Chapter 4 Sensitivity Analysis

We can get a lot more than just the objective function value and the decision variable values from a solved linear program. In particular, we can potentially explore the impact of changes in constrained resources, changes in the objective function, forced changes in decision variables, and the introduction of additional decision variables.

4.1 Base Case

To demonstrate this, let’s revisit our explicit model of production planning. We will use the explicit version for the sake of clarity and simplicity but the same techniques could be used for the generalized model or other linear programs.

Recall the three variable explicit production planning problem from Chapter 2.

\[ \begin{split} \begin{aligned} \text{Max } & 7\cdot Ants +12 \cdot Bats +5\cdot Cats \\ \text{s.t.: } & 1\cdot Ants + 4\cdot Bats +2\cdot Cats \leq 800 \\ & 3\cdot Ants + 6\cdot Bats +2\cdot Cats \leq 900 \\ & 2\cdot Ants + 2\cdot Bats +1\cdot Cats \leq 480 \\ & 2\cdot Ants + 10\cdot Bats +2\cdot Cats \leq 1200 \\ & Ants, Bats, Cats \geq 0 \\ \end{aligned} \end{split} \]

The implementation that we did earlier for production planning was straightforward.

Base3VarModel <- MIPModel() |>
  add_variable(Ants, type = "continuous", lb = 0) |>
  add_variable(Bats, type = "continuous", lb = 0) |>
  add_variable(Cats, type = "continuous", lb = 0) |>
  
  set_objective(7*Ants + 12*Bats + 5*Cats,"max")  |>
  
  add_constraint(1*Ants + 4*Bats + 2*Cats<=800)   |> 
  add_constraint(3*Ants + 6*Bats + 2*Cats<=900)   |> 
  add_constraint(2*Ants + 2*Bats + 1*Cats<=480)   |> 
  add_constraint(2*Ants + 10*Bats + 2*Cats<=1200) |> 
  solve_model(with_ROI(solver="glpk"))

base_case_res <- cbind(objective_value(Base3VarModel),
                    get_solution(Base3VarModel, Ants),
                    get_solution(Base3VarModel, Bats),
                    get_solution(Base3VarModel, Cats))
colnames(base_case_res)<-list("Profit", "Ants", 
                              "Bats", "Cats")
rownames(base_case_res)<-list("Base Case")
kbl(base_case_res, booktabs=T,
    caption="Base Case Production Plan") |>
  kable_styling(latex_options = "hold_position")
TABLE 4.1: Base Case Production Plan
Profit Ants Bats Cats
Base Case 2225 50 0 375

In the base case, we are producing Ants and Cats but not Bats to generate a total profit of \(\$2225\).

4.2 Shadow Prices

4.2.1 Extraction and Interpretation

There are many resources, some are fully used while some are not fully utilized. How do we prioritize the importance of each resource? For example, if the factory manager could add a worker to one department, which should it be? Conversely, if an outside task came up where should she draw the time from? We could modify the model and rerun. For complex situations, this may be necessary. On the other hand, we could also use sensitivity analysis to explore the relative value of the resources.

Let’s begin by examining the row duals, also known as shadow prices.

rduals1 <-as.matrix(get_row_duals(Base3VarModel))
dimnames(rduals1)<-list(c("Machining", "Assembly", 
                          "Testing", "Sensors"), 
                          c("Row Duals"))
kbl(format(rduals1,digits=4), booktabs=T,
    caption="Shadow Prices of Constrained Resources") |>
  kable_styling(latex_options = "hold_position")
TABLE 4.2: Shadow Prices of Constrained Resources
Row Duals
Machining 0.25
Assembly 2.25
Testing 0.00
Sensors 0.00

The row duals or shadow prices for testing is zero. This means that the marginal value of one additional hour of testing labor time is \(\$0\). This makes sense if you examine the amount of testing time used and realize that the company is not using all of the 480 hours available. Therefore adding more testing hours certainly can’t help improve the production plan.

On the other hand, all of the assembly time (resource) is used in the optimal solution. The shadow price of an hour of assembly time is \(\$2.25\). This means that for every hour of additional assembly time within certain limits, the objective function will increase by \(\$2.25\) also. 

All of the 900 hours of labor available in the assembly center are consumed by the optimal production plan. Increasing the assembly hours available may allow the company to change the production plan and increase the profit. While you could rerun the model with increased assembly hours to determine the new optimal production plan but if you only want to know the change in the optimal objective function value, you can determine that from the shadow price of the assembly constraint. Each additional hour (within a certain range) will increase the profit by \(\$2.25\).

This potential for increased profit can’t be achieved by simply increasing the resource - it requires a modified production plan to utilize this increased resource. To find the production plan that creates this increased profit, let’s solve a modified linear program.

4.2.2 Example of Adding an Hour to Assembly

Let’s test the numerical results from the Shadow Price table by adding an hour of labor to the assembly department. The model is represented in the following formulation.

\[ \begin{split} \begin{aligned} \text{Max: } & 7\cdot Ants+12\cdot Bats+5\cdot Cats \\ \text{s.t. } & 1\cdot Ants+4\cdot Bats+2\cdot Cats \leq 800 \\ & 3\cdot Ants+6\cdot Bats+2\cdot Cats \leq \textcolor{red}{901} \\ & 2\cdot Ants+2\cdot Bats+1\cdot Cats \leq 480\\ & 2\cdot Ants+10\cdot Bats+2\cdot Cats \leq 1200 \\ & Ants, \; Bats, \; Cats \geq 0 \end{aligned} \end{split} \]

The code implements the revised model.

IncAssemHrs <- MIPModel() |>
  add_variable(Ants, type = "continuous", lb = 0)  |>
  add_variable(Bats, type = "continuous", lb = 0)  |>
  add_variable(Cats, type = "continuous", lb = 0)  |>
  
  set_objective(7*Ants + 12*Bats + 5*Cats,"max")   |>
  
  add_constraint(1*Ants + 4*Bats + 2*Cats<= 800)   |> 
  add_constraint(3*Ants + 6*Bats + 2*Cats<= 901)   |> 
  add_constraint(2*Ants + 2*Bats + 1*Cats<= 480)   |> 
  add_constraint(2*Ants + 10*Bats + 2*Cats<= 1200) |> 

  solve_model(with_ROI(solver = "glpk"))

inc_assem_res  <- cbind(objective_value(IncAssemHrs),
                     get_solution(IncAssemHrs, Ants),
                     get_solution(IncAssemHrs, Bats),
                     get_solution(IncAssemHrs, Cats))
colnames(inc_assem_res)<-list("Profit", 
                           "Ants", 
                           "Bats", 
                           "Cats")
rownames(inc_assem_res)<-list("+1 Assembly Hr")
temp1 <- rbind(base_case_res, inc_assem_res)
kbl(temp1, booktabs=T,
    caption="Production Plan with One Additional Assembly Hour") |>
  kable_styling(latex_options = "hold_position")
TABLE 4.3: Production Plan with One Additional Assembly Hour
Profit Ants Bats Cats
Base Case 2225.00 50.0 0 375.00
+1 Assembly Hr 2227.25 50.5 0 374.75

These results confirmed that adding one hour of Testing time results in a new production plan that generates an increased profit of \(\$2.25\), exactly as expected.

While it is easy to look at an individual resource when we are looking at problems with only a couple of constraints, the shadow prices can be very helpful in larger problems with dozens, hundreds, or thousands of resources where questions might come up of trying to evaluate or prioritize limited resources.

4.2.3 Shadow Prices of Underutilized Resources

The shadow price on sensors is zero (as was also the case for testing hours). This means that even a large increase in the number of sensors would not affect the maximum profit or the optimal production plan. Essentially there is plenty of sensors available, having more would not allow a better profit plan to be possible. Let’s confirm this as well with a numerical example by adding 10,000 more sensors.

\[ \begin{split} \begin{aligned} \text{Max: } & 7\cdot Ants+12\cdot Bats+5\cdot Cats \\ \text{s.t. } & 1\cdot Ants+4\cdot Bats+2\cdot Cats \leq 800 \\ & 3\cdot Ants+6\cdot Bats+2\cdot Cats \leq 900 \\ & 2\cdot Ants+2\cdot Bats+1\cdot Cats \leq 480 \\ & 2\cdot Ants+10\cdot Bats+2\cdot Cats \leq \textcolor{red}{11200} \\ & Ants, \; Bats, \; Cats \geq 0 \end{aligned} \end{split} \]

In the same way that it was done for adjusting assembly hours earlier, the next chunk shows how to examine the case of a huge increase in the number of sensors.

IncSensor<- MIPModel() |>
  add_variable(Ants, type = "continuous", lb = 0) |>
  add_variable(Bats, type = "continuous", lb = 0) |>
  add_variable(Cats, type = "continuous", lb = 0) |>
  
  set_objective(7*Ants + 12*Bats + 5*Cats,"max")  |>

  add_constraint(1*Ants+4*Bats + 2*Cats<= 800)    |> 
  add_constraint(3*Ants+6*Bats + 2*Cats<=900)     |> 
  add_constraint(2*Ants+2*Bats+1*Cats<=480)       |> 
  add_constraint(2*Ants+10*Bats+2*Cats<=11200)    |> 
  solve_model(with_ROI(solver = "glpk"))

inc_sensor_res  <- cbind(objective_value(IncSensor),
                     get_solution (IncSensor, Ants),
                     get_solution (IncSensor, Bats),
                     get_solution (IncSensor, Cats))
colnames(inc_sensor_res)<-list("Profit", 
                           "Ants", 
                           "Bats", 
                           "Cats")
rownames(inc_sensor_res)<-list("Increased Sensors")
temp2 <- rbind(base_case_res, inc_assem_res, inc_sensor_res)
TABLE 4.4: Production Plan with 10,000 More Sensors.
Profit Ants Bats Cats
Base Case 2225.00 50.0 0 375.00
+1 Assembly Hr 2227.25 50.5 0 374.75
Increased Sensors 2225.00 50.0 0 375.00

Even this massive increase of sensors does not result in any increase in profit or change the production plan.

4.3 Reduced Costs of Variables

Next, we move on to the reduced costs of variables. The reduced cost for a variable is the per unit marginal profit (objective function coefficient) minus the per unit value (in terms of shadow prices) of the resources used by a unit in production. The reduced costs is also often referred to as the column duals. The concept and use of reduced costs frequently may require rereading several times. The mathematical details rely on the structure of linear programming and the Simplex method. We won’t go into detail on the origin of this mathematically in detail here.

4.3.1 Reduced Cost of Ants

Let’s start by examining the Ants. The reduced cost for Ants is the per unit marginal profit minus the per unit value (in terms of shadow prices) of the resources used by a unit in production.

Let’s extract the reduced costs from the results just as we did for the shadow prices. Note that the reduced costs are referred to as column duals in ompr.

cduals1 <-as.matrix(get_column_duals(Base3VarModel) )
dimnames(cduals1)<-list(c("Ants", "Bats", "Cats"), 
                        c("Column Duals"))
TABLE 4.5: Reduced Costs of Variables
Column Duals
Ants 0.0
Bats -2.5
Cats 0.0

These results are interesting. The reduced costs of variables that are between simple upper and lower bounds will be zero. Again, the reduced cost of a variable is the difference between the value of the resources consumed by the product and the value of the product in the objective function. All of our product’s variables have simple lower bounds of zero and no upper bounds. Ants and Cats have zero reduced cost while Bats have a negative reduced cost. Let’s see if this is consistent with the interpretation of reduced costs.

Let’s start by examining the shadow prices of the resources along with the number of each resource used in the production of a single Ant.

ant_res_used<-cbind(rduals1,c(1,3,2,2))
colnames(ant_res_used)<-c("Row Duals", "Resources Used")
ant_res_used <- rbind(ant_res_used,
                      c("TOTAL",t(rduals1)%*%c(1,3,2,2)))
kbl(format(ant_res_used, digits=4), booktabs=T,
    caption="Resources Used by an Ant and shadow prices of resources")
TABLE 4.6: Resources Used by an Ant and shadow prices of resources
Row Duals Resources Used
Machining 0.25 1
Assembly 2.25 3
Testing 0 2
Sensors 0 2
TOTAL 7

Taking the product in each row and adding them up will give the marginal value of the resources consumed by an incremental change in production of Ants. In this case, the marginal value is \(1\cdot 0.25+3\cdot2.25+2\cdot0+2\cdot0=7\). Since the profit per Ant (from the objective function coefficient on Ants) is also \(\$ 7\), they are in balance and the difference between them is zero which is why the reduced cost for Ants is zero. This can be thought of saying that the marginal benefit is equal to the marginal cost of a very small forced change in the value of the Ants variable. The value of the resources used in producing an Ant equals that of the profit of an Ant at the optimal solution.

We can repeat the same process and calculations for Cats

cat_res_used<-cbind(rduals1,c(2,2,1,2))
colnames(cat_res_used)<-c("Row Duals", "Resources Used")
cat_res_used <- rbind(cat_res_used,
                      c("TOTAL",t(rduals1)%*%c(2,2,1,2)))
kbl(format(cat_res_used, digits=4), booktabs=T,
    caption="Resources Used by a Cat and their Shadow Prices") |>
  kable_styling(latex_options = "hold_position")
TABLE 4.7: Resources Used by a Cat and their Shadow Prices
Row Duals Resources Used
Machining 0.25 2
Assembly 2.25 2
Testing 0 1
Sensors 0 2
TOTAL 5

In this case, using the columns from Table 4.6 we can calculate the marginal value as \(2\cdot 0.25+2\cdot2.25+1\cdot0+2\cdot0=5\). Since the profit per Cat (from the objective function coefficient on Cats) is also \(\$ 5\), they are in balance and the difference between them is zero which is why the reduced cost for the Cats variable is also zero.

4.3.2 Reduced Price of Bats

The situation is more interesting for Bats. The production plan does not call for producing any Bats. This means that it is sitting right at the lower bound of zero Bats. This hints that perhaps we would like to make even less than zero Bats if we could and that being forced to make even one Bat would be costly. A reduced cost of \(\$-2.5\) means that the opportunity cost of resources used in producing a Bat is \(\$2.5\) more than the profit of producing a single Bat In other words, we would expect that if we force the production of a single Bat, the over all production plan’s profit will go down by \(\$2.5\).

Let’s go ahead and test this interpretation by again looking at the value of the resources consumed in the production of a Bat and the amount of each resource used.

bat_res_used<-cbind(rduals1,c(4,6,2,10))
colnames(bat_res_used)<-c("Row Duals", "Resources Used")
bat_res_used <- rbind(bat_res_used,
                      c("TOTAL",t(rduals1)%*%c(4,6,2,10)))
TABLE 4.8: Resources Used by a Bat and their Shadow Prices
Row Duals Resources Used
Machining 0.25 4
Assembly 2.25 6
Testing 0 2
Sensors 0 10
TOTAL 14.5

Notice that the values based on shadow prices of the resources used by a Bat are \(4\cdot 0.25+6\cdot2.25+2\cdot0+10\cdot0=14.5\). Alas, the profit for each Bat is just \(\$12\) which means that forcing the production of a single Bat will decrease the production plan’s profit by \(\$12-14.5=-2.5\). In other words, the impact on the objective function is \(\$-2.5\) which is the same as the reduced price of Bats.

Now let’s test it. We will modify the formulation to set a lower bound on the number of Bats to be 1. Note that we do this in case by setting the lb option in the add_variable to be 1. Also, if we had a demand constraint for Bats, we could also be accommodated this by setting the upper bound (ub).

Bat1Model <- MIPModel() |>
  add_variable(Ants, type = "continuous", lb = 0) |>
  add_variable(Bats, type = "continuous", lb = 1) |>
  add_variable(Cats, type = "continuous", lb = 0) |>
  
  set_objective(7*Ants + 12*Bats + 5*Cats,"max")  |>

  add_constraint(1*Ants+4*Bats+2*Cats<=800)       |> 
  add_constraint(3*Ants+6*Bats+2*Cats<=900)       |> 
  add_constraint(2*Ants+2*Bats+1*Cats<=480)       |> 
  add_constraint(2*Ants+10*Bats+2*Cats<=1200)     |> 

  solve_model(with_ROI(solver = "glpk"))

  Bat1_case_res <- cbind(objective_value(Bat1Model),
                         get_solution (Bat1Model, Ants),
                         get_solution (Bat1Model, Bats),
                         get_solution (Bat1Model, Cats))

Let’s compare the results for the new production plan and the original base case looking at the Table 4.9.

rownames(base_case_res) <- "Base Case"
rownames(Bat1_case_res) <- "Force one Bat"
temp3 <-rbind(base_case_res,Bat1_case_res)
kbl(temp3, booktabs=T, 
    caption="Impact of a Forced Change in Bats") |>
  kable_styling(latex_options = "hold_position")
TABLE 4.9: Impact of a Forced Change in Bats
Profit Ants Bats Cats
Base Case 2225.0 50 0 375.0
Force one Bat 2222.5 49 1 373.5

As we expected, the forced change of making one additional Bat resulted in a decrease of the overall profit from \(\$2225\) to \(\$2222.5\). This occurred because making one Bat meant that we had fewer of the precious, limited resources, decreasing overall profit due to limiting our possible production of Ants and Cats.

This meant that the number of Ants and Cats were changed resulting in a lower total profit even though a Bat is on its own profitable.

Another way to view the reduced costs for Bats is to think of it as an opportunity cost for not being able to further change the production of Bats due the simple bound. Think of it as a cost for being pinned at the simple upper or lower bound (often 0). A non-zero reduced cost means that the optimizer would like to relax or loosen the lower bound and the value is how much better the objective would be with a unit change of one relaxing the bound. For example, in our case, with bats being pinned at zero at the optimal solution and a reduced cost of bats of -2.5, this means that if we relaxed the non-negativity from \(Bats\geq0\) to instead be \(Bats\geq-1\) and re-solve, then the objective function would improve by 2.5. In other words, relaxing the simple lower bound would change the production plan to enable profit to increase by 2.5.

4.4 Using Sensitivity Analysis to Evaluate a New Product

Let’s consider a design proposal for a Dog drone. The Dog has a projected profit of \(\$20\) each and uses 8 hours of machining time, 12 of assembly, and 4 of testing. Each dog drone also uses 4 sensors.

dog_res_used<-cbind(rduals1,c(8,12,4,4))
colnames(dog_res_used)<-c("Row Duals", "Resources Used")
dog_res_used <- rbind(dog_res_used,
                      c("TOTAL",t(rduals1)%*%c(8,12,4,4)))
TABLE 4.10: Resources Used by a Dog and their Shadow Prices
Row Duals Resources Used
Machining 0.25 8
Assembly 2.25 12
Testing 0 4
Sensors 0 4
TOTAL 29

Even without adding it to the model, we can check to see if it is worthwhile to consider seriously. The opportunity cost of producing one Dog drone would result in the \(\$20-(8\cdot 0.25+12\cdot2.25+4\cdot0+4\cdot0)=-9.0\). In other words, even though a Dog drone has a much higher profit than the other products, producing one would cost the company nine dollars of overall profit in terms of the opportunity cost in lost profit of other production.
We could interpret this as a simple hard stop on the decision to produce Dogs but we could go one step further by setting a target for redesigning the product or its production process. If the assembly time could be reduced by four hours to just eight hours, then the value of the resources consumed would be equal to the profit and we would be indifferent to producing some Dog drones.

4.5 Exercises

Exercise 4.1 (Adding Eels) Your company has extended production to allow for producing aquatic Eels and is now including a finishing department that primes and paints the drones.

Adding Eels
Characteristic Ants Bats Cats Eels Available
Profit $7 $12 $5 $22
Machining 1 4 2 4 800
Assembly 3 6 2 8 900
Testing 2 2 1 25 480
Sensor 2 10 2 16 1200
Painting 1 1 1 12 500
  1. Use R Markdown to create your own description of the model.
  2. Extend the R Markdown to show your LP Model. Be sure to define models.
  3. Solve the model in R.
  4. Interpret and discuss the model in R Markdown.
  5. Examine and reflect upon the reduced costs and shadow prices from the context of which products to produce and not produce.
  6. Using the results from e), (i.e. reduced cost and shadow prices) make one change to the base model’s objective function that will change the production plan. Rerun and discuss the new results.
  7. Using the results from e), (i.e. reduced cost and shadow prices) make one change to the base model’s resource usage values that will change the production plan. Rerun and discuss the new results.
  8. Using the results from e), (i.e. reduced cost and shadow prices) make one change to the base model’s available resource values that will change the production plan. Rerun and discuss the new results.
  9. Combine the results of the base case e), as well as the variations f) through h) into a single table and discuss the results.

Exercise 4.2 (Revisiting Transportation) Using sensitivity analysis, revisit the transportation exercise from chapter 3.

  1. If one more unit of supply was available, where would it be prioritized and why?
  2. If demand could be increased by one unit, would it affect the result and at which destination node it be preferred and why?

Exercise 4.3 (Identifying Hidden Costs) In a TcDonald’s restaurant, given below is the data for staff time in minutes to perform different steps in order to make Burgers, Coffee, Ice cream and Fries. These steps include: Order receiving, Processing, Preparing, Packaging, and Delivery.

Time (Mins.) Burger Coffee Ice cream Fries Available Minutes
Profit $2 $2 $3 $1
Order receiving 2 2 5 1 1200
Processing 2 1 1 1 1500
Preparing 12 6 2 10 3000
Packaging 3 2 4 2 1800
Delivery 1 3 1 1 1200
  1. Use R Markdown to create your own description of the model.
  2. Extend the R Markdown to show your LP Model. Be sure to define models.
  3. Solve the model in R (To find maximum profit).
  4. Interpret and discuss the model in R Markdown.
  5. Examine and reflect upon the reduced costs and shadow prices. Discuss which step’s available hours need to increases/decrease to make the most profit.