Skip to content

Commit 6dc7e5b

Browse files
committed
feat(distributions): add distribution classes
1 parent 6b1235d commit 6dc7e5b

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

simulation/distributions.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""
2+
Distributions.
3+
4+
Acknowledgements:
5+
> Heather, A. Monks, T. (2025). Python DES RAP Template.
6+
https://github.com/pythonhealthdatascience/rap_template_python_des (MIT).
7+
> Monks, T. (2021) sim-tools: fundamental tools to support the simulation
8+
process in python. https://github.com/TomMonks/sim-tools. (MIT).
9+
"""
10+
11+
import math
12+
import numpy as np
13+
14+
15+
class Exponential:
16+
"""
17+
Generate samples from an exponential distribution.
18+
19+
This class is from Heather and Monks 2025, who adapted from Monks 2021.
20+
"""
21+
def __init__(self, mean, random_seed):
22+
"""
23+
Initialises a new distribution.
24+
25+
Arguments:
26+
mean (float):
27+
Mean of the exponential distribution.
28+
random_seed (int|None):
29+
Random seed to control sampling.
30+
"""
31+
if mean <= 0:
32+
raise ValueError('Exponential mean must be greater than 0.')
33+
34+
self.mean = mean
35+
self.rand = np.random.default_rng(random_seed)
36+
37+
def sample(self, size=None):
38+
"""
39+
Generate sample.
40+
41+
Arguments:
42+
size (int|None):
43+
Number of samples to return. If set to none, then returns a
44+
single sample.
45+
46+
Returns:
47+
float or numpy.ndarray:
48+
A single sample if size is None, or an array of samples if
49+
size is specified.
50+
"""
51+
return self.rand.exponential(self.mean, size=size)
52+
53+
54+
class LogNormal:
55+
"""
56+
Generate samples from a lognormal distribution.
57+
58+
This class is adapted from Monks 2021.
59+
"""
60+
def __init__(self, mean, stdev, random_seed):
61+
"""
62+
Initialises a new distribution.
63+
64+
Arguments:
65+
mean (float):
66+
Mean of the lognormal distribution.
67+
stdev (float):
68+
Standard deviation of the lognormal distribution.
69+
random_seed (int|None):
70+
Random seed to control sampling.
71+
"""
72+
self.mu, self.sigma = (
73+
self.normal_moments_from_lognormal(m=mean, v=stdev**2))
74+
self.rand = np.random.default_rng(random_seed)
75+
76+
def normal_moments_from_lognormal(self, m, v):
77+
"""
78+
Calculate mu and sigma of normal distribution underlying a lognormal
79+
with mean m and variance v. Source: https://blogs.sas.com/content/iml/2
80+
014/06/04/simulate-lognormal-data-with-specified-mean-and-variance.html
81+
82+
Arguments:
83+
m (float):
84+
Mean of the lognormal distribution.
85+
v (float):
86+
Variance of the lognormal distribution.
87+
88+
Returns:
89+
float, float
90+
Mu and sigma.
91+
"""
92+
phi = math.sqrt(v + m**2)
93+
mu = math.log(m**2 / phi)
94+
sigma = math.sqrt(math.log(phi**2 / m**2))
95+
return mu, sigma
96+
97+
def sample(self, size=None):
98+
"""
99+
Generate sample.
100+
101+
Arguments:
102+
size (int|None):
103+
Number of samples to return. If set to none, then returns a
104+
single sample.
105+
106+
Returns:
107+
float or numpy.ndarray:
108+
A single sample if size is None, or an array of samples if
109+
size is specified.
110+
"""
111+
return self.rand.lognormal(self.mu, self.sigma, size=size)
112+
113+
114+
class Discrete:
115+
"""
116+
Generate samples from a discrete distribution.
117+
118+
This class is adapted from Monks 2021.
119+
"""
120+
def __init__(self, values, freq, random_seed):
121+
"""
122+
Initialises a new distribution.
123+
124+
Arguments:
125+
values (array-like):
126+
List of sample values. Must be equal length to freq.
127+
freq (array-like):
128+
List of observed frequencies. Must be equal length to values.
129+
random_seed (int|None):
130+
Random seed to control sampling.
131+
"""
132+
if len(values) != len(freq):
133+
raise ValueError(
134+
'values and freq arguments must be of equal length')
135+
self.values = np.asarray(values)
136+
self.freq = np.asarray(freq)
137+
self.probabilities = self.freq / self.freq.sum()
138+
self.rand = np.random.default_rng(random_seed)
139+
140+
def sample(self, size):
141+
"""
142+
Generate sample.
143+
144+
Arguments:
145+
size (int|None):
146+
Number of samples to return. If set to none, then returns a
147+
single sample.
148+
149+
Returns:
150+
float or numpy.ndarray:
151+
A single sample if size is None, or an array of samples if
152+
size is specified.
153+
"""
154+
sample = self.rand.choice(self.values, p=self.probabilities, size=size)
155+
156+
if size is None:
157+
return sample.item()
158+
return sample

0 commit comments

Comments
 (0)