diff --git a/src/Distributions.jl b/src/Distributions.jl index b18dfa8086..4c7deac44e 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -170,6 +170,7 @@ export TriangularDist, Triweight, Truncated, + Tweedie, Uniform, UnivariateGMM, VonMises, @@ -363,7 +364,7 @@ Supported distributions: NoncentralF, NoncentralHypergeometric, NoncentralT, Normal, NormalCanon, NormalInverseGaussian, Pareto, PGeneralizedGaussian, Poisson, PoissonBinomial, QQPair, Rayleigh, Rician, Skellam, Soliton, StudentizedRange, SymTriangularDist, TDist, TriangularDist, - Triweight, Truncated, Uniform, UnivariateGMM, + Triweight, Truncated, Tweedie, Uniform, UnivariateGMM, VonMises, VonMisesFisher, WalleniusNoncentralHypergeometric, Weibull, Wishart, ZeroMeanIsoNormal, ZeroMeanIsoNormalCanon, ZeroMeanDiagNormal, ZeroMeanDiagNormalCanon, ZeroMeanFullNormal, diff --git a/src/univariate/continuous/tweedie.jl b/src/univariate/continuous/tweedie.jl new file mode 100644 index 0000000000..6e559f290c --- /dev/null +++ b/src/univariate/continuous/tweedie.jl @@ -0,0 +1,248 @@ +""" + Tweedie(μ,σ,p) + +The *Tweedie distribution* with mean `μ ≥ 0`, dispersion `σ ≥ 0` and power `1 ≥ p ≥ 2`. +When ``p = 1`` and ``\\sigma = 1`` it is equivalent to a quasi-Poisson distribution, +and when ``p = 2`` to the Gamma distribution. When ``1 > p > 2``, it is a compound +Poisson-Gamma distribution, with probability density function: + +```math +f(x; \\mu, \\sigma, p) = \\frac{1}{x} W(x, \\sigma^2, p) exp \\left( + \\frac{1}{\\sigma^2}} [ x \\frac{\\mu^(1-p)}{1-p} - \\frac{\\mu^(2-p)}{2-p} ] + \\right), \\quad x > 0 +``` +where ``W`` is [Wright's generalized Bessel function](https://en.wikipedia.org/wiki/Bessel%E2%80%93Maitland_function). + +Note that if ``1 > p > 2`` then the distribution is continuous with a point mass concentrated at zero. +If ``p = 1`` then the distribution is discrete. + +Computation of [`pdf`](@ref) and [`logpdf`](@ref) is carried out using `Float64`. +Accuracy is generally higher than 1e-11, though for some parameter values it can +be as low as 1e-8. + +```julia +Tweedie(μ, σ, p) # Tweedie distribution with location μ, scale σ and power p + +params(d) # Get the parameters, i.e. (μ, σ, p) +location(d) # Get the location parameter, i.e. μ +scale(d) # Get the scale parameter, i.e. σ +shape(d) # Get the shape parameter, i.e. p + +mean(d) # Get the mean, i.e. μ +var(d) # Get the variance, i.e. σ^2 * μ^p +``` + +External links + +- [Tweedie distribution on Wikipedia](https://en.wikipedia.org/wiki/Tweedie_distribution) +- [Compound Poisson distribution on Wikipedia](https://en.wikipedia.org/wiki/Compound_Poisson_distribution) + +References + +- Dunn P. K., Smyth G. K. (2005). "Series evaluation of Tweedie exponential dispersion model densities" + *Statistics and Computing* 15: 267–280. +""" +struct Tweedie{T <: Real} <: ContinuousUnivariateDistribution + μ::T + σ::T + p::T + + Tweedie{T}(µ::T, σ::T, p::T) where {T<:Real} = new{T}(µ, σ, p) +end + +function Tweedie(μ::T, σ::T, p::T; check_args::Bool=true) where {T <: Real} + @check_args( + Tweedie, + (μ, μ >= 0), + (σ, σ >= 0), + (p, 1 <= p <= 2) + ) + return Tweedie{T}(μ, σ, p) +end + +#### Outer constructors +Tweedie(μ::Real, σ::Real, p::Real; check_args::Bool=true) = + Tweedie(promote(μ, σ, p)...; check_args=check_args) +Tweedie(μ::Integer, σ::Integer, p::Integer; check_args::Bool=true) = + Tweedie(float(μ), float(σ), float(p); check_args=check_args) + +#### Conversions +convert(::Type{Tweedie{T}}, μ::S, σ::S, p::S) where {T <: Real, S <: Real} = Tweedie(T(μ), T(σ), T(p)) +Base.convert(::Type{Tweedie{T}}, d::Tweedie) where {T<:Real} = Tweedie{T}(T(d.μ), T(d.σ), T(d.p)) +Base.convert(::Type{Tweedie{T}}, d::Tweedie{T}) where {T<:Real} = d + +@distr_support Tweedie 0 Inf + +#### Parameters + +params(d::Tweedie) = (d.μ, d.σ, d.p) +partype(::Tweedie{T}) where {T} = T + +location(d::Tweedie) = d.μ +scale(d::Tweedie) = d.σ +shape(d::Tweedie) = d.p + +Base.eltype(::Type{Tweedie{T}}) where {T} = float(T) + +#### Statistics + +mean(d::Tweedie) = float(d.μ) +var(d::Tweedie) = d.σ^2 * d.μ^d.p +std(d::Tweedie) = d.σ * d.μ^(d.p/2) + +# Clark, David R. and Charles A. Thayer. 2004. +# “A Primer on the Exponential Family of Distributions.” CAS Discussion Paper Program, 117-148 +# https://www.casact.org/sites/default/files/database/dpp_dpp04_04dpp117.pdf +skewness(d::Tweedie) = d.p * d.σ / sqrt(d.μ ^ (2 - d.p)) +kurtosis(d::Tweedie) = d.p * (2 * d.p - 1) * d.σ^2 / d.μ ^ (2 - d.p) + +function logpdf(d::Tweedie{T}, x::Real)::promote_type(eltype(d), typeof(x)) where {T <: Real} + isnan(x) && return NaN + x >= 0 || return -Inf + # See: Dunn, Smyth (2005). "Series evaluation of Tweedie exponential dispersion model densities" + # Statistics and Computing 15: 267–280. + # pdf(y, μ, p, ϕ) = f(y, θ, ϕ) = c(y, ϕ) * exp(1/ϕ (y θ - κ(θ))) + # κ = cumulant function + # θ = function of expectation μ and power p + # α = (2-p)/(1-p) + # ϕ = σ^2 + # y = x + # for 1
0
+ z = ((p - 1) * ϕ / x) ^ α / ((2 - p) * ϕ)
+ # Use log to reduce risks of overflow when p is close to 1
+ wb = logwrightbessel(Float64(-α), 0.0, Float64(z))
+ # Overflow in `logwrightbessel` doesn't generally indicate that the PDF
+ # value would be larger than `typemax(Float64)`
+ wb == Inf && return NaN
+ res += wb - log(x)
+ end
+ return res
+ end
+end
+
+function cdf(d::Tweedie, x::Real)::promote_type(eltype(d), typeof(x))
+ isnan(x) && return NaN
+ x == Inf && return 1
+ x >= 0 || return 0
+ μ = d.μ
+ p = d.p
+ ϕ = d.σ^2
+ if p == 1
+ return cdf(Poisson(μ / ϕ), x / ϕ)
+ elseif p == 2
+ return cdf(Gamma(1 / ϕ, μ * ϕ), x)
+ else
+ # the mass at zero has to be handled separately as `quadgk` never evaluates at bounds
+ return pdf(d, 0) + quadgk(xi -> pdf(d, xi), 0, x, rtol=1e-12)[1]
+ end
+end
+
+function rand(rng::AbstractRNG, d::Tweedie)
+ μ, p, ϕ = d.μ, d.p, d.σ^2
+ # note that sources often use β = 1/θ for Gamma distribution
+ # e.g. https://en.wikipedia.org/wiki/Compound_Poisson_distribution
+ if p == 1
+ return ϕ * rand(rng, Poisson(μ / ϕ))
+ elseif p == 2
+ return rand(rng, Gamma(1 / ϕ, μ * ϕ))
+ else
+ λ = μ^(2 - p) / ((2 - p) * ϕ)
+ α = (2 - p) / (1 - p)
+ θ = ((p - 1) * ϕ) / μ^(1 - p)
+ N = rand(rng, Poisson(λ))
+ return N == 0 ? zero(θ) : rand(rng, Gamma(- N * α, θ))
+ end
+end
+
+# Implementation inspired by `qtweedie` in R package tweedie
+# licensed under MIT with authorization from Peter Dunn
+function quantile(d::Tweedie{T}, q::Real)::eltype(d) where {T <: Real}
+ μ, ϕ, p = d.μ, d.σ^2, d.p
+
+ if q == 0
+ return zero(T)
+ elseif q == 1
+ return convert(T, Inf)
+ elseif q < 0 || q > 1
+ throw(DomainError(q, "q must be between 0 and 1"))
+ end
+
+ if p == 1
+ return ϕ * quantile(Poisson(μ / ϕ), q)
+ elseif p == 2
+ return quantile(Gamma(1 / ϕ, μ * ϕ), q)
+ else
+ # Handle point mass at zero
+ p_zero = pdf(d, 0)
+ if q <= p_zero
+ return zero(T)
+ end
+
+ # Starting values via interpolation between Poisson and Gamma quantiles
+ qp = ϕ * quantile(Poisson(μ / ϕ), q)
+ qg = quantile(Gamma(1 / ϕ, μ * ϕ), q)
+ startx = (qg - qp) * p + (2 * qp - qg)
+
+ qstart = cdf(d, startx)
+ rx = lx = startx
+ if qstart == q
+ return startx
+ elseif qstart > q
+ while true
+ lx = lx / 2
+ cdf(d, lx) < q && break
+ end
+ elseif qstart < q
+ while true
+ rx = 1.5 * (rx + 2)
+ cdf(d, rx) > q && break
+ end
+ end
+
+ # Cannot use `quantile_newton` as pdf is sometimes multimodal
+ return quantile_bisect(d, q, lx, rx)
+ end
+end
+
+function cquantile(d::Tweedie, q::Real)
+ 0 <= q <= 1 || throw(DomainError(q, "q must be between 0 and 1"))
+ cq = 1.0 - q
+ # Allow for 1 eps tolerance as due to the mass at zero
+ # if `1 - q` is rounded up when storing in floating point,
+ # `cquantile(d, ccdf(d, 0))` can be very different from zero,
+ # which doesn't make mathematical sense
+ if d.p < 2 && cq <= nextfloat(pdf(d, 0))
+ return zero(eltype(d))
+ else
+ return quantile(d, cq)
+ end
+end
+
+function invlogccdf(d::Tweedie, lp::Real)
+ p = -expm1(lp)
+ # Allow for 1 eps tolerance as due to the mass at zero
+ # if `1 - q` is rounded up when storing in floating point,
+ # `invlogccdf(d, logccdf(d, 0))` can be very different from zero,
+ # which doesn't make mathematical sense
+ if d.p < 2 && p <= nextfloat(pdf(d, 0))
+ return zero(eltype(d))
+ else
+ return quantile(d, p)
+ end
+end
\ No newline at end of file
diff --git a/src/univariates.jl b/src/univariates.jl
index 36fc5a214d..86337c9d49 100644
--- a/src/univariates.jl
+++ b/src/univariates.jl
@@ -733,6 +733,7 @@ const continuous_distributions = [
"tdist",
"triangular",
"triweight",
+ "tweedie",
"uniform",
"loguniform", # depends on Uniform
"vonmises",
diff --git a/test/ref/continuous/tweedie.R b/test/ref/continuous/tweedie.R
new file mode 100644
index 0000000000..20a8bb29a1
--- /dev/null
+++ b/test/ref/continuous/tweedie.R
@@ -0,0 +1,32 @@
+# Tweedie Distribution
+# Using R's tweedie package for reference implementation
+
+Tweedie <- R6Class("Tweedie",
+ inherit = ContinuousDistribution,
+ public = list(
+ names = c("mu", "sigma", "p"),
+ mu = NA,
+ sigma = NA,
+ p = NA,
+ initialize = function(mu, sigma, p) {
+ self$mu <- mu
+ self$sigma <- sigma
+ self$p <- p
+ },
+ supp = function() { c(0, Inf) },
+ pdf = function(x, log=FALSE) {
+ val <- tweedie::dtweedie(x, mu=self$mu, phi=self$sigma^2, power=self$p)
+ if (log) {
+ return(log(val))
+ } else {
+ return(val)
+ }
+ },
+ cdf = function(x) {
+ tweedie::ptweedie(x, mu=self$mu, phi=self$sigma^2, power=self$p)
+ },
+ quan = function(v) {
+ tweedie::qtweedie(v, mu=self$mu, phi=self$sigma^2, power=self$p)
+ }
+ )
+)
diff --git a/test/ref/continuous_test.lst b/test/ref/continuous_test.lst
index a0a5418314..d0d206e292 100644
--- a/test/ref/continuous_test.lst
+++ b/test/ref/continuous_test.lst
@@ -209,6 +209,12 @@ TruncatedNormal(27, 3, 0, Inf)
TruncatedNormal(-5, 1, -Inf, -10)
TruncatedNormal(1.8, 1.2, -Inf, 0)
+Tweedie(2.0, 1.5, 1)
+Tweedie(2.0, 1.5, 1.01)
+Tweedie(2.0, 1.5, 1.5)
+Tweedie(2.0, 1.5, 1.99)
+Tweedie(2.0, 1.5, 2.0)
+
Uniform()
Uniform(0.0, 2.0)
Uniform(3.0, 17.0)
diff --git a/test/ref/continuous_test.ref.json b/test/ref/continuous_test.ref.json
index 7f8fb4b182..f262c437a3 100644
--- a/test/ref/continuous_test.ref.json
+++ b/test/ref/continuous_test.ref.json
@@ -5457,6 +5457,136 @@
{ "q": 0.90, "x": -0.0644552402816942 }
]
},
+{
+ "expr": "Tweedie(2.0, 1.5, 1)",
+ "dtype": "Tweedie",
+ "minimum": 0,
+ "maximum": "inf",
+ "properties": {
+ },
+ "points": [
+ { "x": 0, "pdf": 0.411112290507187, "logpdf": -0.888888888888889, "cdf": 0.411112290507187 },
+ { "x": 0, "pdf": 0.411112290507187, "logpdf": -0.888888888888889, "cdf": 0.411112290507187 },
+ { "x": 0, "pdf": 0.411112290507187, "logpdf": -0.888888888888889, "cdf": 0.411112290507187 },
+ { "x": 0, "pdf": 0.411112290507187, "logpdf": -0.888888888888889, "cdf": 0.411112290507187 },
+ { "x": 2.25, "pdf": 0.3654331471175, "logpdf": -1.00667192454527, "cdf": 0.776545437624687 },
+ { "x": 2.25, "pdf": 0.3654331471175, "logpdf": -1.00667192454527, "cdf": 0.776545437624687 },
+ { "x": 2.25, "pdf": 0.3654331471175, "logpdf": -1.00667192454527, "cdf": 0.776545437624687 },
+ { "x": 4.5, "pdf": 0.162414732052222, "logpdf": -1.8176021407616, "cdf": 0.93896016967691 },
+ { "x": 4.5, "pdf": 0.162414732052222, "logpdf": -1.8176021407616, "cdf": 0.93896016967691 }
+ ],
+ "quans": [
+ { "q": 0.10, "x": -0 },
+ { "q": 0.25, "x": 0 },
+ { "q": 0.50, "x": 2.25 },
+ { "q": 0.75, "x": 2.25 },
+ { "q": 0.90, "x": 4.5 }
+ ]
+},
+{
+ "expr": "Tweedie(2.0, 1.5, 1.01)",
+ "dtype": "Tweedie",
+ "minimum": 0,
+ "maximum": "inf",
+ "properties": {
+ },
+ "points": [
+ { "x": 0, "pdf": 0.409972358341713, "logpdf": -0.891665540235274, "cdf": 0.409972358341713 },
+ { "x": 0, "pdf": 0.409972358341713, "logpdf": -0.891665540235274, "cdf": 0.409972358341713 },
+ { "x": 0, "pdf": 0.409972358341713, "logpdf": -0.891665540235274, "cdf": 0.409972358341713 },
+ { "x": 0, "pdf": 0.409972358341713, "logpdf": -0.891665540235274, "cdf": 0.409972358341713 },
+ { "x": 2.08458174204291, "pdf": 0.536709862522414, "logpdf": -0.622297623738618, "cdf": 0.499999999999887 },
+ { "x": 2.24665165372521, "pdf": 0.64524670586641, "logpdf": -0.438122545601544, "cdf": 0.599999999999865 },
+ { "x": 2.42462213545382, "pdf": 0.439428418580859, "logpdf": -0.822280445244871, "cdf": 0.7 },
+ { "x": 4.15658364531784, "pdf": 0.12549162469383, "logpdf": -2.07551625814455, "cdf": 0.8 },
+ { "x": 4.71112894150918, "pdf": 0.152496114134358, "logpdf": -1.8806161643455, "cdf": 0.9 }
+ ],
+ "quans": [
+ { "q": 0.10, "x": 0 },
+ { "q": 0.25, "x": 0 },
+ { "q": 0.50, "x": 2.08458174204291 },
+ { "q": 0.75, "x": 2.58441784358517 },
+ { "q": 0.90, "x": 4.71112894150918 }
+ ]
+},
+{
+ "expr": "Tweedie(2.0, 1.5, 1.5)",
+ "dtype": "Tweedie",
+ "minimum": 0,
+ "maximum": "inf",
+ "properties": {
+ },
+ "points": [
+ { "x": 0, "pdf": 0.284483870247908, "logpdf": -1.25707872210942, "cdf": 0.284483870247908 },
+ { "x": 0, "pdf": 0.284483870247908, "logpdf": -1.25707872210942, "cdf": 0.284483870247908 },
+ { "x": 0.0695940988351711, "pdf": 0.221126951073659, "logpdf": -1.50901830314413, "cdf": 0.3 },
+ { "x": 0.548859776983353, "pdf": 0.196301027378934, "logpdf": -1.62810594399496, "cdf": 0.400000000000001 },
+ { "x": 1.09635850238778, "pdf": 0.169308648560591, "logpdf": -1.77603190691513, "cdf": 0.5 },
+ { "x": 1.74368937890802, "pdf": 0.140219032553443, "logpdf": -1.96454956071761, "cdf": 0.6 },
+ { "x": 2.54916644046212, "pdf": 0.10903389348092, "logpdf": -2.21609649572149, "cdf": 0.7 },
+ { "x": 3.6418146049751, "pdf": 0.0756550324262397, "logpdf": -2.581571318472, "cdf": 0.8 },
+ { "x": 5.42635624081533, "pdf": 0.0397731565622146, "logpdf": -3.22456305247684, "cdf": 0.9 }
+ ],
+ "quans": [
+ { "q": 0.10, "x": 0 },
+ { "q": 0.25, "x": 0 },
+ { "q": 0.50, "x": 1.09635850238778 },
+ { "q": 0.75, "x": 3.04590854984609 },
+ { "q": 0.90, "x": 5.42635624081533 }
+ ]
+},
+{
+ "expr": "Tweedie(2.0, 1.5, 1.99)",
+ "dtype": "Tweedie",
+ "minimum": 0,
+ "maximum": "inf",
+ "properties": {
+ },
+ "points": [
+ { "x": 0.0180116841854315, "pdf": 2.36802160266867, "logpdf": 0.862054839728742, "cdf": 0.100000000000001 },
+ { "x": 0.0909089850716691, "pdf": 0.942832617388424, "logpdf": -0.0588665122220102, "cdf": 0.2 },
+ { "x": 0.235225094656205, "pdf": 0.539396455646777, "logpdf": -0.617304439216196, "cdf": 0.3 },
+ { "x": 0.468504550181304, "pdf": 0.350419323103255, "logpdf": -1.04862477559775, "cdf": 0.399999999999998 },
+ { "x": 0.81685153909239, "pdf": 0.23900152765095, "logpdf": -1.43128533522608, "cdf": 0.5 },
+ { "x": 1.32470072354968, "pdf": 0.163772302491998, "logpdf": -1.80927821531652, "cdf": 0.6 },
+ { "x": 2.07733165294402, "pdf": 0.108243564930168, "logpdf": -2.22337136023913, "cdf": 0.7 },
+ { "x": 3.2704062085269, "pdf": 0.064685097882654, "logpdf": -2.73822443039313, "cdf": 0.8 },
+ { "x": 5.54191528143532, "pdf": 0.0291464642496897, "logpdf": -3.53542166857885, "cdf": 0.9 }
+ ],
+ "quans": [
+ { "q": 0.10, "x": 0.0180116841854315 },
+ { "q": 0.25, "x": 0.153164414123896 },
+ { "q": 0.50, "x": 0.81685153909239 },
+ { "q": 0.75, "x": 2.59770635997387 },
+ { "q": 0.90, "x": 5.54191528143532 }
+ ]
+},
+{
+ "expr": "Tweedie(2.0, 1.5, 2.0)",
+ "dtype": "Tweedie",
+ "minimum": 0,
+ "maximum": "inf",
+ "properties": {
+ },
+ "points": [
+ { "x": 0.0193166098973791, "pdf": 2.29401151609469, "logpdf": 0.830302038788168, "cdf": 0.1 },
+ { "x": 0.0929286024211608, "pdf": 0.942933429728374, "logpdf": -0.0587595929780245, "cdf": 0.2 },
+ { "x": 0.236509737467784, "pdf": 0.543544813747258, "logpdf": -0.609643121823222, "cdf": 0.3 },
+ { "x": 0.467817526522702, "pdf": 0.353460636956834, "logpdf": -1.03998315216011, "cdf": 0.4 },
+ { "x": 0.813387643444831, "pdf": 0.240732079165742, "logpdf": -1.42407066859999, "cdf": 0.5 },
+ { "x": 1.31820953609136, "pdf": 0.164558740147814, "logpdf": -1.80448768955023, "cdf": 0.6 },
+ { "x": 2.06837862454134, "pdf": 0.108450167200076, "logpdf": -2.22146449998544, "cdf": 0.7 },
+ { "x": 3.26120962635577, "pdf": 0.0646025808808938, "logpdf": -2.73950091727501, "cdf": 0.8 },
+ { "x": 5.54018226362739, "pdf": 0.0290033364805552, "logpdf": -3.5403444045599, "cdf": 0.9 }
+ ],
+ "quans": [
+ { "q": 0.10, "x": 0.0193166098973791 },
+ { "q": 0.25, "x": 0.15499278218233 },
+ { "q": 0.50, "x": 0.813387643444831 },
+ { "q": 0.75, "x": 2.58817864849412 },
+ { "q": 0.90, "x": 5.54018226362739 }
+ ]
+},
{
"expr": "Uniform()",
"dtype": "Uniform",
diff --git a/test/ref/rdistributions.R b/test/ref/rdistributions.R
index 8df19b70a7..4a8d1dacd0 100644
--- a/test/ref/rdistributions.R
+++ b/test/ref/rdistributions.R
@@ -77,6 +77,7 @@ source("continuous/studentizedrange.R")
source("continuous/tdist.R")
source("continuous/triangulardist.R")
source("continuous/truncatednormal.R")
+source("continuous/tweedie.R")
source("continuous/uniform.R")
source("continuous/vonmises.R")
source("continuous/weibull.R")
diff --git a/test/ref/renv.lock b/test/ref/renv.lock
index 1e520649f8..8aadd1755a 100644
--- a/test/ref/renv.lock
+++ b/test/ref/renv.lock
@@ -1,6 +1,6 @@
{
"R": {
- "Version": "4.5.1",
+ "Version": "4.5.2",
"Repositories": [
{
"Name": "CRAN",
@@ -1335,6 +1335,12 @@
"Maintainer": "Georgi N. Boshnakov