The OpenD Programming Language

1 /++
2 This module contains algorithms for the $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution).
3 
4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
5 
6 Authors: John Michael Hall
7 
8 Copyright: 2022-3 Mir Stat Authors.
9 
10 +/
11 
12 module mir.stat.distribution.log_normal;
13 
14 import mir.internal.utility: isFloatingPoint;
15 
16 /++
17 Computes the Log-normal probability density function (PDF).
18 
19 Params:
20     x = value to evaluate PDF
21 
22 See_also:
23     $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution)
24 +/
25 @safe pure nothrow @nogc
26 T logNormalPDF(T)(const T x)
27     if (isFloatingPoint!T)
28     in (x >= 0, "x must be greater than or equal to zero")
29 {
30     import mir.math.common: log;
31     import mir.stat.distribution.normal: normalPDF;
32     if (x == 0) return 0;
33     return x.log.normalPDF / x;
34 }
35 
36 /++
37 Ditto, with location and scale parameters (by standardizing `x`).
38 
39 Params:
40     x = value to evaluate PDF
41     mean = location parameter
42     stdDev = scale parameter
43 +/
44 @safe pure nothrow @nogc
45 T logNormalPDF(T)(const T x, const T mean, const T stdDev)
46     if (isFloatingPoint!T)
47     in (x >= 0, "x must be greater than or equal to zero")
48     in (stdDev > 0, "stdDev must be greater than zero")
49 {
50     import mir.math.common: log;
51     import mir.stat.distribution.normal: normalPDF;
52     if (x == 0) return 0;
53     return x.log.normalPDF(mean, stdDev) / x;
54 }
55 
56 ///
57 version(mir_stat_test)
58 @safe pure nothrow @nogc
59 unittest {
60     import mir.test: shouldApprox;
61 
62     logNormalPDF(1.0).shouldApprox == 0.3989423;
63     logNormalPDF(2.0).shouldApprox == 0.156874;
64     logNormalPDF(3.0).shouldApprox == 0.07272826;
65 
66     // Can include location/scale
67     logNormalPDF(1.0, 1, 2).shouldApprox == 0.1760327;
68     logNormalPDF(2.0, 1, 2).shouldApprox == 0.09856858;
69     logNormalPDF(3.0, 1, 2).shouldApprox == 0.06640961;
70 }
71 
72 // check zero or near zero
73 version(mir_stat_test)
74 @safe pure nothrow @nogc
75 unittest {
76     import mir.test: shouldApprox;
77 
78     logNormalPDF(0.0).shouldApprox == 0.0;
79     logNormalPDF(double.min_normal * double.epsilon).shouldApprox == 0.0;
80     logNormalPDF(double.min_normal).shouldApprox == 0.0;
81     
82     logNormalPDF(0.0, 1, 2).shouldApprox == 0.0;
83     logNormalPDF(double.min_normal * double.epsilon, 1, 2).shouldApprox == 0.0;
84     logNormalPDF(double.min_normal, 1, 2).shouldApprox == 0.0;
85 }
86 
87 /++
88 Computes the Log-normal cumulative distribution function (CDF).
89 
90 Params:
91     x = value to evaluate CDF
92 
93 See_also:
94     $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution)
95 +/
96 @safe pure nothrow @nogc
97 T logNormalCDF(T)(const T x)
98     if (isFloatingPoint!T)
99     in (x >= 0, "x must be greater than or equal to zero")
100 {
101     import mir.math.common: log;
102     import mir.stat.distribution.normal: normalCDF;
103     return x.log.normalCDF;
104 }
105 
106 /++
107 Ditto, with location and scale parameters (by standardizing `x`).
108 
109 Params:
110     x = value to evaluate CDF
111     mean = location parameter
112     stdDev = scale parameter
113 +/
114 @safe pure nothrow @nogc
115 T logNormalCDF(T)(const T x, const T mean, const T stdDev)
116     if (isFloatingPoint!T)
117     in (x >= 0, "x must be greater than or equal to zero")
118     in (stdDev > 0, "stdDev must be greater than zero")
119 {
120     import mir.math.common: log;
121     import mir.stat.distribution.normal: normalCDF;
122     return x.log.normalCDF(mean, stdDev);
123 }
124 
125 ///
126 version(mir_stat_test)
127 @safe pure nothrow @nogc
128 unittest {
129     import mir.test: shouldApprox;
130 
131     logNormalCDF(0.0).shouldApprox == 0;
132     logNormalCDF(1.0).shouldApprox == 0.5;
133     logNormalCDF(2.0).shouldApprox == 0.7558914;
134     logNormalCDF(3.0).shouldApprox == 0.8640314;
135 
136     // Can include location/scale
137     logNormalCDF(0.0, 1, 2).shouldApprox == 0;
138     logNormalCDF(1.0, 1, 2).shouldApprox == 0.3085375;
139     logNormalCDF(2.0, 1, 2).shouldApprox == 0.439031;
140     logNormalCDF(3.0, 1, 2).shouldApprox == 0.5196623;
141 }
142 
143 /++
144 Computes the Student's t complementary cumulative distribution function (CCDF).
145 
146 Params:
147     x = value to evaluate CCDF
148 
149 See_also:
150     $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution)
151 +/
152 @safe pure nothrow @nogc
153 T logNormalCCDF(T)(const T x)
154     if (isFloatingPoint!T)
155     in (x >= 0, "x must be greater than or equal to zero")
156 {
157     import mir.math.common: log;
158     import mir.stat.distribution.normal: normalCCDF;
159     return x.log.normalCCDF;
160 }
161 
162 /++
163 Ditto, with location and scale parameters (by standardizing `x`).
164 
165 Params:
166     x = value to evaluate CCDF
167     mean = location parameter
168     stdDev = scale parameter
169 +/
170 @safe pure nothrow @nogc
171 T logNormalCCDF(T)(const T x, const T mean, const T stdDev)
172     if (isFloatingPoint!T)
173     in (x >= 0, "x must be greater than or equal to zero")
174     in (stdDev > 0, "stdDev must be greater than zero")
175 {
176     import mir.math.common: log;
177     import mir.stat.distribution.normal: normalCCDF;
178     return x.log.normalCCDF(mean, stdDev);
179 }
180 
181 ///
182 version(mir_stat_test)
183 @safe pure nothrow @nogc
184 unittest {
185     import mir.test: shouldApprox;
186 
187     logNormalCCDF(0.0).shouldApprox == 1;
188     logNormalCCDF(1.0).shouldApprox == 0.5;
189     logNormalCCDF(2.0).shouldApprox == 0.2441086;
190     logNormalCCDF(3.0).shouldApprox == 0.1359686;
191 
192     // Can include location/scale
193     logNormalCCDF(0.0, 1, 2).shouldApprox == 1;
194     logNormalCCDF(1.0, 1, 2).shouldApprox == 0.6914625;
195     logNormalCCDF(2.0, 1, 2).shouldApprox == 0.560969;
196     logNormalCCDF(3.0, 1, 2).shouldApprox == 0.4803377;
197 }
198 
199 /++
200 Computes the Log-normal inverse cumulative distribution function (InvCDF).
201 
202 Params:
203     p = value to evaluate InvCDF
204 
205 See_also:
206     $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution)
207 +/
208 @safe pure nothrow @nogc
209 T logNormalInvCDF(T)(const T p)
210     if (isFloatingPoint!T)
211     in (p >= 0, "p must be greater than or equal to 0")
212     in (p <= 1, "p must be less than or equal to 1")
213     out (r; r >= 0, "return must be greater than or equal to zero")
214 {
215     import mir.math.common: exp;
216     import mir.stat.distribution.normal: normalInvCDF;
217     return p.normalInvCDF.exp;
218 }
219 
220 /++
221 Ditto, with location and scale parameters (by standardizing `x`).
222 
223 Params:
224     p = value to evaluate InvCDF
225     mean = location parameter
226     stdDev = scale parameter
227 +/
228 @safe pure nothrow @nogc
229 T logNormalInvCDF(T)(const T p, const T mean, const T stdDev)
230     if (isFloatingPoint!T)
231     in (p >= 0, "p must be greater than or equal to 0")
232     in (p <= 1, "p must be less than or equal to 1")
233     in (stdDev > 0, "stdDev must be greater than zero")
234     out (r; r >= 0, "return must be greater than or equal to zero")
235 {
236     import mir.math.common: exp;
237     import mir.stat.distribution.normal: normalInvCDF;
238     return p.normalInvCDF(mean, stdDev).exp;
239 }
240 
241 ///
242 version(mir_stat_test)
243 @safe pure nothrow @nogc
244 unittest {
245     import mir.test: shouldApprox;
246 
247     logNormalInvCDF(0.00).shouldApprox == 0;
248     logNormalInvCDF(0.25).shouldApprox == 0.5094163;
249     logNormalInvCDF(0.50).shouldApprox == 1;
250     logNormalInvCDF(0.75).shouldApprox == 1.963031;
251     logNormalInvCDF(1.00).shouldApprox == double.infinity;
252 
253     // Can include location/scale
254     logNormalInvCDF(0.00, 1, 2).shouldApprox == 0;
255     logNormalInvCDF(0.25, 1, 2).shouldApprox == 0.7054076;
256     logNormalInvCDF(0.50, 1, 2).shouldApprox == 2.718282;
257     logNormalInvCDF(0.75, 1, 2).shouldApprox == 10.47487;
258     logNormalInvCDF(1.00, 1, 2).shouldApprox == double.infinity;
259 }
260 
261 /++
262 Computes the Log-normal log probability density function (LPDF).
263 
264 Params:
265     x = value to evaluate LPDF
266 
267 See_also:
268     $(LINK2 https://en.wikipedia.org/wiki/Log-normal_distribution, Log-normal Distribution)
269 +/
270 @safe pure nothrow @nogc
271 T logNormalLPDF(T)(const T x)
272     if (isFloatingPoint!T)
273     in (x >= 0, "x must be greater than or equal to zero")
274 {
275     import mir.math.common: log;
276     import mir.stat.distribution.normal: normalLPDF;
277     if (x == 0) return -T.infinity;
278     return x.log.normalLPDF - log(x);
279 }
280 
281 /++
282 Ditto, with location and scale parameters (by standardizing `x`).
283 
284 Params:
285     x = value to evaluate LPDF
286     mean = location parameter
287     stdDev = scale parameter
288 +/
289 @safe pure nothrow @nogc
290 T logNormalLPDF(T)(const T x, const T mean, const T stdDev)
291     if (isFloatingPoint!T)
292     in (x >= 0, "x must be greater than or equal to zero")
293     in (stdDev > 0, "stdDev must be greater than zero")
294 {
295     import mir.math.common: log;
296     import mir.stat.distribution.normal: normalLPDF;
297     if (x == 0) return -T.infinity;
298     return x.log.normalLPDF(mean, stdDev) - log(x);
299 }
300 
301 ///
302 version(mir_stat_test)
303 @safe pure nothrow @nogc
304 unittest {
305     import mir.math.common: log;
306     import mir.test: shouldApprox;
307 
308     logNormalLPDF(1.0).shouldApprox == log(0.3989423);
309     logNormalLPDF(2.0).shouldApprox == log(0.156874);
310     logNormalLPDF(3.0).shouldApprox == log(0.07272826);
311 
312     // Can include location/scale
313     logNormalLPDF(1.0, 1, 2).shouldApprox == log(0.1760327);
314     logNormalLPDF(2.0, 1, 2).shouldApprox == log(0.09856858);
315     logNormalLPDF(3.0, 1, 2).shouldApprox == log(0.06640961);
316 }
317 
318 
319 // check zero
320 version(mir_stat_test)
321 @safe pure nothrow @nogc
322 unittest {
323     import mir.test: shouldApprox;
324 
325     logNormalLPDF(0.0).shouldApprox == -double.infinity;
326     logNormalLPDF(0.0, 1, 2).shouldApprox == -double.infinity;
327 }