ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
beta.cpp
Go to the documentation of this file.
1/**
2 * @file beta.cpp
3 * @brief Beta(α, β) family implementation on the unit interval:
4 * shapes @c α, @c β > 0.
5 *
6 * The conjugate prior of Bernoulli / binomial success probabilities and
7 * the workhorse of bounded-quantity modelling. The CDF is the
8 * regularised incomplete beta @f$I_x(\alpha, \beta)@f$ (continued
9 * fraction, file-local); the quantile rides the generic monotone-CDF
10 * bisection over the finite @c [0, 1] support, and truncated moments
11 * are closed-form through the moment-shifted incomplete beta.
12 * @c Beta(1, 1) is @c Uniform(0, 1) and the SQL constructor routes it
13 * there.
14 *
15 * Self-contained family implementation: the class is file-local and
16 * reaches the evaluators only through the registrars at the bottom.
17 */
18#include "DistributionCommon.h"
19
20#include <algorithm>
21#include <cmath>
22#include <memory>
23#include <optional>
24#include <random>
25#include <string>
26#include <utility>
27#include <vector>
28
29namespace provsql {
30
31namespace {
32
33/**
34 * @brief Regularised incomplete beta @f$I_x(a, b)@f$ for
35 * @c a, @c b > 0 and @c x in @c [0, 1].
36 *
37 * Modified Lentz continued fraction (Numerical Recipes §6.4), applied
38 * directly for @c x < (a+1)/(a+b+2) and through the symmetry
39 * @f$I_x(a,b) = 1 - I_{1-x}(b,a)@f$ otherwise so it always converges
40 * fast. NaN on invalid parameters or non-convergence, so callers fall
41 * through to Monte Carlo.
42 */
43double betaCF(double a, double b, double x)
44{
45 const double FPMIN = 1e-300;
46 const double qab = a + b, qap = a + 1.0, qam = a - 1.0;
47 double c = 1.0;
48 double d = 1.0 - qab * x / qap;
49 if (std::fabs(d) < FPMIN) d = FPMIN;
50 d = 1.0 / d;
51 double h = d;
52 for (int m = 1; m <= 500; ++m) {
53 const double m2 = 2.0 * m;
54 double aa = m * (b - m) * x / ((qam + m2) * (a + m2));
55 d = 1.0 + aa * d;
56 if (std::fabs(d) < FPMIN) d = FPMIN;
57 c = 1.0 + aa / c;
58 if (std::fabs(c) < FPMIN) c = FPMIN;
59 d = 1.0 / d;
60 h *= d * c;
61 aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
62 d = 1.0 + aa * d;
63 if (std::fabs(d) < FPMIN) d = FPMIN;
64 c = 1.0 + aa / c;
65 if (std::fabs(c) < FPMIN) c = FPMIN;
66 d = 1.0 / d;
67 const double del = d * c;
68 h *= del;
69 if (std::fabs(del - 1.0) < 1e-15) return h;
70 }
71 return kNaN;
72}
73
74double betaI(double a, double b, double x)
75{
76 if (!(a > 0.0) || !(b > 0.0) || std::isnan(x)) return kNaN;
77 if (x <= 0.0) return 0.0;
78 if (x >= 1.0) return 1.0;
79 const double lbeta =
80 std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
81 const double front =
82 std::exp(a * std::log(x) + b * std::log(1.0 - x) - lbeta);
83 double result;
84 if (x < (a + 1.0) / (a + b + 2.0))
85 result = front * betaCF(a, b, x) / a;
86 else
87 result = 1.0 - front * betaCF(b, a, 1.0 - x) / b;
88 return result;
89}
90
91/** @brief Beta(α=p1 > 0, β=p2 > 0) on [0, 1]. */
92class BetaDistribution final : public BaseDistribution {
93public:
95 const DistributionFamily &family() const override;
96 double mean() const override { return p1_ / (p1_ + p2_); }
97 double variance() const override {
98 const double s = p1_ + p2_;
99 return p1_ * p2_ / (s * s * (s + 1.0));
100 }
101 double rawMoment(unsigned k) const override {
102 if (k == 0) return 1.0;
103 // E[X^k] = prod_{i<k} (α+i)/(α+β+i), a rising-factorial ratio.
104 double r = 1.0;
105 for (unsigned i = 0; i < k; ++i)
106 r *= (p1_ + static_cast<double>(i))
107 / (p1_ + p2_ + static_cast<double>(i));
108 return r;
109 }
110 double pdf(double c) const override {
111 const double a = p1_, b = p2_;
112 if (!(a > 0.0) || !(b > 0.0)) return kNaN;
113 if (c < 0.0 || c > 1.0) return 0.0;
114 if (c == 0.0) {
115 /* Integrable-singularity convention (as Gamma / Weibull at 0):
116 * a shape below 1 diverges at the endpoint and reports NaN so
117 * the uniform-grid quadratures decline to MC. */
118 if (a < 1.0) return kNaN;
119 return (a == 1.0) ? b : 0.0;
120 }
121 if (c == 1.0) {
122 if (b < 1.0) return kNaN;
123 return (b == 1.0) ? a : 0.0;
124 }
125 const double lbeta =
126 std::lgamma(a) + std::lgamma(b) - std::lgamma(a + b);
127 return std::exp((a - 1.0) * std::log(c)
128 + (b - 1.0) * std::log(1.0 - c) - lbeta);
129 }
130 double cdf(double c) const override {
131 return betaI(p1_, p2_, c);
132 }
133 DistSupport support() const override { return {0.0, 1.0}; }
134 bool integrationRange(double &lo, double &hi) const override {
135 if (!(p1_ > 0.0 && p2_ > 0.0)) return false;
136 lo = 0.0;
137 hi = 1.0;
138 return true;
139 }
140 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
141 double lo = trunc_lo, hi = trunc_hi;
142 if (!std::isfinite(lo)) lo = 0.0;
143 if (!std::isfinite(hi)) hi = 1.0;
144 return {std::max(lo, 0.0), std::min(hi, 1.0)};
145 }
146 double sample(std::mt19937_64 &rng) const override {
147 /* No std::beta_distribution: the classic gamma-ratio construction,
148 * X = G_α / (G_α + G_β) with unit-scale gammas. */
149 std::gamma_distribution<double> ga(p1_, 1.0);
150 std::gamma_distribution<double> gb(p2_, 1.0);
151 const double x = ga(rng);
152 const double y = gb(rng);
153 return x / (x + y);
154 }
155 std::optional<double> truncatedRawMoment(double lo, double hi,
156 unsigned k) const override {
157 const double a = p1_, b = p2_;
158 if (!(a > 0.0) || !(b > 0.0)) return std::nullopt;
159 /* E[X^k · 1(lo<X<hi)] = (B(α+k, β)/B(α, β)) ·
160 * (I_hi(α+k, β) - I_lo(α+k, β)): the moment shifts α. */
161 const double x_lo = std::isfinite(lo) ? std::max(lo, 0.0) : 0.0;
162 const double x_hi = std::isfinite(hi) ? std::min(hi, 1.0) : 1.0;
163 const double mass = betaI(a, b, x_hi) - betaI(a, b, x_lo);
164 if (std::isnan(mass) || mass < 1e-12) return std::nullopt;
165 if (k == 0) return 1.0;
166 const double kd = static_cast<double>(k);
167 const double ratio = std::exp(
168 std::lgamma(a + kd) + std::lgamma(a + b)
169 - std::lgamma(a) - std::lgamma(a + b + kd));
170 const double shifted =
171 betaI(a + kd, b, x_hi) - betaI(a + kd, b, x_lo);
172 if (std::isnan(shifted)) return std::nullopt;
173 return ratio * shifted / mass;
174 }
175 std::string serialise() const override {
176 return "beta:" + double_to_text(p1_) + "," + double_to_text(p2_);
177 }
178 std::unique_ptr<Distribution> affine(double a, double b) const override {
179 /* A scaled or shifted Beta leaves the unit interval and the
180 * family. */
181 (void) a; (void) b;
182 return nullptr;
183 }
184};
185
186const DistributionFamily beta_family = {
187 "beta", 2, "Β", {"α", "β"},
188 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
189 return std::make_unique<BetaDistribution>(p1, p2);
190 }};
191
192const DistributionFamily &BetaDistribution::family() const
193{
194 return beta_family;
195}
196
197[[maybe_unused]] const DistributionFamilyRegistrar beta_family_registrar(
198 beta_family);
199
200} // namespace
201
202} // namespace provsql
Internal helpers shared by the per-family Distribution implementations under src/distributions/.
Base holding the two parameters; subclasses add closed forms.
BaseDistribution(double p1, double p2)
double lbeta(double a, double b)
ln B(a, b) = lnΓ(a) + lnΓ(b) − lnΓ(a+b), for a, b > 0.
std::string double_to_text(double v)
Format a double back into the canonical text form used by gate_value extras and gate_rv distribution ...
constexpr double kNaN
A registered family's descriptor: its complete identity.