Price breaks or volume discounts, represent situations where the margin unit price of goods is varying depending on the quantity considered for the purchase. Usually, unit prices are decreasing when purchase quantities increase in order to give the client an incentive to buy more. When suppliers offer price breaks, there is an economic incentive in having purchase quantities correspondingly adjusted to take advantage of those price breaks. Envision offers extensive support for supplier price breaks. In this section, we detail how to model price breaks from the purchasing optimization perspective.
Table representation of price breaks
The most frequent way to represent price breaks for a list of products is to have a table with 3 columns:
Id the identifier of the product
MinQuantity the minimal quantity that needs to be reached to be eligible to the unit price
Price the purchase unit price of the product
In order to keep the table readable, when looking at a given product, the lines are typically sorted by quantities in increasing order. This ordering helps making sense of the magnitude of the discounts.
This representation is prone to negative marginal unit prices
where buying one more unit can result in a lower total price.
Let's consider the product A sold at 1€ per unit. The product A has a price break at 50 units where the price drops to 0.9€ per unit. Purchasing 46 units cost 46€ while purchasing 50 units cost only 45€. Thus, there is no economic incentive to buy any quantities within the range 46-49 units, as it is cheaper to buy 50 units instead. The marginal unit price of the 50th unit is -4€.
Those negative marginal prices are the consequence of the underlying data model adopted for the price breaks. More elaborate price break models - capable of eliminating those negative marginal prices - exist, however those models are beyond the scope of the present section.
In the following, we assume that the price break data can be made available through a table as detailed above.
The purpose of the
function in Envision is to transform a tabular price break data into a distribution that represents the marginal purchase cost of units. The syntax is:
expect Prices[Id, *] // 'Prices' is the price break table
B = pricebrk(Demand, Price, Prices.MinQuantity, Prices.Price, Stock, StockPrice)
The function returns the distribution of the marginal purchase unit price, that is, the price to be paid to purchase the kth
unit. This function is a bit complex. We detail and justify this complexity in the following.
The arguments are:
Demand is a distribution used to pick a support - in the mathematical sense - for the distribution to be returned. The values of this distribution are not used, but the returned distribution is adjusted to be least as precise as
Price is a number, interpreted as default unit price of the product. This value is used if there is no price break for a minimum quantity of 1; either because the price break starts at a value greater than 1, or because there is no price break at all for the product.
Prices.MinQuantity is the smallest quantity for which the price break line applies. It must be an integer greater or equal to 1. Duplicates are not allowed.
Prices.Price is the unit price for this price break line. It applies to all purchased units, not just the ones exceeding
Prices.MinQuantity. It must be a decreasing function of
Stock is the available stock for the product. Non-zero values mean that fewer units need to be purchased to reach a given stock level, which means that price breaks are harder to reach. It must be an non-negative integer. This argument is optional. When this argument is omitted,
StockPrice should be omitted as well. When this argument is omitted the default value is zero.
StockPrice is the unit price for units already in stock. This argument is optional. When it is omitted, the default value is zero.
argument is merely a syntactic sugar intended to deal with situations where the price break table only covers the products that actually benefit from a price break. Through this argument, we avoid the need to extend the table
to have at least one line per product.
Resolution of distributions
A distribution is required as the first argument of
because distributions in Envision are not arbitrarily precise. Indeed, there are practical limits to the resolution of a distribution within Envision. Yet, the price breaks obtained from a given supplier can range from a 1 unit purchase to a (theoretical) 10 million unit purchase. The
distribution is used by the
function in order to adjust the resolution of the returned distribution to the range of interest.
Indeed, the one pitfall that Envision prevents is an inventory optimization logic that end's up suggesting to purchase 999 units while the target price break is at 1000 units. Such a situation could happen if the distribution generated by Envision does not internally differentiate the values at 999 units and 1000 units. By passing a distribution to the
function, Envision ensures that this specific scenario is avoided by adapting the resolution of the returned distribution.
Ordering space vs. Stocking space
The price break table is organized from an ordering
perspective, associated a unit purchase price to every purchased quantities. However, the inventory optimization perspective is organized from a stocking
perspective: when we consider adding +1 unit of stock, we take into account the stock already available, that is, the stocking space
function translates the price break representation from the original ordering space toward the stocking space.
This translation is the reason why
takes two arguments associated to the stock: the stock level and the stock unit price. Those two arguments are used to shift the marginal price distribution to the right by Stock
units. The shift could be done with the regular shift operator
on distributions, but once again, this could trigger situations where minor approximations collide with price break thresholds. The
function internalizes this shift in order to eliminate those approximations.
Marginal cost of units
The distribution returned by
represent the marginal unit purchase cost. The segment [1;Stock] is associated to current stocks and associated to
. Then, if
is the distribution returned by
, then the integral
int(B, Stock + 1, Stock + N)
is the total cost of purchasing N
units beyond the units that are already in stock.
As pointed out above, price break tables are frequently associated with negative marginal unit costs - i.e. situations where purchasing one more unit comes with a negative cost. The distributions returned by
reflect those situations through local negative values. Those negative values are consistent and the direct consequence of the price break data model.
Combining price breaks and stock rewards
The stock reward function computes the distribution of marginal economic returns for every extra unit of inventory held in stock. In a previous section
, we have seen how this function can be associated to economic variables that represent the gross margin, the stock out penalty and the carrying cost. In our previous discussion, those economic variables were plain numbers. However, when price breaks are involved, those economic variables are varying too, along with the quantity being ordered. Those variations are straightforward to model through distributions.
B = pricebrk(Demand, BuyPrice, Prices.MinQuantity, Prices.Price, Stock, StockPrice)
M = SellPrice - B // 'M' is a distribution
S = -0.5 * (SellPrice - B) // 'S' is a distribution
C = -0.3 * B * mean(Leadtime) / 365 // 'C' is a distribution
AM = 0.3
AC = 1 - 0.2 * mean(Leadtime) / 365
RM = stockrwd.m(Demand, AM) * M // point-wise multiplication
RS = stockrwd.s(Demand) * S // idem
RC = stockrwd.c(Demand, AC) * C // idem
R = RM + RS + RC
The primary difference between this block of code and the original one, when we introduced the stock reward function, is
. The column
is turned into a price break distribution
at line 1 through the function
. Then, the rest of script boils down to a directly application of the algebra of distribution which is doing all the complicated work for us.
At lines 3-5, the economic variables are turned into distributions. Indeed, when a constant is added to a distribution, the result of the addition is a distribution. The same goes for subtraction. Above
is the margin reward per unit
(aka the marginal margin), and as the price breaks are typically offering a lower unit price as quantity increases, the distribution
is expected to be increasing.
At lines 9-11, the components of the stock reward functions are associated with the economic distributions
(rather than numbers), but the syntax remains identical. Under the hood, it's point-wise multiplications that take place between distributions. Finally, at line 12, the final stock reward is composed just as we did before.