This notebook illustrates the usage of NewsvendorModel.jl with examples from the excellent textbook by Cachon & Terwiesch (3rd or 4th edition).

These notes are not self-contained but should be considered as a companion to the above text book.

ONeill Hammer 3/2

The leading case of the book is about the O'Neill Hammer 3/2, with the following information (for a detailed treatment, it is referred to the sources below).

Unit values:

  • cost per unit ordered = 110

  • selling price per unit = 190

  • salvage value = 190

Demand distribution:

  • expected demand = 3192

  • standard deviation of demand = 1181

# Load the packages needed
using NewsvendorModel, Distributions
# Define the model
oneill = NVModel(cost = 110, 
                 price = 190, 
                 salvage = 90, 
                 demand = Normal(3192, 1181)
         )
Data of the Newsvendor Model
 * Demand distribution: Normal{Float64}(μ=3192.0, σ=1181.0)
 * Unit cost: 110.00
 * Unit selling price: 190.00
 * Unit salvage value: 90.00
# Solve the model
solve(oneill)
=====================================
Results of maximizing expected profit
 * Optimal quantity: 4186 units
 * Expected profit: 222296.50
=====================================
This is a consequence of
 * Cost of underage:  80.00
   ╚ + Price:               190.00
   ╚ - Cost:                110.00
 * Cost of overage:   20.00
   ╚ + Cost:                110.00
   ╚ - Salvage value:       90.00
 * Critical fractile: 0.80
 * Rounded to closest integer: true
-------------------------------------
Ordering the optimal quantity yields
 * Expected sales: 3060.16 units
 * Expected lost sales: 131.84 units
 * Expected leftover: 1125.84 units
 * Expected salvage revenue: 101325.15
-------------------------------------

Note that the result is slightly different from the textbook, which suggests the order quantity 4,184 (instead of 4,186). This is due to rounding errors in the textbook.

2-stage calculation

The authors also consider the scenario in which it is possible to make a second order later during the selling period (what follows is very brief and I recommend looking up the book). It is assumed that

  • the unit cost for the second order are 20% higher ⇒ 20% * 110 = 22. Yet,

  • demand for the rest of the season is assumed to be certain at that stage;

  • the reorder stage is early enough to ensure that we have enough units until we receive our second delivery

Now there are two decision points:

  1. How many units to order prior to the season starts.

  2. How many units to order at the second stage.

To find the optimal order quantity for (1), we think of the product as follows:

  • Having a unit leftover is effectively loosing Cₒ = 20

  • Having a unit too little is creating not such a heavy damage anymore; instead, we can save the day be ordering remaining demand at the second stage, albeit at a worse price ⇒ we miss out Cᵤ = 22

This is as if we had a product traded for zero cost and zero price, but having a unit left over costs 20 ⇒ salvage = -20 ⇒ Cₒ = 20. Being short a unit costs a backorder penalty of 22 ⇒ Cᵤ = 22.

oneill_1st_order_calculation = NVModel( demand = Normal(3192, 1181),
                                        cost = 0, 
                                        price = 0, 
                                        salvage = -20, 
                                        backorder = 22,
                                        )
Data of the Newsvendor Model
 * Demand distribution: Normal{Float64}(μ=3192.0, σ=1181.0)
 * Unit cost: 0.00
 * Unit selling price: 0.00
 * Unit salvage value: -20.00
 * Unit backorder penalty: 22.00

This yields the following critical fractile for the 1st stage:

critical_fractile(oneill_1st_order_calculation)
0.5238095238095238

This yields the following optimal order quantity for the 1st stage:

q_opt(oneill_1st_order_calculation)
3263

Ordering 3263 units results in the following expected lost sales, which is the expected order quantity for the second order:

lost_sales(oneill, 3263)
436.502002733936

Ordering 3263 further yields the following expected profit (based on original unit values):

profit(oneill, 3263)
210289.7997266064

In total, this promises the following expected profit:

profit(oneill, 3263) + lost_sales(oneill, 3263) * (190 - 132)
235606.9158851747

Selected Exercises

The book offers exercises with solutions. Here is a small selection that illustrates the convinience of Distributions.jl and NewsvendorModel.jl (everything is very brief and a detailed explanation can be found in the book).

McClure Books

# Define demand 
mcclure_demand = Normal(200, 80)
Normal{Float64}(μ=200.0, σ=80.0)
# Define model
mcclure_nvm = NVModel(cost = 12, price = 20, salvage = 12 - 4,demand = mcclure_demand)
Data of the Newsvendor Model
 * Demand distribution: Normal{Float64}(μ=200.0, σ=80.0)
 * Unit cost: 12.00
 * Unit selling price: 20.00
 * Unit salvage value: 8.00
a) Pr[demand > 400]
1 - cdf(mcclure_demand, 400)
0.006209665325776159
b) Pr[demand < 100]
cdf(mcclure_demand, 100)
0.10564977366685525
c) Pr[160 < demand < 240]
cdf(mcclure_demand, 240) - cdf(mcclure_demand, 160)
0.38292492254802624
d) Pr[160 < demand < 240]
q_opt(mcclure_nvm)
234
e) Quantity such that Pr[demand ≤ q] = 95%
quantile(mcclure_demand, 0.95)
331.58829015611775
f)
1 - 0.95
0.050000000000000044
g) Profit if q = 300
profit(mcclure_nvm, 300)
1151.4366064267651

EcoTable Tea

# Define demand 
ecotable_demand = Poisson(4.5)
Poisson{Float64}(λ=4.5)
# Define model
ecotable_nvm = NVModel(cost = 32, price = 55, salvage = 20, demand = ecotable_demand)
Data of the Newsvendor Model
 * Demand distribution: Poisson{Float64}(λ=4.5)
 * Unit cost: 32.00
 * Unit selling price: 55.00
 * Unit salvage value: 20.00
a) Pr[demand > 3]
1 - cdf(ecotable_demand, 3)
0.657704044165409
b) Pr[demand < 7]
cdf(ecotable_demand, 7)
0.9134135283526439
c) Optimal order quantity
q_opt(ecotable_nvm)
5
d) Expected sales at q = 4
sales(ecotable_nvm, 4)
3.411917495756798
d) Expected leftover at q = 6
leftover(ecotable_nvm, 6)
1.8231165154787454
f) Smallest quantity q such that Pr[demand ≤ q] ≥ 90%
quantile(ecotable_demand, 0.90)
7
d) Expected profit at q = 8
profit(ecotable_nvm, 8)
59.13467821051198

Pony Express

First we define the demand:

xs = vec([5_000	10_000	15_000	20_000	25_000	30_000	35_000	40_000	45_000	50_000	55_000	60_000	65_000	70_000	75_000])
15-element Vector{Int64}:
  5000
 10000
 15000
 20000
 25000
 30000
 35000
     ⋮
 50000
 55000
 60000
 65000
 70000
 75000
ps = vec([0.0181	0.0733	0.1467	0.1954	0.1954	0.1563	0.1042	0.0595	0.0298	0.0132	0.0053	0.0019	0.0006	0.0002	0.0001])
15-element Vector{Float64}:
 0.0181
 0.0733
 0.1467
 0.1954
 0.1954
 0.1563
 0.1042
 ⋮
 0.0132
 0.0053
 0.0019
 0.0006
 0.0002
 0.0001
# The distribution is nonparametric
elvis_demand = DiscreteNonParametric(xs,ps)
DiscreteNonParametric{Int64, Float64, Vector{Int64}, Vector{Float64}}(
support: [5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000, 55000, 60000, 65000, 70000, 75000]
p: [0.0181, 0.0733, 0.1467, 0.1954, 0.1954, 0.1563, 0.1042, 0.0595, 0.0298, 0.0132, 0.0053, 0.0019, 0.0006, 0.0002, 0.0001]
)
# Define model
elvis_nvm  = NVModel(cost = 6, price = 12, demand = elvis_demand, salvage = 2.5)
Data of the Newsvendor Model
 * Demand distribution: DiscreteNonParametric{Int64, Float64, Vector{Int64}, Vector{Float64}}(
support: [5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000, 55000, 60000, 65000, 70000, 75000]
p: [0.0181, 0.0733, 0.1467, 0.1954, 0.1954, 0.1563, 0.1042, 0.0595, 0.0298, 0.0132, 0.0053, 0.0019, 0.0006, 0.0002, 0.0001]
)

 * Unit cost: 6.00
 * Unit selling price: 12.00
 * Unit salvage value: 2.50
a)
cdf(elvis_demand, 30_000)
0.7852
b)
q_opt(elvis_nvm)
30000
c)
quantile(elvis_demand, 0.9)
40000
d)
leftover(elvis_nvm, 50_000)
25061.0
e)
quantile(elvis_demand, 1.0) 
75000
profit(elvis_nvm, 75_000) 
-25000.0

The answer in the book appears to be confusing: IMHO lost sales = 0 if 75,000 wigs are ordered.

Flextrola

We just investigate part (j): Variation with log normal demand

# Parameter σ² of log normal distribution from mean= 1000 and standard deviation = 600
σ² = log(1 + 600^2 / 1000^2)
0.30748469974796055
# Parameter μ of log normal distribution from mean = 1000 and standard deviation = 600
μ = log(1000^2 / √(600^2 + 1000^2))
6.754012929108157
flextrola_demand = LogNormal(μ, √σ²)
LogNormal{Float64}(μ=6.754012929108157, σ=0.5545130293761911)
# Indeed, the parameters yield the desired mean
mean(flextrola_demand)
999.9999999999998
# Indeed, the parameters yield the standard deviation
std(flextrola_demand)
599.9999999999998
# Let us plot this as in the textbook
begin
using StatsPlots
plot(xlims=(0, 2500), xlabel = "Demand", ylabel = "Probability")
plot!(Normal(1000, 600), marker = :dot, label="Normal(Normal(1000, 600))")
plot!(flextrola_demand, marker = :hex, label="LogNormal{Float64}(μ=6.86, σ=0.307)")
end
# Define the model
flextrola_nvm = NVModel(cost = 72, price = 121, salvage = 50, demand=flextrola_demand)
Data of the Newsvendor Model
 * Demand distribution: LogNormal{Float64}(μ=6.754012929108157, σ=0.5545130293761911)
 * Unit cost: 72.00
 * Unit selling price: 121.00
 * Unit salvage value: 50.00
solve(flextrola_nvm)
=====================================
Results of maximizing expected profit
 * Optimal quantity: 1129 units
 * Expected profit: 33850.63
=====================================
This is a consequence of
 * Cost of underage:  49.00
   ╚ + Price:               121.00
   ╚ - Cost:                72.00
 * Cost of overage:   22.00
   ╚ + Cost:                72.00
   ╚ - Salvage value:       50.00
 * Critical fractile: 0.69
 * Rounded to closest integer: true
-------------------------------------
Ordering the optimal quantity yields
 * Expected sales: 826.60 units
 * Expected lost sales: 173.40 units
 * Expected leftover: 302.40 units
 * Expected salvage revenue: 15119.98
-------------------------------------

References