Skip to content

Commit 0c647b4

Browse files
committed
Add Normal and StandardNormal
1 parent 3b475c1 commit 0c647b4

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

src/skstats/normal.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import numpy as np
2+
from numpy import inf
3+
4+
from scipy import special
5+
from scipy.stats._distribution_infrastructure import (
6+
ContinuousDistribution,
7+
_RealDomain,
8+
_RealParameter,
9+
_Parameterization,
10+
)
11+
12+
13+
__all__ = ["Normal", "StandardNormal"]
14+
15+
16+
class Normal(ContinuousDistribution):
17+
r"""Normal distribution with prescribed mean and standard deviation.
18+
19+
The probability density function of the normal distribution is:
20+
21+
.. math::
22+
23+
f(x) = \frac{1}{\sigma \sqrt{2 \pi}} \exp {
24+
\left( -\frac{1}{2}\left( \frac{x - \mu}{\sigma} \right)^2 \right)}
25+
26+
"""
27+
28+
# `ShiftedScaledDistribution` allows this to be generated automatically from
29+
# an instance of `StandardNormal`, but the normal distribution is so frequently
30+
# used that it's worth a bit of code duplication to get better performance.
31+
_mu_domain = _RealDomain(endpoints=(-inf, inf))
32+
_sigma_domain = _RealDomain(endpoints=(0, inf))
33+
_x_support = _RealDomain(endpoints=(-inf, inf))
34+
35+
_mu_param = _RealParameter("mu", symbol=r"\mu", domain=_mu_domain, typical=(-1, 1))
36+
_sigma_param = _RealParameter(
37+
"sigma", symbol=r"\sigma", domain=_sigma_domain, typical=(0.5, 1.5)
38+
)
39+
_x_param = _RealParameter("x", domain=_x_support, typical=(-1, 1))
40+
41+
_parameterizations = [_Parameterization(_mu_param, _sigma_param)]
42+
43+
_variable = _x_param
44+
_normalization = 1 / np.sqrt(2 * np.pi)
45+
_log_normalization = np.log(2 * np.pi) / 2
46+
47+
def __new__(cls, mu=None, sigma=None, **kwargs):
48+
if mu is None and sigma is None:
49+
return super().__new__(StandardNormal)
50+
return super().__new__(cls)
51+
52+
def __init__(self, *, mu=0.0, sigma=1.0, **kwargs):
53+
super().__init__(mu=mu, sigma=sigma, **kwargs)
54+
55+
def _logpdf_formula(self, x, *, mu, sigma, **kwargs):
56+
return StandardNormal._logpdf_formula(self, (x - mu) / sigma) - np.log(sigma)
57+
58+
def _pdf_formula(self, x, *, mu, sigma, **kwargs):
59+
return StandardNormal._pdf_formula(self, (x - mu) / sigma) / sigma
60+
61+
def _logcdf_formula(self, x, *, mu, sigma, **kwargs):
62+
return StandardNormal._logcdf_formula(self, (x - mu) / sigma)
63+
64+
def _cdf_formula(self, x, *, mu, sigma, **kwargs):
65+
return StandardNormal._cdf_formula(self, (x - mu) / sigma)
66+
67+
def _logccdf_formula(self, x, *, mu, sigma, **kwargs):
68+
return StandardNormal._logccdf_formula(self, (x - mu) / sigma)
69+
70+
def _ccdf_formula(self, x, *, mu, sigma, **kwargs):
71+
return StandardNormal._ccdf_formula(self, (x - mu) / sigma)
72+
73+
def _icdf_formula(self, x, *, mu, sigma, **kwargs):
74+
return StandardNormal._icdf_formula(self, x) * sigma + mu
75+
76+
def _ilogcdf_formula(self, x, *, mu, sigma, **kwargs):
77+
return StandardNormal._ilogcdf_formula(self, x) * sigma + mu
78+
79+
def _iccdf_formula(self, x, *, mu, sigma, **kwargs):
80+
return StandardNormal._iccdf_formula(self, x) * sigma + mu
81+
82+
def _ilogccdf_formula(self, x, *, mu, sigma, **kwargs):
83+
return StandardNormal._ilogccdf_formula(self, x) * sigma + mu
84+
85+
def _entropy_formula(self, *, mu, sigma, **kwargs):
86+
return StandardNormal._entropy_formula(self) + np.log(abs(sigma))
87+
88+
def _logentropy_formula(self, *, mu, sigma, **kwargs):
89+
lH0 = StandardNormal._logentropy_formula(self)
90+
lls = np.log(np.log(abs(sigma)) + 0j)
91+
return special.logsumexp(np.broadcast_arrays(lH0, lls), axis=0)
92+
93+
def _median_formula(self, *, mu, sigma, **kwargs):
94+
return mu
95+
96+
def _mode_formula(self, *, mu, sigma, **kwargs):
97+
return mu
98+
99+
def _moment_raw_formula(self, order, *, mu, sigma, **kwargs):
100+
if order == 0:
101+
return np.ones_like(mu)
102+
elif order == 1:
103+
return mu
104+
else:
105+
return None
106+
107+
_moment_raw_formula.orders = [0, 1] # type: ignore[attr-defined]
108+
109+
def _moment_central_formula(self, order, *, mu, sigma, **kwargs):
110+
if order == 0:
111+
return np.ones_like(mu)
112+
elif order % 2:
113+
return np.zeros_like(mu)
114+
else:
115+
# exact is faster (and obviously more accurate) for reasonable orders
116+
return sigma**order * special.factorial2(int(order) - 1, exact=True)
117+
118+
def _sample_formula(self, sample_shape, full_shape, rng, *, mu, sigma, **kwargs):
119+
return rng.normal(loc=mu, scale=sigma, size=full_shape)[()]
120+
121+
122+
class StandardNormal(Normal):
123+
r"""Standard normal distribution.
124+
125+
The probability density function of the standard normal distribution is:
126+
127+
.. math::
128+
129+
f(x) = \frac{1}{\sqrt{2 \pi}} \exp \left( -\frac{1}{2} x^2 \right)
130+
131+
"""
132+
133+
_x_support = _RealDomain(endpoints=(-inf, inf))
134+
_x_param = _RealParameter("x", domain=_x_support, typical=(-5, 5))
135+
_variable = _x_param
136+
_parameterizations = []
137+
_normalization = 1 / np.sqrt(2 * np.pi)
138+
_log_normalization = np.log(2 * np.pi) / 2
139+
mu = np.float64(0.0)
140+
sigma = np.float64(1.0)
141+
142+
def __init__(self, **kwargs):
143+
ContinuousDistribution.__init__(self, **kwargs)
144+
145+
def _logpdf_formula(self, x, **kwargs):
146+
return -(self._log_normalization + x**2 / 2)
147+
148+
def _pdf_formula(self, x, **kwargs):
149+
return self._normalization * np.exp(-(x**2) / 2)
150+
151+
def _logcdf_formula(self, x, **kwargs):
152+
return special.log_ndtr(x)
153+
154+
def _cdf_formula(self, x, **kwargs):
155+
return special.ndtr(x)
156+
157+
def _logccdf_formula(self, x, **kwargs):
158+
return special.log_ndtr(-x)
159+
160+
def _ccdf_formula(self, x, **kwargs):
161+
return special.ndtr(-x)
162+
163+
def _icdf_formula(self, x, **kwargs):
164+
return special.ndtri(x)
165+
166+
def _ilogcdf_formula(self, x, **kwargs):
167+
return special.ndtri_exp(x)
168+
169+
def _iccdf_formula(self, x, **kwargs):
170+
return -special.ndtri(x)
171+
172+
def _ilogccdf_formula(self, x, **kwargs):
173+
return -special.ndtri_exp(x)
174+
175+
def _entropy_formula(self, **kwargs):
176+
return (1 + np.log(2 * np.pi)) / 2
177+
178+
def _logentropy_formula(self, **kwargs):
179+
return np.log1p(np.log(2 * np.pi)) - np.log(2)
180+
181+
def _median_formula(self, **kwargs):
182+
return 0
183+
184+
def _mode_formula(self, **kwargs):
185+
return 0
186+
187+
def _moment_raw_formula(self, order, **kwargs):
188+
raw_moments = {0: 1, 1: 0, 2: 1, 3: 0, 4: 3, 5: 0}
189+
return raw_moments.get(order, None)
190+
191+
def _moment_central_formula(self, order, **kwargs):
192+
return self._moment_raw_formula(order, **kwargs)
193+
194+
def _moment_standardized_formula(self, order, **kwargs):
195+
return self._moment_raw_formula(order, **kwargs)
196+
197+
def _sample_formula(self, sample_shape, full_shape, rng, **kwargs):
198+
return rng.normal(size=full_shape)[()]

0 commit comments

Comments
 (0)