ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
negative_binomial.cpp
Go to the documentation of this file.
1/**
2 * @file negative_binomial.cpp
3 * @brief NegativeBinomial(r, p) family implementation (discrete).
4 *
5 * Number of failures before the r-th success, support @c {0, 1, 2, ...},
6 * @c pmf(k) = C(k+r-1, k) p^r (1-p)^k (r real > 0 via the gamma form) -- the
7 * same convention as the literal @c provsql.negative_binomial constructor.
8 * Only ever instantiated for a @b latent leaf
9 * @c provsql.negative_binomial(integer, random_variable); reached by
10 * @c sample(), by @c observe (weight = pmf at the datum), and the moment
11 * readouts. Self-contained: registered at the bottom.
12 */
13#include "DistributionCommon.h"
14
15#include <cmath>
16#include <memory>
17#include <optional>
18#include <random>
19#include <string>
20#include <utility>
21
22namespace provsql {
23
24namespace {
25
26/** @brief NegativeBinomial(r=p1, p=p2) on {0, 1, ...} (failures). */
27class NegativeBinomialDistribution final : public BaseDistribution {
28public:
30 const DistributionFamily &family() const override;
31
32 bool valid() const { return p1_ > 0.0 && p2_ > 0.0 && p2_ <= 1.0; }
33
34 /// log pmf(k) = lgamma(k+r) - lgamma(r) - lgamma(k+1) + r ln p + k ln(1-p).
35 double logpmf(long k) const {
36 const double r = p1_;
37 return std::lgamma(k + r) - std::lgamma(r) - std::lgamma(k + 1.0)
38 + r * std::log(p2_) + k * std::log(1.0 - p2_);
39 }
40
41 double mean() const override { return p1_ * (1.0 - p2_) / p2_; } // r(1-p)/p
42 bool isDiscrete() const override { return true; }
43 double variance() const override {
44 return p1_ * (1.0 - p2_) / (p2_ * p2_);
45 }
46 double rawMoment(unsigned k) const override {
47 if (k == 0) return 1.0;
48 if (!valid()) return kNaN;
49 // Σ_{j>=0} j^k pmf(j), truncated once the cumulative mass is essentially 1
50 // past the mean; the cap bounds a pathologically heavy tail.
51 double total = 0.0, cum = 0.0;
52 const double mu = mean();
53 for (long j = 0; j <= 100000000L; ++j) {
54 const double pj = std::exp(logpmf(j));
55 total += std::pow(static_cast<double>(j), static_cast<double>(k)) * pj;
56 cum += pj;
57 if (j > static_cast<long>(mu) && cum > 1.0 - 1e-15) break;
58 }
59 return total;
60 }
61 double pdf(double c) const override { // pmf at an integer >= 0
62 if (!valid()) return kNaN;
63 const double r = std::nearbyint(c);
64 if (std::fabs(c - r) > 1e-9 || r < 0.0) return 0.0;
65 return std::exp(logpmf(static_cast<long>(r)));
66 }
67 double cdf(double c) const override {
68 if (!valid()) return kNaN;
69 if (c < 0.0) return 0.0;
70 const long kmax = static_cast<long>(std::floor(c));
71 double s = 0.0;
72 for (long k = 0; k <= kmax; ++k) s += std::exp(logpmf(k));
73 return s > 1.0 ? 1.0 : s;
74 }
75 DistSupport support() const override { return {0.0, kInf}; }
76 bool integrationRange(double &lo, double &hi) const override {
77 if (!valid()) return false;
78 lo = 0.0;
79 hi = mean() + 12.0 * std::sqrt(variance()) + 30.0;
80 return true;
81 }
82 std::pair<double, double> plotRange(double trunc_lo,
83 double trunc_hi) const override {
84 double lo = trunc_lo, hi = trunc_hi;
85 if (!std::isfinite(lo)) lo = 0.0;
86 if (!std::isfinite(hi)) hi = mean() + 4.0 * std::sqrt(variance()) + 5.0;
87 return {lo, hi};
88 }
89 double sample(std::mt19937_64 &rng) const override {
90 // Gamma-Poisson mixture (valid for real r): λ ~ Gamma(r, scale=(1-p)/p),
91 // then draw Poisson(λ). Reproduces NegativeBinomial(r, p) for any r > 0.
92 std::gamma_distribution<double> g(p1_, (1.0 - p2_) / p2_);
93 const double lambda = g(rng);
94 std::poisson_distribution<long> po(lambda > 0.0 ? lambda : 1e-12);
95 return static_cast<double>(po(rng));
96 }
97 std::optional<double> quantile(double p) const override {
98 if (!valid()) return std::nullopt;
99 if (p <= 0.0) return 0.0;
100 double s = 0.0;
101 for (long k = 0; k <= 100000000L; ++k) {
102 s += std::exp(logpmf(k));
103 if (s >= p) return static_cast<double>(k);
104 }
105 return std::nullopt;
106 }
107 std::unique_ptr<Distribution> affine(double, double) const override {
108 return nullptr; // discrete: not closed under a·X + b
109 }
110 std::string serialise() const override {
111 return "negative_binomial:" + double_to_text(p1_) + "," + double_to_text(p2_);
112 }
113};
114
115const DistributionFamily negative_binomial_family = {
116 "negative_binomial", 2, "NB", {"r", "p"},
117 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
118 return std::make_unique<NegativeBinomialDistribution>(p1, p2);
119 }};
120
121const DistributionFamily &NegativeBinomialDistribution::family() const
122{
123 return negative_binomial_family;
124}
125
126/* Beta-NegativeBinomial conjugacy, in the family's FAILURES convention
127 * (support {0, 1, ...}, pmf Γ(d+r)/(Γ(r) d!) p^r (1−p)^d): one
128 * observation of d failures before the r-th success (r a known literal)
129 * updates a Beta(α, β) prior to Beta(α+r, β+d). The predictive is
130 * m(d) = Γ(d+r)/(Γ(r) d!) · B(α+r, β+d) / B(α, β). */
131bool negativeBinomialPConjugateUpdate(double &alpha, double &beta,
132 const DistributionTemplate &lik,
133 double d)
134{
135 const double r = lik.p1.literal;
136 if (!(alpha > 0.0) || !(beta > 0.0) || !(r > 0.0)) return false;
137 const double f = std::nearbyint(d);
138 if (std::fabs(d - f) > 1e-9 || f < 0.0) return false;
139 alpha += r;
140 beta += f;
141 return true;
142}
143
144double negativeBinomialPLogPredictive(double alpha, double beta,
145 const DistributionTemplate &lik,
146 double d)
147{
148 const double r = lik.p1.literal;
149 if (!(alpha > 0.0) || !(beta > 0.0) || !(r > 0.0)) return kNaN;
150 const double f = std::nearbyint(d);
151 if (std::fabs(d - f) > 1e-9 || f < 0.0) return kNaN;
152 return std::lgamma(f + r) - std::lgamma(r) - std::lgamma(f + 1.0)
153 + lbeta(alpha + r, beta + f) - lbeta(alpha, beta);
154}
155
156[[maybe_unused]] const ConjugateRuleRegistrar negative_binomial_p_conjugate(
157 "negative_binomial", 1, "beta",
158 {&negativeBinomialPConjugateUpdate, &negativeBinomialPLogPredictive});
159
160[[maybe_unused]] const DistributionFamilyRegistrar
161 negative_binomial_family_registrar(negative_binomial_family);
162
163} // namespace
164
165} // 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.
constexpr double kInf
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.