import matplotlib.pyplot as plt
import drisk as drComposing Discrete and Continuous Distributions
Annual equipment-failure cost and downtime
Motivation
A field-operations team maintains a small fleet of pumps. They need a rough annual maintenance plan that accounts for two kinds of uncertainty:
- how many pump failures occur in a year, and
- the average cost and downtime per failure.
A Poisson distribution is a natural model for the count of failures in a fixed period. Repair cost and downtime are positive, right-skewed quantities. We will use a lognormal distribution for average repair cost and a PERT distribution for average downtime. In this example, these distributions represent the average per-failure outcome for the year, which makes the annual model easy to write as ordinary arithmetic:
annual outcome = number of failures * average outcome per failure
Elicit the inputs
The reliability engineer expects about five pump failures per year across the fleet.
failures = dr.Poisson.elicit(
lam=5.0,
name="Pump failures per year",
)For planning, the team estimates that the average repair cost per failure will usually fall between $3,000 and $12,000. For average downtime, the engineer provides a three-point estimate: optimistic, most likely, and pessimistic hours per failure.
avg_repair_cost = dr.LogNormal.elicit(
lower=3_000,
upper=12_000,
confidence=0.8,
name="Average repair cost per failure",
)
avg_downtime_hours = dr.PERT.elicit(
min=4,
mode=12,
max=36,
name="Average downtime hours per failure",
)The elicited shapes are easy to check before composing the model.
fig, axes = plt.subplots(1, 3, figsize=(12, 3.5))
failures.plot(ax=axes[0], color="#4C78A8")
avg_repair_cost.plot(ax=axes[1], color="#F58518")
avg_downtime_hours.plot(ax=axes[2], color="#54A24B")
fig.tight_layout()
Compose annual outcomes
Because drisk distributions support arithmetic, the annual outcomes read like the underlying planning logic.
annual_repair_cost = failures * avg_repair_cost
annual_repair_cost.name = "Annual repair cost"
annual_downtime_hours = failures * avg_downtime_hours
annual_downtime_hours.name = "Annual downtime hours"
type(annual_repair_cost).__name__'MCModel'
The count distribution is discrete and the average-outcome distributions are continuous, but the composed results are both sampleable MCModel expressions.
Simulate the annual plan
repair_summary = annual_repair_cost.summary(
size=50_000,
seed=42,
threshold=50_000,
)
downtime_summary = annual_downtime_hours.summary(
size=50_000,
seed=42,
threshold=120,
)
repair_summary| mean | p(> 50000) | p99 | p90 | p75 | p50 | p25 | p10 | p1 | |
|---|---|---|---|---|---|---|---|---|---|
| metric | |||||||||
| Annual repair cost | 34747.86 | 0.2 | 2782.42 | 10081.42 | 16736.91 | 27734.82 | 44909.51 | 67337.39 | 133181.73 |
downtime_summary| mean | p(> 120) | p99 | p90 | p75 | p50 | p25 | p10 | p1 | |
|---|---|---|---|---|---|---|---|---|---|
| metric | |||||||||
| Annual downtime hours | 73.4 | 0.14 | 7.22 | 24.78 | 40.22 | 64.14 | 96.75 | 134.96 | 215.47 |
cost_values = repair_summary.loc["Annual repair cost"]
downtime_values = downtime_summary.loc["Annual downtime hours"]
print(f"Mean annual repair cost: ${cost_values['mean']:,.0f}")
print(
"P10 / P50 / P90 annual repair cost: "
f"${cost_values['p10']:,.0f} / ${cost_values['p50']:,.0f} / ${cost_values['p90']:,.0f}"
)
print(f"Probability annual repair cost exceeds $50k: {cost_values['p(> 50000)']:.1%}")
print()
print(f"Mean annual downtime: {downtime_values['mean']:,.0f} hours")
print(
"P10 / P50 / P90 annual downtime: "
f"{downtime_values['p10']:,.0f} / {downtime_values['p50']:,.0f} / {downtime_values['p90']:,.0f} hours"
)
print(f"Probability annual downtime exceeds 120 hours: {downtime_values['p(> 120)']:.1%}")Mean annual repair cost: $34,748
P10 / P50 / P90 annual repair cost: $67,337 / $27,735 / $10,081
Probability annual repair cost exceeds $50k: 20.0%
Mean annual downtime: 73 hours
P10 / P50 / P90 annual downtime: 135 / 64 / 25 hours
Probability annual downtime exceeds 120 hours: 14.0%
Present the result
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
annual_repair_cost.plot(
ax=axes[0],
size=50_000,
seed=42,
color="#F58518",
)
axes[0].axvline(50_000, color="black", linestyle="--", linewidth=1, label="$50k budget")
axes[0].legend()
annual_downtime_hours.plot(
ax=axes[1],
size=50_000,
seed=42,
color="#54A24B",
)
axes[1].axvline(120, color="black", linestyle="--", linewidth=1, label="120 hours")
axes[1].legend()
fig.tight_layout()
What this composition assumes
This compact model samples one average repair cost and one average downtime for each simulated year. That is appropriate when the elicited lognormal distributions represent uncertainty about the annual average outcome per failure, not the outcome of every individual failure.
If the question changes to modeling each individual repair, the same ingredients can be used in a richer simulation: sample the number of failures, then sample and sum one cost or downtime value for each failure. The arithmetic composition shown here is the simpler planning model.