Skip to content

Commit 606d4ff

Browse files
authored
Skewed Student-T distribution (#7252)
1 parent 0ad689c commit 606d4ff

File tree

5 files changed

+199
-1
lines changed

5 files changed

+199
-1
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ Thus, Thomas, Chris and I are pleased to announce that PyMC3 is now in Beta.
989989
* Benjamin Edwards <[email protected]>
990990
* Brian Naughton <[email protected]>
991991
* Chad Heyne <[email protected]>
992-
* Chris Fonnesbeck <chris.fonnesbeck@vanderbilt.edu>
992+
* Chris Fonnesbeck <fonnesbeck@gmail.com>
993993
* Corey Farwell <[email protected]>
994994
* John Salvatier <[email protected]>
995995
* Karlson Pfannschmidt <[email protected]>

docs/source/api/distributions/continuous.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Continuous
3333
PolyaGamma
3434
Rice
3535
SkewNormal
36+
SkewStudentT
3637
StudentT
3738
Triangular
3839
TruncatedNormal

pymc/distributions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
PolyaGamma,
4242
Rice,
4343
SkewNormal,
44+
SkewStudentT,
4445
StudentT,
4546
Triangular,
4647
TruncatedNormal,
@@ -202,4 +203,5 @@
202203
"HurdleLogNormal",
203204
"HurdleNegativeBinomial",
204205
"HurdlePoisson",
206+
"SkewStudentT",
205207
]

pymc/distributions/continuous.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from pytensor.graph.basic import Apply, Variable
3030
from pytensor.graph.op import Op
3131
from pytensor.raise_op import Assert
32+
from pytensor.tensor import gamma as gammafn
3233
from pytensor.tensor import gammaln
3334
from pytensor.tensor.extra_ops import broadcast_shape
3435
from pytensor.tensor.math import betaincinv, gammaincinv, tanh
@@ -130,6 +131,7 @@ def polyagamma_cdf(*args, **kwargs):
130131
"Moyal",
131132
"AsymmetricLaplace",
132133
"PolyaGamma",
134+
"SkewStudentT",
133135
]
134136

135137

@@ -1908,6 +1910,138 @@ def icdf(value, nu, mu, sigma):
19081910
)
19091911

19101912

1913+
class SkewStudentTRV(RandomVariable):
1914+
name = "skewstudentt"
1915+
ndim_supp = 0
1916+
ndims_params = [0, 0, 0, 0]
1917+
dtype = "floatX"
1918+
_print_name = ("SkewStudentT", "\\operatorname{SkewStudentT}")
1919+
1920+
@classmethod
1921+
def rng_fn(cls, rng, a, b, mu, sigma, size=None) -> np.ndarray:
1922+
return np.asarray(
1923+
stats.jf_skew_t.rvs(a=a, b=b, loc=mu, scale=sigma, size=size, random_state=rng)
1924+
)
1925+
1926+
1927+
skewstudentt = SkewStudentTRV()
1928+
1929+
1930+
class SkewStudentT(Continuous):
1931+
r"""
1932+
Skewed Student's T distribution log-likelihood.
1933+
1934+
This follows Jones and Faddy (2003)
1935+
1936+
The pdf of this distribution is
1937+
1938+
.. math::
1939+
1940+
f(t)=f(t ; a, b)=C_{a, b}^{-1}\left\{1+\frac{t}{\left(a+b+t^2\right)^{1 / 2}}\right\}^{a+1 / 2}\left\{1-\frac{t}{\left(a+b+t^2\right)^{1 / 2}}\right\}^{b+1 / 2}
1941+
1942+
where
1943+
1944+
.. math::
1945+
1946+
C_{a, b}=2^{a+b-1} B(a, b)(a+b)^{1 / 2}
1947+
1948+
1949+
======== =============================================================
1950+
Support :math:`x \in [\infty, \infty)`
1951+
Mean :math:`E(T)=\frac{(a-b) \sqrt{(a+b)}}{2} \frac{\Gamma\left(a-\frac{1}{2}\right) \Gamma\left(b-\frac{1}{2}\right)}{\Gamma(a) \Gamma(b)}`
1952+
======== =============================================================
1953+
1954+
Parameters
1955+
----------
1956+
a : tensor_like of float
1957+
First kurtosis parameter (a > 0).
1958+
b : tensor_like of float
1959+
Second kurtosis parameter (b > 0).
1960+
mu : tensor_like of float
1961+
Location parameter.
1962+
sigma : tensor_like of float
1963+
Scale parameter (sigma > 0). Converges to the standard deviation as a and b
1964+
become close (only required if lam is not specified). Defaults to 1.
1965+
lam : tensor_like of float, optional
1966+
Scale parameter (lam > 0). Converges to the precision as a and b
1967+
become close (only required if sigma is not specified). Defaults to 1.
1968+
1969+
"""
1970+
1971+
rv_op = skewstudentt
1972+
1973+
@classmethod
1974+
def dist(cls, a, b, *, mu=0, sigma=None, lam=None, **kwargs):
1975+
a = pt.as_tensor_variable(a)
1976+
b = pt.as_tensor_variable(b)
1977+
lam, sigma = get_tau_sigma(tau=lam, sigma=sigma)
1978+
sigma = pt.as_tensor_variable(sigma)
1979+
1980+
return super().dist([a, b, mu, sigma], **kwargs)
1981+
1982+
def support_point(rv, size, a, b, mu, sigma):
1983+
a, b, mu, _ = pt.broadcast_arrays(a, b, mu, sigma)
1984+
Et = mu + (a - b) * pt.sqrt(a + b) * gammafn(a - 0.5) * gammafn(b - 0.5) / (
1985+
2 * gammafn(a) * gammafn(b)
1986+
)
1987+
if not rv_size_is_none(size):
1988+
Et = pt.full(size, Et)
1989+
return Et
1990+
1991+
def logp(value, a, b, mu, sigma):
1992+
_, sigma = get_tau_sigma(sigma=sigma)
1993+
1994+
x = (value - mu) / sigma
1995+
1996+
a_ = (a + 0.5) * pt.log(1 + x / pt.sqrt(a + b + x**2))
1997+
b_ = (b + 0.5) * pt.log(1 - x / pt.sqrt(a + b + x**2))
1998+
c = (a + b - 1) * pt.log(2) + pt.special.betaln(a, b) + 0.5 * pt.log(a + b)
1999+
2000+
res = a_ + b_ - c - pt.log(sigma)
2001+
2002+
return check_parameters(
2003+
res,
2004+
a > 0,
2005+
b > 0,
2006+
sigma > 0,
2007+
msg="a > 0, b > 0, sigma > 0",
2008+
)
2009+
2010+
def logcdf(value, a, b, mu, sigma):
2011+
_, sigma = get_tau_sigma(sigma=sigma)
2012+
2013+
x = (value - mu) / sigma
2014+
2015+
y = (1 + x / pt.sqrt(a + b + x**2)) * 0.5
2016+
res = pt.log(pt.betainc(a, b, y))
2017+
2018+
return check_parameters(
2019+
res,
2020+
a > 0,
2021+
b > 0,
2022+
sigma > 0,
2023+
msg="a > 0, b > 0, sigma > 0",
2024+
)
2025+
2026+
def icdf(value, a, b, mu, sigma):
2027+
_, sigma = get_tau_sigma(sigma=sigma)
2028+
2029+
bval = betaincinv(a, b, value)
2030+
num = (2 * bval - 1) * pt.sqrt(a + b)
2031+
denom = 2 * pt.sqrt(bval * (1 - bval))
2032+
res = num / denom
2033+
2034+
res = mu + res * sigma
2035+
res = check_icdf_value(res, value)
2036+
return check_icdf_parameters(
2037+
res,
2038+
a > 0,
2039+
b > 0,
2040+
sigma > 0,
2041+
msg="a > 0, b > 0, sigma > 0",
2042+
)
2043+
2044+
19112045
class Pareto(BoundedContinuous):
19122046
r"""
19132047
Pareto log-likelihood.

tests/distributions/test_continuous.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,14 @@ def test_studentt_logp(self):
549549
lambda value, nu, mu, sigma: st.t.logpdf(value, nu, mu, sigma),
550550
)
551551

552+
def test_skewstudentt_logp(self):
553+
check_logp(
554+
pm.SkewStudentT,
555+
R,
556+
{"a": Rplus, "b": Rplus, "mu": R, "sigma": Rplus},
557+
lambda value, a, b, mu, sigma: st.jf_skew_t.logpdf(value, a, b, mu, sigma),
558+
)
559+
552560
@pytest.mark.skipif(
553561
condition=(pytensor.config.floatX == "float32"),
554562
reason="Fails on float32 due to numerical issues",
@@ -574,6 +582,25 @@ def test_studentt_icdf(self):
574582
lambda q, nu, mu, sigma: st.t.ppf(q, nu, mu, sigma),
575583
)
576584

585+
@pytest.mark.skipif(
586+
condition=(pytensor.config.floatX == "float32"),
587+
reason="Fails on float32 due to numerical issues",
588+
)
589+
def test_skewstudentt_logcdf(self):
590+
check_logcdf(
591+
pm.SkewStudentT,
592+
R,
593+
{"a": Rplus, "b": Rplus, "mu": R, "sigma": Rplus},
594+
lambda value, a, b, mu, sigma: st.jf_skew_t.logcdf(value, a, b, mu, sigma),
595+
)
596+
597+
def test_skewstudentt_icdf(self):
598+
check_icdf(
599+
pm.SkewStudentT,
600+
{"a": Rplusbig, "b": Rplusbig, "mu": R, "sigma": Rplusbig},
601+
lambda q, a, b, mu, sigma: st.jf_skew_t.ppf(q, a, b, mu, sigma),
602+
)
603+
577604
def test_cauchy(self):
578605
check_logp(
579606
pm.Cauchy,
@@ -1250,6 +1277,27 @@ def test_studentt_support_point(self, mu, nu, sigma, size, expected):
12501277
pm.StudentT("x", mu=mu, nu=nu, sigma=sigma, size=size)
12511278
assert_support_point_is_expected(model, expected)
12521279

1280+
@pytest.mark.parametrize(
1281+
"a, b, mu, sigma, size, expected",
1282+
[
1283+
(1, 1, 0, 1, None, 0),
1284+
(np.ones(5), np.ones(5), 0, 1, None, np.zeros(5)),
1285+
(10, 10, np.arange(5), np.arange(1, 6), None, np.arange(5)),
1286+
(
1287+
10,
1288+
10,
1289+
np.arange(5),
1290+
np.arange(1, 6),
1291+
(2, 5),
1292+
np.full((2, 5), np.arange(5)),
1293+
),
1294+
],
1295+
)
1296+
def test_skewstudentt_support_point(self, a, b, mu, sigma, size, expected):
1297+
with pm.Model() as model:
1298+
pm.SkewStudentT("x", a=a, b=b, mu=mu, sigma=sigma, size=size)
1299+
assert_support_point_is_expected(model, expected)
1300+
12531301
@pytest.mark.parametrize(
12541302
"alpha, beta, size, expected",
12551303
[
@@ -1896,6 +1944,19 @@ def halfstudentt_rng_fn(self, df, loc, scale, size, rng):
18961944
]
18971945

18981946

1947+
class TestSkewStudentT(BaseTestDistributionRandom):
1948+
pymc_dist = pm.SkewStudentT
1949+
pymc_dist_params = {"a": 5.0, "b": 5.0, "mu": -1.0, "sigma": 2.0}
1950+
expected_rv_op_params = {"a": 5.0, "b": 5.0, "mu": -1.0, "sigma": 2.0}
1951+
reference_dist_params = {"a": 5.0, "b": 5.0, "loc": -1.0, "scale": 2.0}
1952+
reference_dist = seeded_scipy_distribution_builder("jf_skew_t")
1953+
checks_to_run = [
1954+
"check_pymc_params_match_rv_op",
1955+
"check_pymc_draws_match_reference",
1956+
"check_rv_size",
1957+
]
1958+
1959+
18991960
class TestMoyal(BaseTestDistributionRandom):
19001961
pymc_dist = pm.Moyal
19011962
pymc_dist_params = {"mu": 0.0, "sigma": 1.0}

0 commit comments

Comments
 (0)