Skip to content
25 changes: 16 additions & 9 deletions src/probabilit/distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,30 @@ def Triangular(low, mode, high, low_perc=0.1, high_perc=0.9):
Distribution("triang", loc=-2.2360679774997894, scale=14.472135954999578, c=0.5000000000000001)
>>> Triangular(low=1, mode=5, high=9, low_perc=0.25, high_perc=0.75)
Distribution("triang", loc=-8.656854249492383, scale=27.313708498984766, c=0.5)
>>> Triangular(low=1, mode=5, high=9, low_perc=0, high_perc=1)
Distribution("triang", loc=1, scale=8, c=0.5)
"""
# A few comments on fitting can be found here:
# https://docs.analytica.com/index.php/Triangular10_50_90

if not (low < mode < high):
raise ValueError(f"Must have {low=} < {mode=} < {high=}")
if not ((0 < low_perc <= 1.0) and (0 <= high_perc < 1.0)):
if not ((0 <= low_perc <= 1.0) and (0 <= high_perc <= 1.0)):
raise ValueError("Percentiles must be between 0 and 1.")

# Optimize parameters
loc, scale, c = _fit_triangular_distribution(
low=low,
mode=mode,
high=high,
low_perc=low_perc,
high_perc=high_perc,
)
# No need to optimize if low and high are boundaries of distribution support
if np.isclose(low_perc, 0.0) and np.isclose(high_perc, 1.0):
loc, scale, c = low, high - low, (mode - low) / (high - low)

else:
# Optimize parameters
loc, scale, c = _fit_triangular_distribution(
low=low,
mode=mode,
high=high,
low_perc=low_perc,
high_perc=high_perc,
)
return Distribution("triang", loc=loc, scale=scale, c=c)


Expand Down
11 changes: 6 additions & 5 deletions tests/test_modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ def test_total_person_hours(self):

for i in range(num_rivets):
total_person_hours += Triangular(
low=3.75, mode=4.25, high=5.5, low_perc=0.00001, high_perc=0.99999
low=3.75, mode=4.25, high=5.5, low_perc=0, high_perc=1.0
)

num_samples = 10000
num_samples = 1000
res_total_person_hours = total_person_hours.sample(num_samples, rng)

# The mean and standard deviation of a Triangular(3.75, 4.25, 5.5) are 4.5 and 0.368,
Expand All @@ -126,10 +126,11 @@ def test_total_person_hours(self):
expected_std = 0.368 * np.sqrt(num_rivets)

sample_mean = np.mean(res_total_person_hours)
sample_std = np.std(res_total_person_hours)
sample_std = np.std(res_total_person_hours, ddof=1)

assert abs(sample_mean - expected_mean) < 0.3
assert abs(sample_std - expected_std) < 0.1
# Within 2% of theoretical values
np.testing.assert_allclose(sample_mean, expected_mean, rtol=0.02)
np.testing.assert_allclose(sample_std, expected_std, rtol=0.02)


def test_copying():
Expand Down