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.
You can load this notebook in Pluto from here: https://github.com/frankhuettner/NewsvendorModel.jl/blob/main/docs/src/textbook/cachon%2Bterwiesch.jl
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:
How many units to order prior to the season starts.
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
Gerard Cachon, Christian Terwiesch, Matching Supply with Demand: An Introduction to Operations Management. McGraw-Hill (2012), Chapters 12 and 13.
Gerard Cachon, Christian Terwiesch, Matching Supply with Demand: An Introduction to Operations Management. McGraw-Hill (2018), Chapters 14 and 15.