/
Probability.lua
133 lines (109 loc) · 2.88 KB
/
Probability.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
--[=[
Probability utility functions
@class Probability
]=]
local Probability = {}
--[=[
Returns a boxMuller random distribution
@return number
]=]
function Probability.boxMuller()
return math.sqrt(-2 * math.log(math.random())) * math.cos(2 * math.pi * math.random()) / 2
end
--[=[
Returns a normal distribution
@param mean number
@param standardDeviation number
@return number
]=]
function Probability.normal(mean, standardDeviation)
return mean + Probability.boxMuller() * standardDeviation
end
--[=[
Returns a bounded normal, clamping the normal value
@param mean number
@param standardDeviation number
@param hardMin number
@param hardMax number
@return number
]=]
function Probability.boundedNormal(mean, standardDeviation, hardMin, hardMax)
return math.clamp(Probability.normal(mean, standardDeviation), hardMin, hardMax)
end
--[=[
Approximation of the error function (erf) using the Abramowitz and Stegun formula
https://en.wikipedia.org/wiki/Error_function
@param x number
@return number
]=]
function Probability.erf(x)
local t = 1 / (1 + 0.5 * math.abs(x))
local erf_approx = 1 - t * math.exp(-x * x - 1.26551223 +
t * (1.00002368 +
t * (0.37409196 +
t * (0.09678418 +
t * (-0.18628806 +
t * (0.27886807 +
t * (-1.13520398 +
t * (1.48851587 +
t * (-0.82215223 +
t * (0.17087277))))))))))
if x >= 0 then
return erf_approx
else
return -erf_approx
end
end
--[=[
Standard normal cumulative distribution function. Returns the value from 0 to 1.
This is also known as percentile!
@param zScore number
@return number
]=]
function Probability.cdf(zScore)
assert(type(zScore) == "number", "Bad zScore")
return 0.5 * (1 + Probability.erf(zScore / math.sqrt(2)))
end
--[=[
Function to calculate the inverse error function (erfinv) using Newton's method
@param x number
@return number
]=]
function Probability.erfinv(x)
assert(type(x) == "number", "Bad x")
if x < -1 or x > 1 then
return nil
elseif x == -1 then
return -math.huge
elseif x == 1 then
return math.huge
end
local tolerance = 1e-15
local maxIterations = 1000
local function derivative(y)
return 2 * math.exp(-y * y) / math.sqrt(math.pi)
end
local y = 0
for _ = 1, maxIterations do
local error = Probability.erf(y) - x
if math.abs(error) < tolerance then
return y
end
y = y - error / derivative(y)
end
-- If Newton's method fails to converge, return nil
return nil
end
--[=[
Standard normal cumulative distribution function. Returns the value from 0 to 1.
This is also known as percentile!
@param percentile number
@return number
]=]
function Probability.percentileToZScore(percentile)
assert(type(percentile) == "number" and percentile >= 0 and percentile <= 1, "Bad percentile")
-- Calculate z-score using inverse cumulative distribution function (norm.ppf)
local zScore = math.sqrt(2) * Probability.erfinv(2 * percentile - 1)
return zScore
end
return Probability