Supplier price breaks

Price breaks (volume discounts) make the unit price depend on the quantity ordered. This creates incentives that are not visible in a flat unit price: the total cost can jump or even drop at a breakpoint, depending on how the supplier applies the discount. Envision models those incentives explicitly with zedfuncs so the economics can be reasoned about, not guessed.

Table of contents

Merchant vs fiscal price breaks

Two interpretations exist:

The difference shows up in the marginal price curve, which is exactly what pricebrk.m() and pricebrk.f() return.

table Items[id] = with
  [| as id, as StartPrice |]
  [| "spoon", 10 |]

table Breaks = with
  [| as Id, as Quantity, as Price |]
  [| "spoon", 5, 9 |]
  [| "spoon", 10, 8 |]

expect Breaks.id = Breaks.Id

Items.ZM = pricebrk.m(Items.StartPrice, Breaks.Quantity, Breaks.Price)
Items.ZF = pricebrk.f(Items.StartPrice, Breaks.Quantity, Breaks.Price)

table Q = extend.range(12 into Items)
Q.UnitM = valueAt(Items.ZM, Q.N)
Q.UnitF = valueAt(Items.ZF, Q.N)
Q.TotalM = int(Items.ZM, 1, Q.N)
Q.TotalF = int(Items.ZF, 1, Q.N)

show table "Breakpoints compared" with
  Q.N as "Qty"
  Q.UnitM
  Q.UnitF
  Q.TotalM
  Q.TotalF

Example output:

Qty UnitM UnitF TotalM TotalF
1 10 10 10 10
2 10 10 20 20
3 10 10 30 30
4 10 10 40 40
5 5 9 45 49
6 9 9 54 58
7 9 9 63 67
8 9 9 72 76
9 9 9 81 85
10 -1 8 80 93
11 8 8 88 101
12 8 8 96 109

The merchant curve shows negative or very low marginal prices at the breakpoint: those units compensate for the retroactive discount on earlier units. This is why buying 10 units can be cheaper than buying 9.

Ordering space vs stocking space

Price breaks are defined in the ordering space: the index is “units ordered in this purchase.” Inventory optimization is often in stocking space: the index is “total stock position after the purchase.” Shifting the zedfunc aligns the two.

table Items[id] = with
  [| as id, as StartPrice, as StockOnHand |]
  [| "spoon", 10, 6 |]

table Breaks = with
  [| as Id, as Quantity, as Price |]
  [| "spoon", 5, 9 |]
  [| "spoon", 10, 8 |]

expect Breaks.id = Breaks.Id

Items.ZM = pricebrk.m(Items.StartPrice, Breaks.Quantity, Breaks.Price)
Items.ZX = shift(Items.ZM, Items.StockOnHand)

table Pos = extend.range(12 into Items)
Pos.OrderingMarginal = valueAt(Items.ZM, Pos.N)
Pos.StockingMarginal = valueAt(Items.ZX, Pos.N)

show table "Ordering vs stocking" with
  Pos.N as "StockPos"
  Pos.OrderingMarginal
  Pos.StockingMarginal

Example output:

StockPos OrderingMarginal StockingMarginal
1 10 0
2 10 0
3 10 0
4 10 0
5 5 0
6 9 0
7 9 10
8 9 10
9 9 10
10 -1 10
11 8 5
12 8 9

The stocking curve is zero for already-held units, then follows the ordering curve once new units start to be purchased. This makes price breaks compatible with reward functions that operate on stock positions.

Implications for economic optimization

Price breaks can invert the usual “one more unit costs more” intuition. The merchant curve can make the marginal cost of a breakpoint unit negative, while the fiscal curve keeps marginal costs non-negative. This is why price breaks should be modeled as zedfuncs and composed with reward or cost curves rather than flattened into a single unit price.

User Contributed Notes
0 notes + add a note