from BPTK_Py import Model
from BPTK_Py import sd_functions as sd
Competitive Pricing Dynamics
Competitive Pricing Model
We create our model using the Model
class as follows:
= Model(starttime=0.0,stoptime=2.0,dt=0.25,name='CompetitvePricing') model
The stock and flow model consists of six parts:
- Production and inventory
- Demand formation
- Price adjustment
- Profit
- Perceived Inventory
- Expected Profitability
The following sections explain each part in more detail. Furthermore, they demonstrate the composition of stocks, flows, converters and constants.
Creating model
1) Production and Inventory
Stocks: production capacity, production, inventory
= model.stock("productionCapacity")
productionCapacity = model.stock("production")
production = model.stock("inventory") inventory
Flows: production start rate, production rate, consumption rate
= model.flow("productionStartRate")
productionStartRate = model.flow("productionRate")
productionRate = model.flow("consumptionRate") consumptionRate
Converters: effect of profitability on capacity utilization,capacity utilization, inventory coverage
= model.converter("capacityUtilization")
capacityUtilization = model.converter("effectOfProfitabilityOnCapacityUtilzation")
effectOfProfitabilityOnCapacityUtilzation = model.converter("inventoryCoverage") inventoryCoverage
Constants: production time
Normalized expected profitability and demand are not constants. They are results of other parts.
= model.constant("productionTime") productionTime
2) Demand formation
This model does not have any stocks or flows.
Converters: relative value of product, effect of relative value on demand, reference demand, demand
= model.converter("relativeValueOfProduct")
relativeValueOfProduct = model.converter("effectOfRelativeValueOnDemand")
effectOfRelativeValueOnDemand = model.converter("referenceDemand")
referenceDemand = model.converter("demand") demand
Constants: price of substitutes, size of shock, market shock on
Price is not a constant. It is a result of another part.
= model.constant("sizeOfShock")
sizeOfShock = model.constant("marketShockOn")
marketShockOn = model.constant("priceOfSubstitutes") priceOfSubstitutes
3) Price Adjustment
This section contains two models. The first model determines the minimum price. The second describes the formation of the product’s price.
This model does not have any stocks, flows or constants. Unit variable cost and unit capacity cost are results of other parts.
Converters: capacity cost per unit, minimum price
= model.converter("minimumPrice")
minimumPrice = model.converter("capacityCostPerUnit") capacityCostPerUnit
Stocks: expected price
= model.stock("expectedPrice") expectedPrice
Flows: change in expected price
= model.flow("changeInExpectedPrice") changeInExpectedPrice
Converters: indicated price, effect of inventory coverage on price, price
= model.converter("indicatedPrice")
indicatedPrice = model.converter("effectOfInventoryCoverageOnPrice")
effectOfInventoryCoverageOnPrice = model.converter("price") price
Constants:
Minimum price and normalized inventory coverage are not constants. Minimum price is a result of the previous model and normalized inventory coverage is a result of another part.
= model.constant("priceAdjustmentTime") priceAdjustmentTime
4) Profit
This model has no stocks and flows. Production capacity is not a stock of this part. It is a result of the “Production and Inventory” part from the beginning.
Converters: capacity cost, variable cost, cost, revenue, profit
= model.converter("capacityCost")
capacityCost = model.converter("variableCost")
variableCost = model.converter("cost")
cost = model.converter("revenue")
revenue = model.converter("profit") profit
Constants: unit capacity cost, unit variable cost
Production rate, price and consumption rate are not constants of this model. They are results of other parts. Production rate and consumption rate are converters of the “Production and Inventory” model. Price is a converter of the “Price Adjustment” part.
= model.constant("unitCapacityCost")
unitCapacityCost = model.constant("unitVariableCost") unitVariableCost
5) Perceived Inventory
Stocks: perceived inventory coverage
= model.stock("perceivedInventoryCoverage") perceivedInventoryCoverage
Flows: change in perceived inventory coverage
= model.flow("changeInPerceivedInventoryCoverage") changeInPerceivedInventoryCoverage
Converters: normalized perceived inventory coverage
= model.converter("normalizedPerceivedInventoryCoverage") normalizedPerceivedInventoryCoverage
Constants: inventory coverage perception time, reference inventory coverage
Inventory coverage is not a constant of this part. It is a converter and a result of the “Production and Inventory” model.
= model.constant("referenceInventoryCoverage")
referenceInventoryCoverage = model.constant("inventoryCoveragePerceptionTime") inventoryCoveragePerceptionTime
6) Expected Profitability
Stocks: expected profitability
= model.stock("expectedProfitability") expectedProfitability
Flows: change in expected profitability
= model.flow("changeInExpectedProfitability") changeInExpectedProfitability
Converters: normalized expected profitability
= model.converter("normalizedExpectedProfitability") normalizedExpectedProfitability
Constants: profit adjustment time, reference expected profitability
Profit is not a constant of this part. It is a result of the “Profit” model.
= model.constant("referenceExpectedProfitability")
referenceExpectedProfitability = model.constant("profitAdjustmentTime") profitAdjustmentTime
Initializing the stocks
= 200.0
productionCapacity.initial_value = 300.0
production.initial_value = 300.0
inventory.initial_value = 3.0
expectedPrice.initial_value = 3.0
perceivedInventoryCoverage.initial_value = 100.0 expectedProfitability.initial_value
Defining Equations
Production and Inventory
= 3.0 productionTime.equation
= sd.lookup(normalizedExpectedProfitability,"effectOfProfitabilityOnCapacityUtilzation")
effectOfProfitabilityOnCapacityUtilzation.equation = effectOfProfitabilityOnCapacityUtilzation
capacityUtilization.equation = capacityUtilization*productionCapacity
productionStartRate.equation = sd.min(production, sd.delay(model, productionStartRate, productionTime, 100.0))
productionRate.equation = sd.min(inventory,demand)
consumptionRate.equation = inventory/consumptionRate inventoryCoverage.equation
The stocks production and inventory have inflows and outflows.
= productionStartRate - productionRate
production.equation = productionRate - consumptionRate inventory.equation
We define the effect of profitability on capacity utilzation in our model using a non-linear relationship (depending on the normalized expected profitability). We capture this relationship in a lookup table that we store in the points
property of the model (using a Python list):
"effectOfProfitabilityOnCapacityUtilzation"] = [
model.points[0.0,0.324],
[0.167,0.33],
[0.333,0.372],
[0.5,0.394],
[0.667,0.41],
[0.833,0.42],
[1.0,0.5],
[1.167,0.745],
[1.333,0.80075],
[1.5,0.8565],
[1.667,0.91225],
[1.833,0.968],
[2.0,0.968]
[ ]
"effectOfProfitabilityOnCapacityUtilzation") model.plot_lookup(
Demand Formation
= 50.0
sizeOfShock.equation = 0.0
marketShockOn.equation = 3.0 priceOfSubstitutes.equation
= priceOfSubstitutes/price
relativeValueOfProduct.equation = sd.lookup(relativeValueOfProduct,"effectOfRelativeValueOnDemand")
effectOfRelativeValueOnDemand.equation = 100+marketShockOn*sd.step(sizeOfShock,10.0)
referenceDemand.equation = effectOfRelativeValueOnDemand*referenceDemand demand.equation
"effectOfRelativeValueOnDemand"] = [
model.points[0.0,0.17],
[0.167,0.191],
[0.333,0.213],
[0.5,0.277],
[0.667,0.351],
[0.833,0.479],
[1.0,1.0],
[1.167,1.362],
[1.333,1.479],
[1.5,1.574],
[1.667,1.638],
[1.833,1.66],
[2.0,1.66]
[ ]
"effectOfRelativeValueOnDemand") model.plot_lookup(
Price Adjustment
= 3.0 priceAdjustmentTime.equation
= capacityCost/productionRate
capacityCostPerUnit.equation = unitVariableCost+capacityCostPerUnit
minimumPrice.equation = sd.lookup(normalizedPerceivedInventoryCoverage,"effectOfInventoryCoverageOnPrice")
effectOfInventoryCoverageOnPrice.equation = expectedPrice/effectOfInventoryCoverageOnPrice
price.equation = sd.max(price, minimumPrice)
indicatedPrice.equation = (indicatedPrice-expectedPrice)/priceAdjustmentTime changeInExpectedPrice.equation
"effectOfInventoryCoverageOnPrice"] = [
model.points[0.0,1.404],
[0.167,1.415],
[0.333,1.404],
[0.5,1.372],
[0.667,1.351],
[0.833,1.277],
[1.0,1.0],
[1.167,0.787],
[1.333,0.550],
[1.5,0.4],
[1.667,0.34],
[1.833,0.298],
[2.0,0.298]
[ ]
"effectOfInventoryCoverageOnPrice") model.plot_lookup(
Profit
= 0.5
unitCapacityCost.equation = 1.0 unitVariableCost.equation
= unitCapacityCost*productionCapacity
capacityCost.equation = unitVariableCost*productionRate
variableCost.equation = variableCost + capacityCost
cost.equation = price * consumptionRate
revenue.equation = revenue - cost profit.equation
Perceived Inventory Coverage
= 3.0
referenceInventoryCoverage.equation = 3.0 inventoryCoveragePerceptionTime.equation
= (inventoryCoverage-perceivedInventoryCoverage)/inventoryCoveragePerceptionTime
changeInPerceivedInventoryCoverage.equation = perceivedInventoryCoverage/referenceInventoryCoverage
normalizedPerceivedInventoryCoverage.equation = changeInPerceivedInventoryCoverage perceivedInventoryCoverage.equation
Expected Profitability
= 100.0
referenceExpectedProfitability.equation = 12.0 profitAdjustmentTime.equation
= (profit-expectedProfitability)/profitAdjustmentTime
changeInExpectedProfitability.equation = changeInExpectedProfitability
expectedProfitability.equation = expectedProfitability/referenceExpectedProfitability normalizedExpectedProfitability.equation
Scenario Management
We now use the scenario management of the BPTK-Py framework. We first import the library.
import BPTK_Py
= BPTK_Py.bptk() bptk
Then we set up a scenario manager using a Python dictionary. The scenario manager identifies the baseline constants of the model:
= {
scenario_manager "smCompetitivePricing":{
"model": model,
"base_constants": {
"marketShockOn":0,
"sizeOfShock":50
},"base_points":{
"effectOfProfitabilityOnCapacityUtilization":
[0.0, 0.324],
[0.16666666666666666, 0.33],
[0.3333333333333333, 0.372],
[0.5, 0.394],
[0.6666666666666666, 0.41],
[0.8333333333333334, 0.42],
[1.0, 0.5],
[1.1666666666666667, 0.745],
[1.3333333333333333, 0.80075],
[1.5, 0.8565],
[1.6666666666666667, 0.91225],
[1.8333333333333333, 0.968],
[2.0, 0.968]
[
],"effectOfRelativeValueOnDemand" :
[0.0, 0.17],
[0.16666666666666666, 0.191],
[0.3333333333333333, 0.213],
[0.5, 0.277],
[0.6666666666666666, 0.351],
[0.8333333333333334, 0.479],
[1.0, 1.0],
[1.1666666666666667, 1.362],
[1.3333333333333333, 1.479],
[1.5, 1.574],
[1.6666666666666667, 1.638],
[1.8333333333333333, 1.66],
[2.0, 1.66]
[
]
,"effectOfInventoryCoverageOnPrice" :
[0.0, 1.404],
[0.16666666666666666, 1.415],
[0.3333333333333333, 1.404],
[0.5, 1.372],
[0.6666666666666666, 1.351],
[0.8333333333333334, 1.277],
[1.0, 1.0],
[1.1666666666666667, 0.787],
[1.3333333333333333, 0.55],
[1.5, 0.4],
[1.6666666666666667, 0.34],
[1.8333333333333333, 0.298],
[2.0, 0.298]
[
]
},"scenarios":{
"base":{
}
}
} }
The next step is to register the scenario manager as follows:
bptk.register_scenario_manager(scenario_manager)
Once we have this, we can define and register more scenarios as follows:
bptk.register_scenarios(=
scenarios
{"marketShock":{
"constants":{
"marketShockOn":1
}
},"availabilityLoop":{
"constants":{
"marketShockOn":1
},"points":{
"effectOfProfitabilityOnCapacityUtilization" :
[0.0, 0.5],
[0.16666666666666666, 0.5],
[0.3333333333333333, 0.5],
[0.5, 0.5],
[0.6666666666666666, 0.5],
[0.8333333333333334, 0.5],
[1.0, 0.5],
[1.1666666666666667, 0.5],
[1.3333333333333333, 0.5],
[1.5, 0.5],
[1.6666666666666667, 0.5],
[1.8333333333333333, 0.5],
[2.0, 0.5]
[
],"effectOfRelativeValueOnDemand" :
[0.0, 1.0],
[0.16666666666666666,1.0],
[0.3333333333333333, 1.0],
[0.5, 1.0],
[0.6666666666666666, 1.0],
[0.8333333333333334, 1.0],
[1.0, 1.0],
[1.1666666666666667, 1.0],
[1.3333333333333333,1.0],
[1.5, 1.0],
[1.6666666666666667,1.0],
[1.8333333333333333, 1.0],
[2.0, 1.0]
[
],"effectOfInventoryCoverageOnPrice" :
[0.0, 1.0],
[0.16666666666666666, 1.0],
[0.3333333333333333, 1.0],
[0.5, 1.0],
[0.6666666666666666, 1.0],
[0.8333333333333334, 1.0],
[1.0, 1.0],
[1.1666666666666667, 1.0],
[1.3333333333333333, 1.0],
[1.5, 1.0],
[1.6666666666666667, 1.0],
[1.8333333333333333, 1.0],
[2.0, 1.0]
[
]
}
},"capacityUtilizationLoop":{
"constants":{
"marketShockOn":1
},"points":{
"effectOfRelativeValueOnDemand" :
[0.0, 1.0],
[0.16666666666666666,1.0],
[0.3333333333333333, 1.0],
[0.5, 1.0],
[0.6666666666666666, 1.0],
[0.8333333333333334, 1.0],
[1.0, 1.0],
[1.1666666666666667, 1.0],
[1.3333333333333333,1.0],
[1.5, 1.0],
[1.6666666666666667,1.0],
[1.8333333333333333, 1.0],
[2.0, 1.0]
[
]
}
},"substitutionLoop":{
"constants":{
"marketShockOn":1
},"points":{
"effectOfProfitabilityOnCapacityUtilization" :
[0.0, 0.5],
[0.16666666666666666, 0.5],
[0.3333333333333333, 0.5],
[0.5, 0.5],
[0.6666666666666666, 0.5],
[0.8333333333333334, 0.5],
[1.0, 0.5],
[1.1666666666666667, 0.5],
[1.3333333333333333, 0.5],
[1.5, 0.5],
[1.6666666666666667, 0.5],
[1.8333333333333333, 0.5],
[2.0, 0.5]
[
]
}
}
}
,="smCompetitivePricing") scenario_manager
Using the plot_lookup
function on the bptk class, we can compare the lookup functions between different scenarios.
bptk.plot_lookup(=["smCompetitivePricing"],
scenario_managers=["effectOfProfitabilityOnCapacityUtilization"],
lookup_names=["base","availabilityLoop"]) scenarios
Scenario Experiments
Base Case
Let’s quickly run through the base case first, which starts the model in equilibrium.
The equilibrium price for our product is set at EUR 3. This is equal to both the indicated price and the expected price. The minimum price (the amount we need to be profitable) is at EUR 1.5
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["minimumPrice","price","indicatedPrice","expectedPrice"]
equations )
Initially there is a reference demand of 100 units per month. Because the market is in equilibrium, the reference demand (i.e. the demand given the equilibrium price) equals the actual demand.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["demand","referenceDemand"]
equations )
Our production capacity is 200 units per month - but we only produce 100 units per month to avoid overstocking and are thus at a utilization of 50%. 300 units are “work in progress” within production.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["productionCapacity","productionRate","production"]
equations )
Capacity utilization is thus at 50%.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["capacityUtilization"]
equations )
The inventory is also at 300 and the consumption rate equals the demand, i.e. is at 100.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["inventory","consumptionRate"]
equations )
This leads to an inventory coverage of 3.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["inventoryCoverage"]
equations )
Market Shock
Let’s investigate what happens if there is a sudden increase in the underlying demand of 50% at timestep 10. Note that the increase in demand is at the current level of pricing.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["marketShock"],
scenarios=["referenceDemand"]
equations )
The graph below shows how the actual demand and the inventory develops - as expected, there is an inital demand peak. But this causes the inventory to drop, which increases the price. The increase price leads to increased production, which then lowers the price and thus increases demand.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["marketShock"],
scenarios=["demand", "inventory"]
equations )
This is because the actual price affects the demand - if the price is higher then the reference price, demand drops, if the price is lower, demand increases compared to the reference demand.
The plot below shows how the dependency is quantified in this model using a non-linear function - the exact shape of this function will depend on your specific situation.
bptk.plot_lookup(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["effectOfRelativeValueOnDemand"]
lookup_names )
We also assume that prices are sensitive to the availability of the product. If the product becomes scare (i.e. the inventory coverage falls), prices go up, and vice versa. The dependency is modelled using the following table function:
bptk.plot_lookup(=["smCompetitivePricing"],
scenario_managers=["base"],
scenarios=["effectOfInventoryCoverageOnPrice"]
lookup_names )
The production rate increases to a new, much higher level.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["marketShock"],
scenarios=['productionRate']
equations )
The price increases initially, because of the drop in inventory coverage. But the inventory coverage quickly recoveres due to increase production. Because inventory coverage influences the price, this leads to a price that is actually lower than the original price, but also to a demand that is higher than that indicated by the initial market shock.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["marketShock"],
scenarios=['price','inventoryCoverage']
equations )
even though the overall demand incrase is only at around 75%, our profits more than double, as seen in the graph below.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base","marketShock"],
scenarios=['profit']
equations )
This is because we are utilizing our production capacity better: we go from a utilization of 50% to a utilization above 90%, as seen in the graph below.
bptk.plot_scenarios(=["smCompetitivePricing"],
scenario_managers=["base","marketShock"],
scenarios=['capacityUtilization']
equations )
The figures above show that with the given shock of a 50% increase in underlying demand, we get close to our capacity limits.