Skip to content

Commit d53f5d6

Browse files
committed
Improve runtime performance via fewer calls
We'll pre-compute and pass around some values which we are highly likely to need more than once. There's very little overhead in the use once case. I suppose if it is common to call these and not use the functions.values ever then this might be more expensive. I have no idea what kind of side-effects might be expected, though Some of this code was already done in this style, so I assume this is readable enough and perhaps welcome.
1 parent 8384650 commit d53f5d6

File tree

8 files changed

+104
-74
lines changed

8 files changed

+104
-74
lines changed

lib/statistics/distributions/beta.ex

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ defmodule Statistics.Distributions.Beta do
1717
"""
1818
@spec pdf(number, number) :: fun
1919
def pdf(a, b) do
20+
bab = Functions.beta(a, b)
21+
2022
fn x ->
2123
cond do
2224
x <= 0.0 ->
2325
0.0
2426

2527
true ->
26-
Math.pow(x, a - 1) * Math.pow(1 - x, b - 1) / Functions.beta(a, b)
28+
Math.pow(x, a - 1) * Math.pow(1 - x, b - 1) / bab
2729
end
2830
end
2931
end
@@ -91,15 +93,17 @@ defmodule Statistics.Distributions.Beta do
9193
9294
"""
9395
@spec rand(number, number) :: number
94-
def rand(a, b) do
96+
def rand(a, b), do: rand(pdf(a, b))
97+
98+
defp rand(rpdf) do
9599
# beta only exists between 0 and 1
96100
x = Math.rand()
97101

98-
if pdf(a, b).(x) > Math.rand() do
102+
if rpdf.(x) > Math.rand() do
99103
x
100104
else
101105
# keep trying
102-
rand(a, b)
106+
rand(rpdf)
103107
end
104108
end
105109
end

lib/statistics/distributions/binomial.ex

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,18 @@ defmodule Statistics.Distributions.Binomial do
6666
@spec ppf(non_neg_integer, number) :: fun
6767
def ppf(n, p) do
6868
fn x ->
69-
ppf_tande(x, n, p)
69+
ppf_tande(x, n, p, cdf(n, p), 0)
7070
end
7171
end
7272

7373
# trial-and-error method which refines guesses
7474
# to arbitrary number of decimal places
75-
defp ppf_tande(x, n, p, g \\ 0) do
76-
g_cdf = cdf(n, p).(g)
75+
defp ppf_tande(x, n, p, npcdf, g) do
76+
g_cdf = npcdf.(g)
7777

7878
cond do
7979
x > g_cdf ->
80-
ppf_tande(x, n, p, g + 1)
80+
ppf_tande(x, n, p, npcdf, g + 1)
8181

8282
x <= g_cdf ->
8383
g
@@ -96,14 +96,16 @@ defmodule Statistics.Distributions.Binomial do
9696
9797
"""
9898
@spec rand(non_neg_integer, number) :: non_neg_integer
99-
def rand(n, p) do
99+
def rand(n, p), do: rand(n, p, pmf(n, p))
100+
101+
defp rand(n, p, rpmf) do
100102
x = Math.rand() * n
101103

102-
if pmf(n, p).(x) > Math.rand() do
104+
if rpmf.(x) > Math.rand() do
103105
Float.round(x)
104106
else
105107
# keep trying
106-
rand(n, p)
108+
rand(n, p, rpmf)
107109
end
108110
end
109111
end

lib/statistics/distributions/chisq.ex

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ defmodule Statistics.Distributions.Chisq do
1919
"""
2020
@spec pdf(non_neg_integer) :: fun
2121
def pdf(df) do
22-
fn x ->
23-
1 / (Math.pow(2, df / 2) * Functions.gamma(df / 2)) * Math.pow(x, df / 2 - 1) *
24-
Math.exp(-1 * x / 2)
25-
end
22+
hdf = df / 2
23+
g = Math.pow(2, hdf) * Functions.gamma(hdf)
24+
25+
fn x -> 1 / g * Math.pow(x, hdf - 1) * Math.exp(-1 * x / 2) end
2626
end
2727

2828
@doc """
@@ -36,9 +36,11 @@ defmodule Statistics.Distributions.Chisq do
3636
"""
3737
@spec cdf(non_neg_integer) :: fun
3838
def cdf(df) do
39+
hdf = df / 2.0
40+
g = Functions.gamma(hdf)
41+
3942
fn x ->
40-
g = Functions.gamma(df / 2.0)
41-
b = Functions.gammainc(df / 2.0, x / 2.0)
43+
b = Functions.gammainc(hdf, x / 2.0)
4244
b / g
4345
end
4446
end
@@ -55,28 +57,28 @@ defmodule Statistics.Distributions.Chisq do
5557
@spec ppf(non_neg_integer) :: fun
5658
def ppf(df) do
5759
fn x ->
58-
ppf_tande(x, df)
60+
ppf_tande(x, cdf(df))
5961
end
6062
end
6163

6264
# trial-and-error method which refines guesses
6365
# to arbitrary number of decimal places
64-
defp ppf_tande(x, df, precision \\ 14) do
65-
ppf_tande(x, df, 0, precision + 2, 0)
66+
defp ppf_tande(x, tcdf, precision \\ 14) do
67+
ppf_tande(x, tcdf, 0, precision + 2, 0)
6668
end
6769

6870
defp ppf_tande(_, _, g, precision, precision) do
6971
g
7072
end
7173

72-
defp ppf_tande(x, df, g, precision, p) do
74+
defp ppf_tande(x, tcdf, g, precision, p) do
7375
increment = 100 / Math.pow(10, p)
7476
guess = g + increment
7577

76-
if x < cdf(df).(guess) do
77-
ppf_tande(x, df, g, precision, p + 1)
78+
if x < tcdf.(guess) do
79+
ppf_tande(x, tcdf, g, precision, p + 1)
7880
else
79-
ppf_tande(x, df, guess, precision, p)
81+
ppf_tande(x, tcdf, guess, precision, p)
8082
end
8183
end
8284

@@ -92,14 +94,16 @@ defmodule Statistics.Distributions.Chisq do
9294
9395
"""
9496
@spec rand(non_neg_integer) :: number
95-
def rand(df) do
97+
def rand(df), do: rand(df, cdf(df))
98+
99+
defp rand(df, rcdf) do
96100
x = Math.rand() * 100
97101

98-
if pdf(df).(x) > Math.rand() do
102+
if rcdf.(x) > Math.rand() do
99103
x
100104
else
101105
# keep trying
102-
rand(df)
106+
rand(df, rcdf)
103107
end
104108
end
105109
end

lib/statistics/distributions/f.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ defmodule Statistics.Distributions.F do
2020
"""
2121
@spec pdf(number, number) :: fun
2222
def pdf(d1, d2) do
23+
powa = Math.pow(d2, d2)
24+
cfac = Functions.beta(d1 / 2, d2 / 2)
25+
2326
fn x ->
2427
# create components
25-
a = Math.pow(d1 * x, d1) * Math.pow(d2, d2)
28+
a = Math.pow(d1 * x, d1) * powa
2629
b = Math.pow(d1 * x + d2, d1 + d2)
27-
c = x * Functions.beta(d1 / 2, d2 / 2)
30+
c = x * cfac
2831
# for the equation
2932
Math.sqrt(a / b) / c
3033
end
@@ -47,9 +50,11 @@ defmodule Statistics.Distributions.F do
4750
# which is the CDF of the Beta distribution
4851
@spec cdf(number, number) :: fun
4952
def cdf(d1, d2) do
53+
bcdf = Beta.cdf(d1 / 2, d2 / 2)
54+
5055
fn x ->
5156
xx = d1 * x / (d1 * x + d2)
52-
Beta.cdf(d1 / 2, d2 / 2).(xx)
57+
bcdf.(xx)
5358
end
5459
end
5560

lib/statistics/distributions/hypergeometric.ex

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ defmodule Statistics.Distributions.Hypergeometric do
1717
"""
1818
@spec pmf(non_neg_integer, non_neg_integer, non_neg_integer) :: fun
1919
def pmf(pn, pk, n) do
20+
combos = Math.combination(pn, n)
21+
2022
fn k ->
2123
cond do
2224
n < k ->
@@ -33,7 +35,7 @@ defmodule Statistics.Distributions.Hypergeometric do
3335

3436
true ->
3537
xk = Math.to_int(k)
36-
Math.combination(pk, xk) * Math.combination(pn - pk, n - xk) / Math.combination(pn, n)
38+
Math.combination(pk, xk) * Math.combination(pn - pk, n - xk) / combos
3739
end
3840
end
3941
end
@@ -46,10 +48,12 @@ defmodule Statistics.Distributions.Hypergeometric do
4648
"""
4749
@spec cdf(non_neg_integer, non_neg_integer, non_neg_integer) :: fun
4850
def cdf(pn, pk, n) do
51+
cpmf = pmf(pn, pk, n)
52+
4953
fn k ->
5054
0..Math.to_int(Math.floor(k))
5155
|> Enum.to_list()
52-
|> Enum.map(fn i -> pmf(pn, pk, n).(i) end)
56+
|> Enum.map(fn i -> cpmf.(i) end)
5357
|> Enum.sum()
5458
end
5559
end
@@ -63,19 +67,19 @@ defmodule Statistics.Distributions.Hypergeometric do
6367
@spec ppf(non_neg_integer, non_neg_integer, non_neg_integer) :: fun
6468
def ppf(pn, pk, n) do
6569
fn x ->
66-
ppf_tande(x, pn, pk, n)
70+
ppf_tande(x, cdf(pn, pk, n), 0)
6771
end
6872
end
6973

7074
# trial-and-error method which refines guesses
7175
# to arbitrary number of decimal places
7276

73-
defp ppf_tande(x, pn, pk, n, guess \\ 0) do
74-
g_cdf = cdf(pn, pk, n).(guess)
77+
defp ppf_tande(x, tcdf, guess) do
78+
g_cdf = tcdf.(guess)
7579

7680
cond do
7781
x > g_cdf ->
78-
ppf_tande(x, pn, pk, n, guess + 1)
82+
ppf_tande(x, tcdf, guess + 1)
7983

8084
x <= g_cdf ->
8185
guess
@@ -86,14 +90,16 @@ defmodule Statistics.Distributions.Hypergeometric do
8690
Draw a random number from hypergeometric distribution
8791
"""
8892
@spec rand(non_neg_integer, non_neg_integer, non_neg_integer) :: non_neg_integer
89-
def rand(pn, pk, n) do
93+
def rand(pn, pk, n), do: rand(pk, pmf(pn, pk, n))
94+
95+
defp rand(pk, rpmf) do
9096
x = Math.floor(Math.rand() * pk)
9197

92-
if pmf(pn, pk, n).(x) > Math.rand() do
98+
if rpmf.(x) > Math.rand() do
9399
Float.round(x)
94100
else
95101
# keep trying
96-
rand(pn, pk, n)
102+
rand(pk, rpmf)
97103
end
98104
end
99105
end

lib/statistics/distributions/normal.ex

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ defmodule Statistics.Distributions.Normal do
6161

6262
@spec cdf(number, number) :: fun
6363
def cdf(mu, sigma) do
64+
denom = sigma * Math.sqrt(2)
65+
6466
fn x ->
65-
0.5 * (1.0 + Functions.erf((x - mu) / (sigma * Math.sqrt(2))))
67+
0.5 * (1.0 + Functions.erf((x - mu) / denom))
6668
end
6769
end
6870

@@ -127,27 +129,25 @@ defmodule Statistics.Distributions.Normal do
127129
end
128130

129131
@spec rand(number, number) :: number
130-
def rand(mu, sigma) do
132+
def rand(mu, sigma), do: rand(mu, sigma, pdf(0, 1))
133+
134+
defp rand(mu, sigma, rpdf) do
131135
# Note: an alternate method exists and may be better
132136
# Inverse transform sampling - https://en.wikipedia.org/wiki/Inverse_transform_sampling
133137
# ----
134138
# Generate a random number between -10,+10
135139
# (probability of 10 ocurring in a Normal(0,1) distribution is
136140
# too small to calculate with the precision available to us)
137141
x = Math.rand() * 20 - 10
138-
rmu = 0
139-
rsigma = 1
140142

141143
cond do
142-
pdf(rmu, rsigma).(x) > Math.rand() ->
143-
# get z-score
144-
z = (rmu - x) / rsigma
144+
rpdf.(x) > Math.rand() ->
145145
# transpose to specified distribution
146-
mu + z * sigma
146+
mu - x * sigma
147147

148148
true ->
149149
# keep trying
150-
rand(mu, sigma)
150+
rand(mu, sigma, rpdf)
151151
end
152152
end
153153
end

lib/statistics/distributions/poisson.ex

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ defmodule Statistics.Distributions.Poisson do
2121
"""
2222
@spec pmf(number) :: fun
2323
def pmf(lambda) do
24+
nexp = Math.exp(-lambda)
25+
2426
fn k ->
25-
Math.pow(lambda, k) / Math.factorial(k) * Math.exp(-lambda)
27+
Math.pow(lambda, k) / Math.factorial(k) * nexp
2628
end
2729
end
2830

@@ -37,12 +39,14 @@ defmodule Statistics.Distributions.Poisson do
3739
"""
3840
@spec cdf(number) :: fun
3941
def cdf(lambda) do
42+
nexp = Math.exp(-1 * lambda)
43+
4044
fn k ->
4145
s =
4246
Enum.map(0..Math.to_int(k), fn x -> Math.pow(lambda, x) / Math.factorial(x) end)
4347
|> Enum.sum()
4448

45-
Math.exp(-lambda) * s
49+
nexp * s
4650
end
4751
end
4852

@@ -61,15 +65,17 @@ defmodule Statistics.Distributions.Poisson do
6165
"""
6266
@spec ppf(number) :: fun
6367
def ppf(lambda) do
68+
lcdf = cdf(lambda)
69+
6470
fn x ->
65-
ppf_tande(x, lambda, 0.0)
71+
ppf_tande(x, lcdf, 0.0)
6672
end
6773
end
6874

6975
# the trusty trial-and-error method
70-
defp ppf_tande(x, lambda, guess) do
71-
if x > cdf(lambda).(guess) do
72-
ppf_tande(x, lambda, guess + 1)
76+
defp ppf_tande(x, lcdf, guess) do
77+
if x > lcdf.(guess) do
78+
ppf_tande(x, lcdf, guess + 1)
7379
else
7480
guess
7581
end
@@ -87,14 +93,16 @@ defmodule Statistics.Distributions.Poisson do
8793
8894
"""
8995
@spec rand(number) :: number
90-
def rand(lambda) do
96+
def rand(lambda), do: rand(lambda, pmf(lambda))
97+
98+
defp rand(lambda, lpmf) do
9199
x = (Math.rand() * 100 + lambda) |> Math.floor()
92100

93-
if pmf(lambda).(x) > Math.rand() do
101+
if lpmf.(x) > Math.rand() do
94102
x
95103
else
96104
# keep trying
97-
rand(lambda)
105+
rand(lambda, lpmf)
98106
end
99107
end
100108
end

0 commit comments

Comments
 (0)