News Vendor Problem

Imagine a newspaper vendor who buys copies wholesale every morning and sells them through the day. Demand is uncertain, as some days are busy and some quiet. If the vendor buys too few copies, they miss out on sales (and on the profit those sales would have brought in). If they buy too many, the leftover papers are wasted at the end of the day, since yesterday's news doesn't sell tomorrow. How many copies should the vendor buy? That is the newsvendor problem, finding the order quantity that, on average, gives the highest profit when you weigh the cost of running out against the cost of leftovers. The same balance shows up for any perishable product with unpredictable demand, such as fresh produce, baked goods, daily prepared meals, magazines and flowers.

Problem Definition

The newsvendor orders Q units every day. Each unit costs c, and the sell price is p. The newsvendor faces normally distributed demand with mean μ and standard deviation σ. If, on a certain day, the actual demand is k, then the net profit after selling k units is

Profit = Revenue − Order Cost = k × p − Q × c

However, any units left unsold at the end of the day are wasted, since they cannot be sold the next day. The newsvendor can sell these unsold units back to the supplier at a lower price, called the salvage value s. If the actual demand that day is less than the order size Q, then the net profit is

Profit = Revenue + Salvage − Order Cost = k × p + (Q − k) × s − Q × c

When the actual demand k is more than the newsvendor's order quantity Q, the profit is

Profit = Q × p − Q × c     (In this case, all available units are sold, and the profit is the revenue from selling Q units minus the total ordering cost.)

So, the newsvendor problem is defined by the following parameters.

  • Ordering cost (c). Cost per unit ordered.
  • Sell price (p). Price per unit sold.
  • Salvage value (s). Value per unsold unit at the end of the period.
  • Mean demand (μ). Average demand during the period.
  • Standard deviation of demand (σ). How much the demand varies.
  • Number of samples (n). Number of demand samples for the simulation.
  • Order quantity (Q). Units ordered for the period.

The objective is to find the order quantity Q* that maximizes the expected profit.

Analytical Solution

The optimal order quantity is

Q* = μ + σ · Φ−1 (Cu / (Cu + Co))

where

  • Cu = p − c is the underage cost (profit lost per unit of demand that could not be met),
  • Co = c − s is the overage cost (cost per unsold unit),
  • Φ−1 is the inverse of the standard normal distribution, and
  • Cu/(Cu + Co) is the critical ratio (the share of demand to satisfy).

Example Parameters

The parameters in this example are
c = 2
p = 5
s = 1
μ = 100
σ = 15
n = 1000
Q = 100 (example order quantity)

Putting in the values
Cu = 5 − 2 = 3
Co = 2 − 1 = 1
Critical ratio = 3/(3 + 1) = 0.75
Q = 100 + 15 · Φ−1(0.75)
Q
≈ 100 + 15 · 0.6745 = 110.12
So, the optimal order quantity is about 110 units.

Modeling and Simulating the Newsvendor Problem

It is a simple supply chain with three nodes, an infinite supplier, the newsvendor (a retailer), and a customer demand at the newsvendor with normally distributed order sizes. One time unit is read as one day.

Newsvendor

Simulation Setup

  • An infinite_supplier gives the newsvendor an unlimited supply.
  • The simulation focuses on the newsvendor, newsvendor1, which
    • keeps a perishable inventory with a shelf life of 1 day, and
    • uses a periodic replenishment policy, placing a replenishment order every day.
  • The link between the supplier and the newsvendor has a lead time of 0, so each day's order arrives at once.
  • The customer demand places one customer order a day, with an order size drawn from a normal distribution by the normal_quantity() function.
  • With consume_available set to True, a customer whose order cannot be filled in full takes the units that are there. This is needed because the simulation represents the whole day's demand as a single customer order, while in reality many customers each buy a newspaper.
  • After the run, the profit is worked out from the units sold, the expired units (sold back at the salvage value) and the ordering cost.
import simpy
import random
import numpy as np
import matplotlib.pyplot as plt
import SupplyNetPy.Components as scm

random.seed(42) # fixed seed for repeatable results
mean = 100
std_dev = 15
num_samples = 1000
def normal_quantity():
    """Generates a random order quantity based on a normal distribution."""
    sample = random.gauss(mean, std_dev)
    while(sample<0):
        sample = random.gauss(mean, std_dev)
    return int(sample)

cost = 2 # newspaper buy price
sell_price = 5 # newspaper sell price
salvage = 1 # newspaper salvage price
order_quantity = 100 # initial order quantity
cost_arr = [] # to store cost for each order quantity
profit_arr = [] # to store profit for each order quantity

for order_quantity in range(10, 200, 10):

    env = simpy.Environment()

    # create an infinite supplier
    supplier1 = scm.Supplier(env=env, ID="S1", name="Supplier1", node_type="infinite_supplier")

    #create the distributor
    newsvendor1 = scm.InventoryNode(env=env, ID="D1", name="News Vendor", node_type="distributor",
                                    capacity=float('inf'), initial_level=order_quantity, 
                                    inventory_holding_cost=0.1, inventory_type="perishable", 
                                    shelf_life=1, replenishment_policy=scm.PeriodicReplenishment,
                                    policy_param={"T": 1, "Q": order_quantity}, 
                                    product_sell_price=sell_price, product_buy_price=cost)

    link1 = scm.Link(env=env,ID="l1", source=supplier1, sink=newsvendor1, cost=10, lead_time=lambda: 0)

    demand1 = scm.Demand(env=env,ID="d1", name="demand_d1",
                        order_arrival_model=lambda: 1, consume_available=True,
                        order_quantity_model=normal_quantity, demand_node=newsvendor1)

    scm.global_logger.disable_logging()
    env.run(until=num_samples)

    # Calculate the cost and profit
    # total demand fulfilled by newsvendor is available in variable demand_fulfilled
    # list element 0 is number of orders, element 1 is total quantity
    daily_sales = newsvendor1.stats.demand_fulfilled[1] 
    wasted_inventory = newsvendor1.inventory.waste
    everyday_profit = (daily_sales*sell_price + wasted_inventory*salvage - order_quantity*num_samples*cost)/num_samples
    profit_arr.append([order_quantity,everyday_profit])

profit_arr = np.array(profit_arr) # convert to numpy array for easier manipulation
Q = np.argmax(profit_arr[:,1]) # index of maximum profit
plt.plot(profit_arr[:,0], profit_arr[:,1], marker='.', linestyle='-', color='b')
plt.plot(profit_arr[Q,0], profit_arr[Q,1], marker='o', linestyle='-', color='r', label=f'Q={profit_arr[Q,0]}')
plt.xlabel('Order Quantity')
plt.ylabel('Profit')
plt.title('Profit vs Order Quantity')
plt.legend()
plt.grid()
plt.show()

The following plot shows how the profit changes with the order quantity (Q). The highest profit is reached when Q is about 110, which confirms the analytical solution for the optimal order quantity.

Profit for each order quantity Q, highest at about Q = 110.

Takeaway

This example showed how to solve the newsvendor problem by simulation with the SupplyNetPy library. By simulating different order quantities and comparing the resulting profits, we can see and confirm the order quantity that maximizes the expected profit. This approach can be extended to more complex inventory scenarios and demand distributions, giving useful insights for inventory decisions.