ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
pareto.cpp
Go to the documentation of this file.
1/**
2 * @file pareto.cpp
3 * @brief Pareto(xₘ, α) family implementation: scale (minimum)
4 * @c xₘ > 0, shape @c α > 0.
5 *
6 * The canonical heavy-tailed power law (wealth, city / file sizes,
7 * insurance large losses). Raw moments are @b infinite for
8 * @c α <= @c j and reported as such (+Infinity, honestly) rather than
9 * estimated; the tail's self-similarity (X | X > a is
10 * Pareto(max(a, xₘ), α)) gives exact truncated moments and a
11 * rejection-free conditional sampler, and ln(X/xₘ) being Exp(α) gives
12 * a full cross-parameter comparator closed form -- important because
13 * the tail is exactly where a uniform quadrature grid fails.
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/** @brief Pareto(xₘ=p1 scale > 0, α=p2 shape > 0). */
34class ParetoDistribution final : public BaseDistribution {
35public:
37 const DistributionFamily &family() const override;
38 double mean() const override {
39 if (!(p2_ > 1.0)) return kInf; /* diverges for α <= 1 */
40 return p2_ * p1_ / (p2_ - 1.0);
41 }
42 double variance() const override {
43 if (!(p2_ > 2.0)) return kInf; /* diverges for α <= 2 */
44 const double am1 = p2_ - 1.0;
45 return p1_ * p1_ * p2_ / (am1 * am1 * (p2_ - 2.0));
46 }
47 double rawMoment(unsigned k) const override {
48 if (k == 0) return 1.0;
49 const double j = static_cast<double>(k);
50 if (!(p2_ > j)) return kInf; /* E[X^j] diverges for α <= j */
51 return p2_ * std::pow(p1_, j) / (p2_ - j);
52 }
53 double pdf(double c) const override {
54 const double xm = p1_, alpha = p2_;
55 if (!(xm > 0.0) || !(alpha > 0.0)) return kNaN;
56 if (c < xm) return 0.0;
57 return (alpha / xm) * std::pow(xm / c, alpha + 1.0);
58 }
59 double cdf(double c) const override {
60 const double xm = p1_, alpha = p2_;
61 if (!(xm > 0.0) || !(alpha > 0.0)) return kNaN;
62 if (c <= xm) return 0.0;
63 return 1.0 - std::pow(xm / c, alpha);
64 }
65 DistSupport support() const override { return {p1_, kInf}; }
66 bool integrationRange(double &lo, double &hi) const override {
67 if (!(p1_ > 0.0 && p2_ > 0.0)) return false;
68 /* quantile(1 - 1e-9). The power-law tail means the window is
69 * xₘ·10^{9/α} -- huge for small α -- so a uniform grid resolves it
70 * poorly; the full cross-parameter comparator closed form below
71 * keeps Pareto-vs-Pareto off that path. */
72 lo = p1_;
73 hi = p1_ * std::pow(1e9, 1.0 / p2_);
74 return true;
75 }
76 std::pair<double, double> plotRange(double trunc_lo, double trunc_hi) const override {
77 double lo = trunc_lo, hi = trunc_hi;
78 if (!std::isfinite(lo)) lo = p1_;
79 if (!std::isfinite(hi))
80 hi = p1_ * std::pow(1e3, 1.0 / p2_); /* q(0.999) */
81 return {lo, hi};
82 }
83 double sample(std::mt19937_64 &rng) const override {
84 /* Inverse CDF of a uniform draw (no std::pareto_distribution). */
85 std::uniform_real_distribution<double> U01(0.0, 1.0);
86 return p1_ * std::pow(1.0 - U01(rng), -1.0 / p2_);
87 }
88 std::optional<double> quantile(double p) const override {
89 if (!(p1_ > 0.0) || !(p2_ > 0.0)) return std::nullopt;
90 return p1_ * std::pow(1.0 - p, -1.0 / p2_);
91 }
92 std::optional<double> truncatedRawMoment(double lo, double hi,
93 unsigned j) const override {
94 const double xm = p1_, alpha = p2_;
95 if (!(xm > 0.0) || !(alpha > 0.0)) return std::nullopt;
96 const double a = std::max(std::isfinite(lo) ? lo : xm, xm);
97 if (std::isfinite(hi) && hi <= a) return std::nullopt;
98 const double mass = std::pow(xm / a, alpha)
99 - (std::isfinite(hi) ? std::pow(xm / hi, alpha) : 0.0);
100 if (mass < 1e-12) return std::nullopt;
101 if (j == 0) return 1.0;
102 const double jd = static_cast<double>(j);
103 if (jd == alpha) return std::nullopt; /* logarithmic edge case */
104 if (!std::isfinite(hi) && !(alpha > jd))
105 return kInf; /* honestly divergent */
106 /* ∫_a^b x^j α xₘ^α x^{-α-1} dx
107 * = α xₘ^α (b^{j-α} - a^{j-α}) / (j - α). */
108 const double upper =
109 std::isfinite(hi) ? std::pow(hi, jd - alpha) : 0.0;
110 return alpha * std::pow(xm, alpha)
111 * (upper - std::pow(a, jd - alpha)) / (jd - alpha) / mass;
112 }
113 std::optional<std::vector<double>> sampleTruncated(
114 std::mt19937_64 &rng, double lo, double hi, unsigned n) const override {
115 const double xm = p1_, alpha = p2_;
116 if (!(xm > 0.0) || !(alpha > 0.0)) return std::nullopt;
117 const double a = std::max(std::isfinite(lo) ? lo : xm, xm);
118 std::vector<double> out;
119 out.reserve(n);
120 std::uniform_real_distribution<double> U01(0.0, 1.0);
121 if (std::isinf(hi)) {
122 /* Self-similarity: X | X > a is Pareto(a, α). */
123 for (unsigned i = 0; i < n; ++i)
124 out.push_back(a * std::pow(1.0 - U01(rng), -1.0 / alpha));
125 return out;
126 }
127 if (hi <= a) return std::nullopt;
128 /* Two-sided: exact inverse CDF on [F(a), F(hi)]. */
129 const double F_a = 1.0 - std::pow(xm / a, alpha);
130 const double F_b = 1.0 - std::pow(xm / hi, alpha);
131 if (!(F_a < F_b)) return std::nullopt;
132 for (unsigned i = 0; i < n; ++i) {
133 const double u = F_a + U01(rng) * (F_b - F_a);
134 out.push_back(xm * std::pow(1.0 - u, -1.0 / alpha));
135 }
136 return out;
137 }
138 std::optional<double> iidOrderStatMean(std::size_t n,
139 bool isMax) const override {
140 if (!(p1_ > 0.0) || !(p2_ > 0.0)) return std::nullopt;
141 if (isMax) return std::nullopt; /* no elementary max closed form */
142 /* Min-stability: the min of n i.i.d. Pareto(xₘ, α) is
143 * Pareto(xₘ, nα). */
144 const double na = static_cast<double>(n) * p2_;
145 if (!(na > 1.0)) return kInf;
146 return na * p1_ / (na - 1.0);
147 }
148 std::unique_ptr<Distribution> affine(double a, double b) const override {
149 /* Positive scalings rescale xₘ; negations / offsets leave the
150 * family. */
151 if (!(a > 0.0) || b != 0.0) return nullptr;
152 return std::make_unique<ParetoDistribution>(a * p1_, p2_);
153 }
154 std::string serialise() const override {
155 return "pareto:" + double_to_text(p1_) + "," + double_to_text(p2_);
156 }
157};
158
159const DistributionFamily pareto_family = {
160 "pareto", 2, "Par", {"xₘ", "α"},
161 +[](double p1, double p2) -> std::unique_ptr<Distribution> {
162 return std::make_unique<ParetoDistribution>(p1, p2);
163 }};
164
165const DistributionFamily &ParetoDistribution::family() const
166{
167 return pareto_family;
168}
169
170/* P(X < Y) for independent Paretos, exact for ANY parameters (this is
171 * the pair whose heavy tails the generic quadrature handles worst).
172 * With xₘX <= xₘY:
173 * P(X < Y) = 1 - (α_Y / (α_X + α_Y)) · (xₘX / xₘY)^{α_X}
174 * (integrate F_X over Y's density; same-scale reduces to the
175 * exponential ratio α_X/(α_X+α_Y) through ln(X/xₘ) ~ Exp(α));
176 * the xₘX > xₘY case is the mirror complement. */
177double paretoPairLess(const Distribution &X, const Distribution &Y)
178{
179 const double xmx = X.p1(), ax = X.p2();
180 const double xmy = Y.p1(), ay = Y.p2();
181 if (!(xmx > 0.0) || !(ax > 0.0) || !(xmy > 0.0) || !(ay > 0.0))
182 return kNaN;
183 if (xmx <= xmy)
184 return 1.0 - (ay / (ax + ay)) * std::pow(xmx / xmy, ax);
185 return (ax / (ax + ay)) * std::pow(xmy / xmx, ay);
186}
187
188[[maybe_unused]] const ComparatorRuleRegistrar pareto_less_rule(
189 "pareto", "pareto", &paretoPairLess);
190
191/* Gamma-Pareto conjugacy: a Pareto(xₘ₀, θ) observation with the latent
192 * in the SHAPE slot (xₘ₀ a known literal scale) updates a Gamma(k, λ)
193 * prior to Gamma(k+1, λ + ln(d/xₘ₀)) -- the likelihood
194 * f(d|α) = (α/d)·e^{−α ln(d/xₘ₀)} is a gamma kernel in α. The
195 * predictive is m(d) = (k/d) · λ^k / (λ + ln(d/xₘ₀))^{k+1}. */
196bool paretoShapeConjugateUpdate(double &k, double &lambda,
197 const DistributionTemplate &lik, double d)
198{
199 const double xm0 = lik.p1.literal;
200 if (!(k > 0.0) || !(lambda > 0.0) || !(xm0 > 0.0) || !(d >= xm0))
201 return false;
202 k += 1.0;
203 lambda += std::log(d / xm0);
204 return true;
205}
206
207double paretoShapeLogPredictive(double k, double lambda,
208 const DistributionTemplate &lik, double d)
209{
210 const double xm0 = lik.p1.literal;
211 if (!(k > 0.0) || !(lambda > 0.0) || !(xm0 > 0.0) || !(d >= xm0))
212 return kNaN;
213 const double t = std::log(d / xm0);
214 return std::log(k) - std::log(d) + k * std::log(lambda)
215 - (k + 1.0) * std::log(lambda + t);
216}
217
218[[maybe_unused]] const ConjugateRuleRegistrar pareto_shape_conjugate(
219 "pareto", 1, "gamma",
220 {&paretoShapeConjugateUpdate, &paretoShapeLogPredictive});
221
222[[maybe_unused]] const DistributionFamilyRegistrar pareto_family_registrar(
223 pareto_family);
224
225} // namespace
226
227} // 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)
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.