The OpenD Programming Language

1 /++
2 This module contains algorithms for the $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric 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.geometric;
13 
14 import mir.internal.utility: isFloatingPoint;
15 
16 /++
17 Computes the geometric probability density function (PMF).
18 
19 Params:
20     k = value to evaluate PMF
21     p = `true` probability
22 
23 See_also: $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric Distribution)
24 +/
25 @safe pure @nogc nothrow
26 T geometricPMF(T)(const size_t k, const T p)
27     if (isFloatingPoint!T)
28     in (p >= 0, "p must be greater than or equal to 0")
29     in (p <= 1, "p must be less than or equal to 1")
30 {
31     import mir.math.common: pow;
32     return pow(1 - p, k) * p;   
33 }
34 
35 ///
36 @safe pure @nogc nothrow
37 version(mir_stat_test)
38 unittest
39 {
40     import mir.test: shouldApprox;
41 
42     0.geometricPMF(0.5).shouldApprox == 0.5;
43     1.geometricPMF(0.5).shouldApprox == 0.25;
44     2.geometricPMF(0.25).shouldApprox == 0.140625;
45 }
46 
47 /++
48 Computes the geometric cumulative density function (CDF).
49 
50 Params:
51     k = value to evaluate CDF
52     p = `true` probability
53 
54 See_also: $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric Distribution)
55 +/
56 @safe pure @nogc nothrow
57 T geometricCDF(T)(const size_t k, const T p)
58     if (isFloatingPoint!T)
59     in (p >= 0, "p must be greater than or equal to 0")
60     in (p <= 1, "p must be less than or equal to 1")
61 {
62     import mir.math.common: pow;
63     return 1 - pow(1 - p, k + 1);   
64 }
65 
66 /// ditto
67 @safe pure @nogc nothrow
68 T geometricCDF(T)(const T x, const T p)
69     if (isFloatingPoint!T)
70     in (x >= -1, "x must be larger than or equal to -1")
71     in (p >= 0, "p must be greater than or equal to 0")
72     in (p <= 1, "p must be less than or equal to 1")
73 {
74     if (x < 0)
75         return 0;
76     return geometricCDF!T(cast(const size_t) x, p);  
77 }
78 
79 ///
80 @safe pure @nogc nothrow
81 version(mir_stat_test)
82 unittest
83 {
84     import mir.test: shouldApprox;
85 
86     geometricCDF(-1.0, 0.5).shouldApprox == 0; // UFCS chaining deduces this as size_t instead of a floating point type
87     0.geometricCDF(0.5).shouldApprox == 0.5;
88     1.geometricCDF(0.5).shouldApprox == 0.75;
89     2.geometricCDF(0.5).shouldApprox == 0.875;
90 
91     geometricCDF(-1.0, 0.25).shouldApprox == 0; // UFCS chaining deduces this as size_t instead of a floating point type
92     0.geometricCDF(0.25).shouldApprox == 0.25;
93     1.geometricCDF(0.25).shouldApprox == 0.4375;
94     2.geometricCDF(0.25).shouldApprox == 0.578125;
95     2.5.geometricCDF(0.25).shouldApprox == 0.578125;
96 }
97 
98 /++
99 Computes the geometric complementary cumulative density function (CCDF).
100 
101 Params:
102     k = value to evaluate CCDF
103     p = `true` probability
104 
105 See_also: $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric Distribution)
106 +/
107 @safe pure @nogc nothrow
108 T geometricCCDF(T)(const size_t k, const T p)
109     if (isFloatingPoint!T)
110     in (p >= 0, "p must be greater than or equal to 0")
111     in (p <= 1, "p must be less than or equal to 1")
112 {
113     import mir.math.common: pow;
114     return pow(1 - p, k + 1);   
115 }
116 
117 /// ditto
118 @safe pure @nogc nothrow
119 T geometricCCDF(T)(const T x, const T p)
120     if (isFloatingPoint!T)
121     in (x >= -1, "x must be larger than or equal to -1")
122     in (p >= 0, "p must be greater than or equal to 0")
123     in (p <= 1, "p must be less than or equal to 1")
124 {
125     if (x < 0)
126         return 1;
127     return geometricCCDF!T(cast(const size_t) x, p);  
128 }
129 
130 ///
131 @safe pure @nogc nothrow
132 version(mir_stat_test)
133 unittest
134 {
135     import mir.test: shouldApprox;
136 
137     geometricCCDF(-1.0, 0.5).shouldApprox == 1.0; // UFCS chaining deduces this as size_t instead of a floating point type
138     0.geometricCCDF(0.5).shouldApprox == 0.5;
139     1.geometricCCDF(0.5).shouldApprox == 0.25;
140     2.geometricCCDF(0.5).shouldApprox == 0.125;
141 
142     geometricCCDF(-1.0, 0.25).shouldApprox == 1.0; // UFCS chaining deduces this as size_t instead of a floating point type
143     0.geometricCCDF(0.25).shouldApprox == 0.75;
144     1.geometricCCDF(0.25).shouldApprox == 0.5625;
145     2.geometricCCDF(0.25).shouldApprox == 0.421875;
146     2.5.geometricCCDF(0.25).shouldApprox == 0.421875;
147 }
148 
149 /++
150 Computes the geometric inverse cumulative distribution function (InvCDF).
151 
152 Params:
153     q = value to evaluate InvCDF
154     p = `true` probability
155 
156 See_also: $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric Distribution)
157 +/
158 @safe pure @nogc nothrow
159 T geometricInvCDF(T)(const T q, const T p)
160     if (isFloatingPoint!T)
161     in (q >= 0, "q must be greater than or equal to 0")
162     in (q <= 1, "q must be less than or equal to 1")
163     in (p >= 0, "p must be greater than or equal to 0")
164     in (p <= 1, "p must be less than or equal to 1")
165 {
166     import mir.math.common: approxEqual, ceil, nearbyint, log;
167     if (q == 1) {
168         return T.infinity;
169     }
170     if (p == 1) {
171         return 0;
172     }
173     T guess = log(1 - q) / log(1 - p);
174     T guessNearby = nearbyint(guess);
175     if (approxEqual(guess, guessNearby, T.epsilon * 2)) {
176         return guessNearby - 1;
177     } else {
178         return ceil(guess) - 1;
179     }
180 }
181 
182 ///
183 @safe pure @nogc nothrow
184 version(mir_stat_test)
185 unittest
186 {
187     import mir.test: should;
188 
189     0.geometricInvCDF(0.5).should == -1;
190     0.5.geometricInvCDF(0.5).should == 0;
191     0.75.geometricInvCDF(0.5).should == 1;
192     0.875.geometricInvCDF(0.5).should == 2;
193     0.95.geometricInvCDF(0.5).should == 4;
194 
195     0.geometricInvCDF(0.25).should == -1;
196     0.25.geometricInvCDF(0.25).should == 0;
197     0.4375.geometricInvCDF(0.25).should == 1;
198     0.578125.geometricInvCDF(0.25).should == 2;
199     0.95.geometricInvCDF(0.25).should == 10;
200 
201     0.5.geometricInvCDF(1).should == 0;
202     1.geometricInvCDF(0.5).should == double.infinity;
203 }
204 
205 /++
206 Computes the geometric log probability density function (LPMF).
207 
208 Params:
209     k = value to evaluate LPMF
210     p = `true` probability
211 
212 See_also: $(LINK2 https://en.wikipedia.org/wiki/Geometric_distribution, Geometric Distribution)
213 +/
214 @safe pure @nogc nothrow
215 T geometricLPMF(T)(const size_t k, const T p)
216     if (isFloatingPoint!T)
217     in (p >= 0, "p must be greater than or equal to 0")
218     in (p <= 1, "p must be less than or equal to 1")
219 {
220     import mir.math.common: log;
221     import mir.math.internal.xlogy: xlog1py;
222     return xlog1py(k, -p) + log(p);   
223 }
224 
225 ///
226 @safe pure @nogc nothrow
227 version(mir_stat_test)
228 unittest
229 {
230     import mir.math.common: exp;
231     import mir.test: shouldApprox;
232 
233     0.geometricLPMF(0.5).exp.shouldApprox == 0.geometricPMF(0.5);
234     1.geometricLPMF(0.5).exp.shouldApprox == 1.geometricPMF(0.5);
235     2.geometricLPMF(0.25).exp.shouldApprox == 2.geometricPMF(0.25);
236 }