gx2 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gx2/__init__.py +53 -0
- gx2/_basic.py +144 -0
- gx2/_convert.py +106 -0
- gx2/_distribution.py +363 -0
- gx2/_helpers.py +210 -0
- gx2/_methods.py +430 -0
- gx2/_ray.py +399 -0
- gx2-1.0.0.dist-info/METADATA +150 -0
- gx2-1.0.0.dist-info/RECORD +12 -0
- gx2-1.0.0.dist-info/WHEEL +5 -0
- gx2-1.0.0.dist-info/licenses/LICENSE +21 -0
- gx2-1.0.0.dist-info/top_level.txt +1 -0
gx2/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""gx2 - Generalized chi-square distribution
|
|
2
|
+
============================================
|
|
3
|
+
|
|
4
|
+
Python port of the MATLAB *Generalized chi-square distribution* toolbox by
|
|
5
|
+
Abhranil Das (Center for Perceptual Systems, The University of Texas at
|
|
6
|
+
Austin). It computes the statistics, characteristic function, pdf, cdf,
|
|
7
|
+
inverse cdf and random numbers of the generalized chi-square distribution --
|
|
8
|
+
the distribution of a weighted sum of non-central chi-square variables plus a
|
|
9
|
+
normal variable, equivalently the quadratic form of a normal vector.
|
|
10
|
+
|
|
11
|
+
A generalized chi-square is parametrised by:
|
|
12
|
+
|
|
13
|
+
w weights of the non-central chi-square terms
|
|
14
|
+
k their degrees of freedom
|
|
15
|
+
lambda_ their non-centralities (named ``lambda_`` since ``lambda`` is a
|
|
16
|
+
Python keyword)
|
|
17
|
+
s scale of the added normal term
|
|
18
|
+
m offset
|
|
19
|
+
|
|
20
|
+
If you use this code, please cite:
|
|
21
|
+
1. A method to integrate and classify normal distributions
|
|
22
|
+
(https://arxiv.org/abs/2012.14331)
|
|
23
|
+
2. New methods for computing the generalized chi-square distribution
|
|
24
|
+
(https://arxiv.org/abs/2404.05062)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from ._basic import stat, char, rnd
|
|
28
|
+
from ._convert import gx2_to_norm_quad_params, norm_quad_to_gx2_params
|
|
29
|
+
from ._distribution import cdf, pdf, inv, log_cdf
|
|
30
|
+
from ._methods import (imhof, imhof_integrand, ruben, ifft,
|
|
31
|
+
pearson, cdf_pearson, tail, ellipse)
|
|
32
|
+
from ._ray import (cdf_ray, pdf_ray, ray_integrand, int_norm_ray,
|
|
33
|
+
norm_prob_across_rays, norm_prob_across_angles)
|
|
34
|
+
from ._helpers import (log_sum_exp, signed_log_sum_exp, phi_ray,
|
|
35
|
+
Phibar_ray_split, Phibar_sym, prob_ray_sym, standard_quad)
|
|
36
|
+
|
|
37
|
+
__version__ = "1.0.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# core distribution API
|
|
41
|
+
"stat", "char", "rnd", "cdf", "pdf", "inv", "log_cdf",
|
|
42
|
+
# parameter conversions
|
|
43
|
+
"gx2_to_norm_quad_params", "norm_quad_to_gx2_params",
|
|
44
|
+
# individual methods
|
|
45
|
+
"imhof", "imhof_integrand", "ruben", "ifft", "pearson",
|
|
46
|
+
"cdf_pearson", "tail", "ellipse",
|
|
47
|
+
# ray method internals
|
|
48
|
+
"cdf_ray", "pdf_ray", "ray_integrand", "int_norm_ray",
|
|
49
|
+
"norm_prob_across_rays", "norm_prob_across_angles",
|
|
50
|
+
# numerical helpers
|
|
51
|
+
"log_sum_exp", "signed_log_sum_exp", "phi_ray", "Phibar_ray_split",
|
|
52
|
+
"Phibar_sym", "prob_ray_sym", "standard_quad",
|
|
53
|
+
]
|
gx2/_basic.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Mean/variance, characteristic function, and random number generation.
|
|
2
|
+
Mirrors ``gx2stat.m``, ``gx2char.m`` and ``gx2rnd.m``.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.stats import ncx2, chi2, norm
|
|
7
|
+
|
|
8
|
+
from ._helpers import asrow
|
|
9
|
+
from ._convert import gx2_to_norm_quad_params
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def stat(w, k, lambda_, s, m):
|
|
13
|
+
"""Mean and variance of a generalized chi-square distribution.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
w : array_like
|
|
18
|
+
Weights of the non-central chi-square terms.
|
|
19
|
+
k : array_like
|
|
20
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
21
|
+
lambda_ : array_like
|
|
22
|
+
Non-centrality parameters of the non-central chi-square terms.
|
|
23
|
+
s : float
|
|
24
|
+
Scale (standard deviation) of the added normal term.
|
|
25
|
+
m : float
|
|
26
|
+
Constant offset added to the distribution.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
mu : float
|
|
31
|
+
Mean.
|
|
32
|
+
v : float
|
|
33
|
+
Variance.
|
|
34
|
+
"""
|
|
35
|
+
w = asrow(w)
|
|
36
|
+
k = asrow(k)
|
|
37
|
+
lambda_ = asrow(lambda_)
|
|
38
|
+
mu = float(np.dot(w, k + lambda_) + m)
|
|
39
|
+
v = float(2 * np.dot(w ** 2, k + 2 * lambda_) + s ** 2)
|
|
40
|
+
return mu, v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def char(t, w, k, lambda_, s, m):
|
|
44
|
+
"""Characteristic function of a generalized chi-square distribution.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
t : array_like
|
|
49
|
+
Point(s) at which to evaluate the characteristic function.
|
|
50
|
+
w : array_like
|
|
51
|
+
Weights of the non-central chi-square terms.
|
|
52
|
+
k : array_like
|
|
53
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
54
|
+
lambda_ : array_like
|
|
55
|
+
Non-centrality parameters of the non-central chi-square terms.
|
|
56
|
+
s : float
|
|
57
|
+
Scale (standard deviation) of the added normal term.
|
|
58
|
+
m : float
|
|
59
|
+
Constant offset added to the distribution.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
phi : ndarray of complex
|
|
64
|
+
The characteristic function at each ``t``, shaped like ``t``.
|
|
65
|
+
"""
|
|
66
|
+
w = asrow(w)
|
|
67
|
+
k = asrow(k)
|
|
68
|
+
lambda_ = asrow(lambda_)
|
|
69
|
+
t = np.asarray(t, dtype=float)
|
|
70
|
+
tf = t.ravel()
|
|
71
|
+
tc = tf[:, None] # column
|
|
72
|
+
|
|
73
|
+
term = np.sum((w * lambda_) / (1 - 2j * tc * w), axis=1)
|
|
74
|
+
denom = np.prod((1 - 2j * w * tc) ** (k / 2), axis=1)
|
|
75
|
+
phi = np.exp(1j * m * tf + 1j * tf * term - s ** 2 * tf ** 2 / 2) / denom
|
|
76
|
+
return phi.reshape(t.shape)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rnd(w, k, lambda_, s, m, size=None, method="sum"):
|
|
80
|
+
"""Generate generalized chi-square random numbers.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
w : array_like
|
|
85
|
+
Weights of the non-central chi-square terms.
|
|
86
|
+
k : array_like
|
|
87
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
88
|
+
lambda_ : array_like
|
|
89
|
+
Non-centrality parameters of the non-central chi-square terms.
|
|
90
|
+
s : float
|
|
91
|
+
Scale (standard deviation) of the added normal term.
|
|
92
|
+
m : float
|
|
93
|
+
Constant offset added to the distribution.
|
|
94
|
+
size : int or tuple, optional
|
|
95
|
+
Output shape. A scalar ``n`` gives an ``n x n`` array; a tuple gives
|
|
96
|
+
that exact shape. If omitted, a single scalar is returned.
|
|
97
|
+
method : {'sum', 'norm_quad'}
|
|
98
|
+
``'sum'`` (default) generates non-central chi-square and normal numbers
|
|
99
|
+
and adds them. ``'norm_quad'`` generates standard normal vectors and
|
|
100
|
+
computes their quadratic form.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
r : float or ndarray
|
|
105
|
+
Random sample(s), of shape ``size`` (or a scalar if ``size`` is None).
|
|
106
|
+
"""
|
|
107
|
+
w = asrow(w)
|
|
108
|
+
k = asrow(k)
|
|
109
|
+
lambda_ = asrow(lambda_)
|
|
110
|
+
|
|
111
|
+
if size is None:
|
|
112
|
+
shape = ()
|
|
113
|
+
elif np.isscalar(size):
|
|
114
|
+
shape = (int(size), int(size))
|
|
115
|
+
else:
|
|
116
|
+
shape = tuple(int(x) for x in size)
|
|
117
|
+
|
|
118
|
+
method = str(method).lower()
|
|
119
|
+
if method == "sum":
|
|
120
|
+
r = np.zeros(shape)
|
|
121
|
+
for wi, ki, li in zip(w, k, lambda_):
|
|
122
|
+
if li == 0:
|
|
123
|
+
r = r + wi * chi2.rvs(df=ki, size=shape)
|
|
124
|
+
else:
|
|
125
|
+
r = r + wi * ncx2.rvs(df=ki, nc=li, size=shape)
|
|
126
|
+
if s:
|
|
127
|
+
r = r + norm.rvs(loc=m, scale=s, size=shape)
|
|
128
|
+
else:
|
|
129
|
+
r = r + m
|
|
130
|
+
return r
|
|
131
|
+
elif method == "norm_quad":
|
|
132
|
+
quad = gx2_to_norm_quad_params(w, k, lambda_, s, m)
|
|
133
|
+
q1 = np.asarray(quad["q1"]).ravel()
|
|
134
|
+
q2 = np.asarray(quad["q2"])
|
|
135
|
+
q0 = quad["q0"]
|
|
136
|
+
dim = q1.size
|
|
137
|
+
n = int(np.prod(shape)) if shape else 1
|
|
138
|
+
z = norm.rvs(loc=0, scale=1, size=(dim, n))
|
|
139
|
+
r = np.sum(z * (q2 @ z), axis=0) + q1 @ z + q0
|
|
140
|
+
if shape:
|
|
141
|
+
return r.reshape(shape)
|
|
142
|
+
return float(r[0])
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError("method must be 'sum' or 'norm_quad'")
|
gx2/_convert.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Conversions between generalized chi-square parameters and the quadratic form
|
|
2
|
+
of a normal vector. Mirrors ``gx2_to_norm_quad_params.m`` and
|
|
3
|
+
``norm_quad_to_gx2_params.m``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from ._helpers import asrow, uniquetol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def gx2_to_norm_quad_params(w, k, lambda_, s, m):
|
|
11
|
+
"""Quadratic-form coefficients of the standard normal whose quadratic form
|
|
12
|
+
is the given generalized chi-square.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
w, k, lambda_ : array_like
|
|
17
|
+
Weights, degrees of freedom and non-centralities of the non-central
|
|
18
|
+
chi-square terms.
|
|
19
|
+
s : float
|
|
20
|
+
Scale of the normal term.
|
|
21
|
+
m : float
|
|
22
|
+
Offset.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
quad : dict
|
|
27
|
+
``{'q2': matrix, 'q1': vector, 'q0': scalar}``. The dimension of the
|
|
28
|
+
standard normal is ``len(q1)``.
|
|
29
|
+
"""
|
|
30
|
+
w = asrow(w)
|
|
31
|
+
k = asrow(k)
|
|
32
|
+
lambda_ = asrow(lambda_)
|
|
33
|
+
|
|
34
|
+
q2_parts = []
|
|
35
|
+
q1_parts = []
|
|
36
|
+
for wi, ki, li in zip(w, k, lambda_):
|
|
37
|
+
ki = int(round(ki))
|
|
38
|
+
q2_parts.append(np.full(ki, wi))
|
|
39
|
+
q1_parts.append(np.concatenate(([wi * np.sqrt(li)], np.zeros(ki - 1))))
|
|
40
|
+
q2 = np.concatenate(q2_parts) if q2_parts else np.array([])
|
|
41
|
+
q1 = -2 * (np.concatenate(q1_parts) if q1_parts else np.array([]))
|
|
42
|
+
|
|
43
|
+
if s:
|
|
44
|
+
q2 = np.append(q2, 0.0)
|
|
45
|
+
q1 = np.append(q1, s)
|
|
46
|
+
|
|
47
|
+
return {"q2": np.diag(q2), "q1": q1.astype(float), "q0": float(np.dot(w, lambda_) + m)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def norm_quad_to_gx2_params(mu, v, quad, merge=True):
|
|
51
|
+
"""Parameters of the generalized chi-square distribution of a quadratic
|
|
52
|
+
form ``q(x) = x' q2 x + q1' x + q0`` of a normal vector ``x ~ N(mu, v)``.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
mu : array_like
|
|
57
|
+
Column vector of the normal mean.
|
|
58
|
+
v : array_like
|
|
59
|
+
Normal covariance matrix.
|
|
60
|
+
quad : dict
|
|
61
|
+
``{'q2': matrix, 'q1': vector, 'q0': scalar}``.
|
|
62
|
+
merge : bool, optional
|
|
63
|
+
If True (default), merge non-central chi-square components with
|
|
64
|
+
close-enough weights into single components. Set False to return all
|
|
65
|
+
raw exact components.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
w, k, lambda_, s, m
|
|
70
|
+
"""
|
|
71
|
+
mu = np.asarray(mu, dtype=float).ravel()
|
|
72
|
+
v = np.asarray(v, dtype=float)
|
|
73
|
+
q2_in = np.asarray(quad["q2"], dtype=float)
|
|
74
|
+
q1_in = np.asarray(quad["q1"], dtype=float).ravel()
|
|
75
|
+
q0_in = float(quad["q0"])
|
|
76
|
+
|
|
77
|
+
q2_sym = 0.5 * (q2_in + q2_in.T)
|
|
78
|
+
|
|
79
|
+
# sqrtm(v) avoiding small negative eigenvalues
|
|
80
|
+
d, R = np.linalg.eigh(v)
|
|
81
|
+
d = np.where(d < 0, 0.0, d)
|
|
82
|
+
sqrt_v = R @ np.diag(np.sqrt(d)) @ R.T
|
|
83
|
+
|
|
84
|
+
q2 = sqrt_v @ q2_sym @ sqrt_v
|
|
85
|
+
q2 = (q2 + q2.T) / 2
|
|
86
|
+
q1 = sqrt_v @ (2 * q2_sym @ mu + q1_in)
|
|
87
|
+
q0 = float(mu @ q2_sym @ mu + q1_in @ mu + q0_in)
|
|
88
|
+
|
|
89
|
+
d2, R2 = np.linalg.eigh(q2)
|
|
90
|
+
d = d2
|
|
91
|
+
b = (R2.T @ q1)
|
|
92
|
+
|
|
93
|
+
nz = d != 0
|
|
94
|
+
if merge:
|
|
95
|
+
w, ic = uniquetol(d[nz])
|
|
96
|
+
k = np.bincount(ic, minlength=w.size).astype(float)
|
|
97
|
+
b_sq_sum = np.bincount(ic, weights=b[nz] ** 2, minlength=w.size)
|
|
98
|
+
lambda_ = b_sq_sum / (4 * w ** 2)
|
|
99
|
+
else:
|
|
100
|
+
w = d[nz].copy()
|
|
101
|
+
k = np.ones(w.size)
|
|
102
|
+
lambda_ = b[nz] ** 2 / (4 * w ** 2)
|
|
103
|
+
|
|
104
|
+
m = q0 - np.dot(w, lambda_)
|
|
105
|
+
s = np.linalg.norm(b[~nz])
|
|
106
|
+
return w, k, lambda_, s, m
|
gx2/_distribution.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Top-level cdf, pdf and inverse-cdf with automatic method selection.
|
|
2
|
+
Mirrors gx2cdf.m, gx2pdf.m, gx2inv.m and log_gx2cdf.m.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.stats import norm
|
|
8
|
+
|
|
9
|
+
from ._helpers import asrow, fzero
|
|
10
|
+
from ._basic import stat
|
|
11
|
+
from ._methods import (imhof, ruben, ifft, pearson, tail,
|
|
12
|
+
ellipse, _ncx2cdf)
|
|
13
|
+
from ._ray import cdf_ray, pdf_ray, int_norm_ray
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _filter(func, kwargs):
|
|
17
|
+
"""Keep only kwargs accepted by ``func`` (mimics MATLAB KeepUnmatched)."""
|
|
18
|
+
params = inspect.signature(func).parameters
|
|
19
|
+
return {k: v for k, v in kwargs.items() if k in params}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_full(x):
|
|
23
|
+
return isinstance(x, str) and x.lower() == "full"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ===========================================================================
|
|
27
|
+
# cdf
|
|
28
|
+
# ===========================================================================
|
|
29
|
+
|
|
30
|
+
def cdf(x, w, k, lambda_, s, m, side="lower", method="auto",
|
|
31
|
+
full_output=False, **kwargs):
|
|
32
|
+
"""Cdf of a generalized chi-square distribution.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
x : float, array_like, or 'full'
|
|
37
|
+
Point(s) at which to evaluate the cdf. The output has the same shape as
|
|
38
|
+
``x``. Pass the string ``'full'`` to evaluate the cdf over a grid that
|
|
39
|
+
automatically spans the whole distribution; the grid is then returned
|
|
40
|
+
as ``x_grid`` (see Returns), so use ``'full'`` together with the
|
|
41
|
+
returned tuple.
|
|
42
|
+
w : array_like
|
|
43
|
+
Weights of the non-central chi-square terms.
|
|
44
|
+
k : array_like
|
|
45
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
46
|
+
lambda_ : array_like
|
|
47
|
+
Non-centrality parameters of the non-central chi-square terms (one per
|
|
48
|
+
term, same length as ``w`` and ``k``).
|
|
49
|
+
s : float
|
|
50
|
+
Scale (standard deviation) of the added normal term.
|
|
51
|
+
m : float
|
|
52
|
+
Constant offset added to the distribution.
|
|
53
|
+
side : {'lower', 'upper'}
|
|
54
|
+
``'lower'`` (default) returns the cdf P(X <= x). ``'upper'`` returns the
|
|
55
|
+
complementary cdf P(X > x), computed in a way that stays accurate when
|
|
56
|
+
it is very small.
|
|
57
|
+
method : {'auto','imhof','ray','ifft','ruben','tail','pearson','ellipse'}
|
|
58
|
+
Algorithm used. ``'auto'`` (default) picks a suitable one for the given
|
|
59
|
+
parameters. The others let you choose explicitly; some have constraints
|
|
60
|
+
(e.g. ``'ruben'`` and ``'ellipse'`` need all ``w`` the same sign and
|
|
61
|
+
``s == 0``).
|
|
62
|
+
full_output : bool
|
|
63
|
+
Controls how many values are returned (see Returns). Defaults to False,
|
|
64
|
+
and is forced True when ``x='full'``.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
p : float or ndarray
|
|
69
|
+
The cdf (or complementary cdf if ``side='upper'``) at each ``x``, shaped
|
|
70
|
+
like ``x``. This is the sole return value when ``full_output`` is False.
|
|
71
|
+
With the ``'tail'``, ``'ellipse'`` and ``'ray'`` methods, any entry that
|
|
72
|
+
is too small for double precision (below ~1e-308) is returned instead as
|
|
73
|
+
its base-10 logarithm, which is negative; entries that fit normally stay
|
|
74
|
+
positive. This lets the far tails be represented down to arbitrarily
|
|
75
|
+
tiny probabilities.
|
|
76
|
+
p_err : ndarray or None
|
|
77
|
+
Returned only when ``full_output=True``. An estimate of the numerical
|
|
78
|
+
error in ``p`` (its exact meaning depends on the method, e.g. the
|
|
79
|
+
Monte-Carlo standard error for ``'ray'`` or an error bound for
|
|
80
|
+
``'ruben'``), or ``None`` for methods that do not provide one.
|
|
81
|
+
x_grid : ndarray or None
|
|
82
|
+
Returned only when ``full_output=True``. When ``x='full'``, the array of
|
|
83
|
+
points at which ``p`` was evaluated; otherwise ``None``.
|
|
84
|
+
|
|
85
|
+
Examples
|
|
86
|
+
--------
|
|
87
|
+
>>> import gx2
|
|
88
|
+
>>> gx2.cdf(25, [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5) # cdf at x=25
|
|
89
|
+
>>> gx2.cdf(25, [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5, side='upper')
|
|
90
|
+
>>> p, p_err, x_grid = gx2.cdf('full', [1, -5, 2], [1, 2, 3], [2, 3, 7], 0, 5,
|
|
91
|
+
... full_output=True)
|
|
92
|
+
"""
|
|
93
|
+
w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
|
|
94
|
+
full = _is_full(x)
|
|
95
|
+
if not full:
|
|
96
|
+
x = np.asarray(x, dtype=float)
|
|
97
|
+
p_err = None
|
|
98
|
+
x_grid = None
|
|
99
|
+
|
|
100
|
+
if full:
|
|
101
|
+
method = "ifft"
|
|
102
|
+
|
|
103
|
+
if method == "auto":
|
|
104
|
+
uw = np.unique(w)
|
|
105
|
+
if (not s) and uw.size == 1:
|
|
106
|
+
uw0 = uw[0]
|
|
107
|
+
lower = ((np.sign(uw0) == 1 and side == "lower")
|
|
108
|
+
or (np.sign(uw0) == -1 and side == "upper"))
|
|
109
|
+
p = _ncx2cdf((x - m) / uw0, np.sum(k), np.sum(lambda_), upper=not lower)
|
|
110
|
+
elif np.sum(np.abs(w)) == 0 and s:
|
|
111
|
+
p = norm.sf(x, m, s) if side == "upper" else norm.cdf(x, m, s)
|
|
112
|
+
elif not s:
|
|
113
|
+
if (np.all(w > 0) and side == "lower") or (np.all(w < 0) and side == "upper"):
|
|
114
|
+
try:
|
|
115
|
+
p, p_err = ruben(x, w, k, lambda_, m, side=side,
|
|
116
|
+
**_filter(ruben, kwargs))
|
|
117
|
+
except Exception:
|
|
118
|
+
p, p_err = imhof(x, w, k, lambda_, 0, m, side=side,
|
|
119
|
+
**_filter(imhof, kwargs))
|
|
120
|
+
else:
|
|
121
|
+
p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
|
|
122
|
+
**_filter(imhof, kwargs))
|
|
123
|
+
else:
|
|
124
|
+
p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
|
|
125
|
+
**_filter(imhof, kwargs))
|
|
126
|
+
elif method == "ifft":
|
|
127
|
+
p, x_grid = ifft(x, w, k, lambda_, s, m, side=side, output="cdf",
|
|
128
|
+
**_filter(ifft, kwargs))
|
|
129
|
+
elif method == "ray":
|
|
130
|
+
p, p_err = cdf_ray(x, w, k, lambda_, s, m, side=side,
|
|
131
|
+
**_filter(int_norm_ray, kwargs))
|
|
132
|
+
elif method == "imhof":
|
|
133
|
+
p, p_err = imhof(x, w, k, lambda_, s, m, side=side,
|
|
134
|
+
**_filter(imhof, kwargs))
|
|
135
|
+
elif method == "ruben":
|
|
136
|
+
if s or not (np.all(w > 0) or np.all(w < 0)):
|
|
137
|
+
raise ValueError("Ruben's method can only be used when all w are "
|
|
138
|
+
"the same sign and s=0.")
|
|
139
|
+
p, p_err = ruben(x, w, k, lambda_, m, side=side,
|
|
140
|
+
**_filter(ruben, kwargs))
|
|
141
|
+
elif method == "tail":
|
|
142
|
+
p = tail(x, w, k, lambda_, s, m, side=side, **_filter(tail, kwargs))
|
|
143
|
+
elif method == "pearson":
|
|
144
|
+
p = pearson(x, w, k, lambda_, s, m, side=side, **_filter(pearson, kwargs))
|
|
145
|
+
elif method == "ellipse":
|
|
146
|
+
if s or not (np.all(w > 0) or np.all(w < 0)):
|
|
147
|
+
raise ValueError("The ellipse approximation can only be used when "
|
|
148
|
+
"all w are the same sign and s=0.")
|
|
149
|
+
p, p_err = ellipse(x, w, k, lambda_, m, side=side,
|
|
150
|
+
**_filter(ellipse, kwargs))
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError("unknown method %r" % method)
|
|
153
|
+
|
|
154
|
+
if full or full_output:
|
|
155
|
+
return p, p_err, x_grid
|
|
156
|
+
return p
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ===========================================================================
|
|
160
|
+
# pdf
|
|
161
|
+
# ===========================================================================
|
|
162
|
+
|
|
163
|
+
def pdf(x, w, k, lambda_, s, m, side="lower", method="auto", diff=False,
|
|
164
|
+
dx=None, full_output=False, **kwargs):
|
|
165
|
+
"""Pdf of a generalized chi-square distribution.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
x : float, array_like, or 'full'
|
|
170
|
+
Point(s) at which to evaluate the pdf; output is shaped like ``x``.
|
|
171
|
+
``'full'`` evaluates over an automatically chosen spanning grid, which
|
|
172
|
+
is returned as ``x_grid`` (see Returns).
|
|
173
|
+
w : array_like
|
|
174
|
+
Weights of the non-central chi-square terms.
|
|
175
|
+
k : array_like
|
|
176
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
177
|
+
lambda_ : array_like
|
|
178
|
+
Non-centrality parameters of the non-central chi-square terms.
|
|
179
|
+
s : float
|
|
180
|
+
Scale (standard deviation) of the added normal term.
|
|
181
|
+
m : float
|
|
182
|
+
Constant offset added to the distribution.
|
|
183
|
+
side : {'lower', 'upper'}
|
|
184
|
+
Only affects the ``'tail'`` method, selecting which infinite tail to
|
|
185
|
+
approximate.
|
|
186
|
+
method : {'auto','imhof','ray','ifft','ruben','tail','pearson','ellipse'}
|
|
187
|
+
Algorithm used; ``'auto'`` (default) picks a suitable one.
|
|
188
|
+
diff : bool
|
|
189
|
+
If True, obtain the pdf by numerically differentiating :func:`cdf`
|
|
190
|
+
instead of evaluating it directly.
|
|
191
|
+
dx : float, optional
|
|
192
|
+
Step size for the ``diff=True`` finite difference. Defaults to the
|
|
193
|
+
distribution's standard deviation divided by 1e4.
|
|
194
|
+
full_output : bool
|
|
195
|
+
Controls how many values are returned (see Returns). Defaults to False,
|
|
196
|
+
and is forced True when ``x='full'``.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
f : float or ndarray
|
|
201
|
+
The pdf at each ``x``, shaped like ``x``. This is the sole return value
|
|
202
|
+
when ``full_output`` is False. As with :func:`cdf`, the ``'tail'``,
|
|
203
|
+
``'ellipse'`` and ``'ray'`` methods return the (negative) base-10
|
|
204
|
+
logarithm for entries too small for double precision.
|
|
205
|
+
f_err : ndarray or None
|
|
206
|
+
Returned only when ``full_output=True``. A method-dependent estimate of
|
|
207
|
+
the numerical error in ``f``, or ``None`` if unavailable.
|
|
208
|
+
x_grid : ndarray or None
|
|
209
|
+
Returned only when ``full_output=True``. The grid of points used when
|
|
210
|
+
``x='full'``; otherwise ``None``.
|
|
211
|
+
"""
|
|
212
|
+
w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
|
|
213
|
+
full = _is_full(x)
|
|
214
|
+
if not full:
|
|
215
|
+
x = np.asarray(x, dtype=float)
|
|
216
|
+
f_err = None
|
|
217
|
+
x_grid = None
|
|
218
|
+
|
|
219
|
+
if full:
|
|
220
|
+
method = "ifft"
|
|
221
|
+
|
|
222
|
+
if not diff:
|
|
223
|
+
if method == "auto":
|
|
224
|
+
uw = np.unique(w)
|
|
225
|
+
if (not s) and uw.size == 1 and not full:
|
|
226
|
+
from ._methods import _ncx2pdf
|
|
227
|
+
f = _ncx2pdf((x - m) / uw[0], np.sum(k), np.sum(lambda_)) / abs(uw[0])
|
|
228
|
+
elif np.sum(np.abs(w)) == 0 and s:
|
|
229
|
+
f = norm.pdf(x, m, s)
|
|
230
|
+
else:
|
|
231
|
+
f, _ = imhof(x, w, k, lambda_, s, m, output="pdf",
|
|
232
|
+
**_filter(imhof, kwargs))
|
|
233
|
+
elif method == "imhof":
|
|
234
|
+
f, _ = imhof(x, w, k, lambda_, s, m, output="pdf",
|
|
235
|
+
**_filter(imhof, kwargs))
|
|
236
|
+
elif method == "ruben":
|
|
237
|
+
if s or not (np.all(w > 0) or np.all(w < 0)):
|
|
238
|
+
raise ValueError("Ruben's method can only be used when all w are "
|
|
239
|
+
"the same sign and s=0.")
|
|
240
|
+
f, _ = ruben(x, w, k, lambda_, m, output="pdf",
|
|
241
|
+
**_filter(ruben, kwargs))
|
|
242
|
+
elif method == "tail":
|
|
243
|
+
f = tail(x, w, k, lambda_, s, m, side=side, output="pdf",
|
|
244
|
+
**_filter(tail, kwargs))
|
|
245
|
+
elif method == "pearson":
|
|
246
|
+
f = pearson(x, w, k, lambda_, s, m, side=side, output="pdf",
|
|
247
|
+
**_filter(pearson, kwargs))
|
|
248
|
+
elif method == "ellipse":
|
|
249
|
+
if s or not (np.all(w > 0) or np.all(w < 0)):
|
|
250
|
+
raise ValueError("The ellipse approximation can only be used when "
|
|
251
|
+
"all w are the same sign and s=0.")
|
|
252
|
+
f, f_err = ellipse(x, w, k, lambda_, m, side=side, output="pdf",
|
|
253
|
+
**_filter(ellipse, kwargs))
|
|
254
|
+
elif method == "ray":
|
|
255
|
+
f, f_err = pdf_ray(x, w, k, lambda_, s, m,
|
|
256
|
+
**_filter(pdf_ray, kwargs))
|
|
257
|
+
elif method == "ifft":
|
|
258
|
+
f, x_grid = ifft(x, w, k, lambda_, s, m, side=side, output="pdf",
|
|
259
|
+
**_filter(ifft, kwargs))
|
|
260
|
+
else:
|
|
261
|
+
raise ValueError("unknown method %r" % method)
|
|
262
|
+
else:
|
|
263
|
+
if dx is None:
|
|
264
|
+
_, v = stat(w, k, lambda_, s, m)
|
|
265
|
+
dx = np.sqrt(v) / 1e4
|
|
266
|
+
p_left = cdf(x - dx, w, k, lambda_, s, m, side=side, method=method, **kwargs)
|
|
267
|
+
p_right = cdf(x + dx, w, k, lambda_, s, m, side=side, method=method, **kwargs)
|
|
268
|
+
f = (p_right - p_left) / (2 * dx)
|
|
269
|
+
f = np.maximum(f, 0)
|
|
270
|
+
|
|
271
|
+
if full or full_output:
|
|
272
|
+
return f, f_err, x_grid
|
|
273
|
+
return f
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ===========================================================================
|
|
277
|
+
# inverse cdf
|
|
278
|
+
# ===========================================================================
|
|
279
|
+
|
|
280
|
+
def inv(p, w, k, lambda_, s, m, side="lower", method="auto", **kwargs):
|
|
281
|
+
"""Inverse cdf (quantile function) of a generalized chi-square distribution.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
p : float or array_like
|
|
286
|
+
Probability or probabilities at which to evaluate the quantile, in
|
|
287
|
+
(0, 1]. A negative value is interpreted as the base-10 logarithm of the
|
|
288
|
+
probability, which lets you request quantiles for probabilities below
|
|
289
|
+
~1e-308 (e.g. ``p=-1000`` means a cdf of 1e-1000); pair this with a
|
|
290
|
+
forward method that reaches such tiny values, e.g. ``method='tail'``,
|
|
291
|
+
``'ellipse'`` or ``'ray'``.
|
|
292
|
+
w : array_like
|
|
293
|
+
Weights of the non-central chi-square terms.
|
|
294
|
+
k : array_like
|
|
295
|
+
Degrees of freedom of the non-central chi-square terms.
|
|
296
|
+
lambda_ : array_like
|
|
297
|
+
Non-centrality parameters of the non-central chi-square terms.
|
|
298
|
+
s : float
|
|
299
|
+
Scale (standard deviation) of the added normal term.
|
|
300
|
+
m : float
|
|
301
|
+
Constant offset added to the distribution.
|
|
302
|
+
side : {'lower', 'upper'}
|
|
303
|
+
``'upper'`` inverts the complementary cdf, i.e. ``p`` is a tail
|
|
304
|
+
probability.
|
|
305
|
+
method : str
|
|
306
|
+
Forward cdf method used while root-finding; see :func:`cdf`.
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
x : float or ndarray
|
|
311
|
+
The quantile(s): the value(s) of x at which the cdf equals ``p``.
|
|
312
|
+
Returns a scalar for scalar ``p``, otherwise an array shaped like ``p``.
|
|
313
|
+
"""
|
|
314
|
+
w = asrow(w); k = asrow(k); lambda_ = asrow(lambda_)
|
|
315
|
+
p = np.atleast_1d(np.asarray(p, dtype=float))
|
|
316
|
+
|
|
317
|
+
uw = np.unique(w)
|
|
318
|
+
if (not s) and uw.size == 1 and np.all(p > 0):
|
|
319
|
+
uw0 = uw[0]
|
|
320
|
+
pp = 1 - p if side == "upper" else p
|
|
321
|
+
from scipy.stats import ncx2, chi2
|
|
322
|
+
df = np.sum(k); nc = np.sum(lambda_)
|
|
323
|
+
|
|
324
|
+
def _ncx2inv(pr):
|
|
325
|
+
if nc == 0:
|
|
326
|
+
return chi2.ppf(pr, df)
|
|
327
|
+
return ncx2.ppf(pr, df, nc)
|
|
328
|
+
|
|
329
|
+
if np.sign(uw0) == 1:
|
|
330
|
+
x = _ncx2inv(pp) * uw0 + m
|
|
331
|
+
elif np.sign(uw0) == -1:
|
|
332
|
+
x = _ncx2inv(1 - pp) * uw0 + m
|
|
333
|
+
else:
|
|
334
|
+
x = np.zeros_like(pp)
|
|
335
|
+
else:
|
|
336
|
+
mu, _ = stat(w, k, lambda_, s, m)
|
|
337
|
+
|
|
338
|
+
def solve_one(pi):
|
|
339
|
+
if pi > 0:
|
|
340
|
+
f = lambda xx: cdf(xx, w, k, lambda_, s, m, side=side,
|
|
341
|
+
method=method, **kwargs) - pi
|
|
342
|
+
else:
|
|
343
|
+
f = lambda xx: log_cdf(xx, w, k, lambda_, s, m, side=side,
|
|
344
|
+
method=method, **kwargs) - pi
|
|
345
|
+
return fzero(f, mu)
|
|
346
|
+
|
|
347
|
+
x = np.array([solve_one(pi) for pi in p])
|
|
348
|
+
|
|
349
|
+
x = np.asarray(x)
|
|
350
|
+
if x.size == 1:
|
|
351
|
+
return float(x.ravel()[0])
|
|
352
|
+
return x
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def log_cdf(x, w, k, lambda_, s, m, **kwargs):
|
|
356
|
+
"""log10 of the cdf, returning the (negative) value itself when the cdf
|
|
357
|
+
has already underflowed to a log10 value."""
|
|
358
|
+
p = cdf(x, w, k, lambda_, s, m, **kwargs)
|
|
359
|
+
p = float(np.asarray(p).ravel()[0]) if np.size(p) == 1 else p
|
|
360
|
+
if np.isscalar(p) or np.ndim(p) == 0:
|
|
361
|
+
return p if p <= 0 else np.log10(p)
|
|
362
|
+
p = np.asarray(p, dtype=float)
|
|
363
|
+
return np.where(p <= 0, p, np.log10(np.where(p > 0, p, 1)))
|