import matplotlib.pyplot as plt
import drisk as drComposing Monte Carlo Models
A new product launch profit model
Motivation
A product team is considering a new launch. The key inputs are uncertain:
- the addressable market size,
- the share of that market that converts to customers,
- the average selling price,
- the unit cost, and
- the fixed launch cost.
With drisk, each uncertain input can be represented as an elicited distribution. The business model can then be written with ordinary Python arithmetic. Arithmetic on distribution objects returns an MCModel, a lazy Monte Carlo expression that samples each input and applies NumPy operations sample-by-sample.
Elicit uncertain inputs
We use PERT distributions for expert min/mode/max estimates, a logit-normal distribution for a conversion rate bounded between 0 and 1, and a lognormal distribution for positive skew in selling price.
market_size = dr.PERT.elicit(
min=20_000,
mode=50_000,
max=100_000,
name="Addressable market size",
)
conversion = dr.LogitNormal.elicit(
lower=0.02,
upper=0.08,
confidence=0.8,
name="Conversion rate",
)
price = dr.LogNormal.elicit(
lower=40,
upper=80,
confidence=0.8,
name="Average selling price",
)
unit_cost = dr.PERT.elicit(
min=12,
mode=18,
max=30,
name="Unit cost",
)
fixed_cost = dr.PERT.elicit(
min=100_000,
mode=150_000,
max=250_000,
name="Fixed launch cost",
)Each distribution also has a convenience .plot() method for a quick visual check of the elicited shape.
price.plot(color="limegreen")
unit_cost.plot(color='salmon')
Compose the model
The profit model is just the business logic:
customers = market_size * conversion
revenue = customers * price
variable_cost = customers * unit_cost
profit = revenue - variable_cost - fixed_cost
type(profit).__name__'MCModel'
profit is not an analytic distribution. It is an MCModel: a sampleable expression built from distributions, constants, and arithmetic operations.
Simulate outcomes
sims = profit.sample(size=50_000, seed=42)
summary = profit.summary(threshold=0)
summary| mean | p(> 0) | p99 | p90 | p75 | p50 | p25 | p10 | p1 | |
|---|---|---|---|---|---|---|---|---|---|
| metric | |||||||||
| value | -59958.86 | 0.17 | -183166.16 | -142182.21 | -114149.73 | -77754.02 | -27099.81 | 40256.46 | 244085.91 |
Present the result
fig, ax = plt.subplots(figsize=(8, 4.5))
profit.plot(ax=ax, color="#4C78A8")
ax.set_title("Simulated product launch profit")
ax.set_xlabel("Profit")
fig.tight_layout()
Why composition matters
The final model is readable because it mirrors the underlying decision logic:
profit = market_size * conversion * (price - unit_cost) - fixed_cost
profit.sample(size=5, seed=7)array([-123317.17486257, -56545.4697437 , -93175.65503927,
-96029.98966468, -99183.96846255])
That expression remains lazy until .sample(...) is called. During sampling, drisk uses a shared random number generator across the inputs and NumPy broadcasting for the sample-wise arithmetic.