Skip to content

Commit b145dfa

Browse files
mdhaberpetrelharp
andauthored
ENH: stats.chisquare: add sum_check to disable check that total observed/expected counts are equal (scipy#21695)
* ENH: stats.chisquare: add 'sum_check' to disable check that total counts are equal --------- Co-authored-by: Peter Ralph <[email protected]>
1 parent 1d5ca99 commit b145dfa

File tree

2 files changed

+76
-38
lines changed

2 files changed

+76
-38
lines changed

scipy/stats/_stats_py.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7072,6 +7072,10 @@ def power_divergence(f_obs, f_exp=None, ddof=0, axis=0, lambda_=None):
70727072
(array([ 3.5 , 9.25]), array([ 0.62338763, 0.09949846]))
70737073
70747074
"""
7075+
return _power_divergence(f_obs, f_exp=f_exp, ddof=ddof, axis=axis, lambda_=lambda_)
7076+
7077+
7078+
def _power_divergence(f_obs, f_exp, ddof, axis, lambda_, sum_check=True):
70757079
xp = array_namespace(f_obs)
70767080
default_float = xp.asarray(1.).dtype
70777081

@@ -7110,21 +7114,23 @@ def warn_masked(arg):
71107114
bshape = _broadcast_shapes((f_obs_float.shape, f_exp.shape))
71117115
f_obs_float = _m_broadcast_to(f_obs_float, bshape, xp=xp)
71127116
f_exp = _m_broadcast_to(f_exp, bshape, xp=xp)
7113-
dtype_res = xp.result_type(f_obs.dtype, f_exp.dtype)
7114-
rtol = xp.finfo(dtype_res).eps**0.5 # to pass existing tests
7115-
with np.errstate(invalid='ignore'):
7116-
f_obs_sum = _m_sum(f_obs_float, axis=axis, preserve_mask=False, xp=xp)
7117-
f_exp_sum = _m_sum(f_exp, axis=axis, preserve_mask=False, xp=xp)
7118-
relative_diff = (xp.abs(f_obs_sum - f_exp_sum) /
7119-
xp.minimum(f_obs_sum, f_exp_sum))
7120-
diff_gt_tol = xp.any(relative_diff > rtol, axis=None)
7121-
if diff_gt_tol:
7122-
msg = (f"For each axis slice, the sum of the observed "
7123-
f"frequencies must agree with the sum of the "
7124-
f"expected frequencies to a relative tolerance "
7125-
f"of {rtol}, but the percent differences are:\n"
7126-
f"{relative_diff}")
7127-
raise ValueError(msg)
7117+
7118+
if sum_check:
7119+
dtype_res = xp.result_type(f_obs.dtype, f_exp.dtype)
7120+
rtol = xp.finfo(dtype_res).eps**0.5 # to pass existing tests
7121+
with np.errstate(invalid='ignore'):
7122+
f_obs_sum = _m_sum(f_obs_float, axis=axis, preserve_mask=False, xp=xp)
7123+
f_exp_sum = _m_sum(f_exp, axis=axis, preserve_mask=False, xp=xp)
7124+
relative_diff = (xp.abs(f_obs_sum - f_exp_sum) /
7125+
xp.minimum(f_obs_sum, f_exp_sum))
7126+
diff_gt_tol = xp.any(relative_diff > rtol, axis=None)
7127+
if diff_gt_tol:
7128+
msg = (f"For each axis slice, the sum of the observed "
7129+
f"frequencies must agree with the sum of the "
7130+
f"expected frequencies to a relative tolerance "
7131+
f"of {rtol}, but the percent differences are:\n"
7132+
f"{relative_diff}")
7133+
raise ValueError(msg)
71287134

71297135
else:
71307136
# Ignore 'invalid' errors so the edge case of a data set with length 0
@@ -7164,29 +7170,36 @@ def warn_masked(arg):
71647170
return Power_divergenceResult(stat, pvalue)
71657171

71667172

7167-
def chisquare(f_obs, f_exp=None, ddof=0, axis=0):
7168-
"""Calculate a one-way chi-square test.
7173+
def chisquare(f_obs, f_exp=None, ddof=0, axis=0, *, sum_check=True):
7174+
"""Perform Pearson's chi-squared test.
71697175
7170-
The chi-square test tests the null hypothesis that the categorical data
7171-
has the given frequencies.
7176+
Pearson's chi-squared test [1]_ is a goodness-of-fit test for a multinomial
7177+
distribution with given probabilities; that is, it assesses the null hypothesis
7178+
that the observed frequencies (counts) are obtained by independent
7179+
sampling of *N* observations from a categorical distribution with given
7180+
expected frequencies.
71727181
71737182
Parameters
71747183
----------
71757184
f_obs : array_like
71767185
Observed frequencies in each category.
71777186
f_exp : array_like, optional
7178-
Expected frequencies in each category. By default the categories are
7187+
Expected frequencies in each category. By default, the categories are
71797188
assumed to be equally likely.
71807189
ddof : int, optional
71817190
"Delta degrees of freedom": adjustment to the degrees of freedom
71827191
for the p-value. The p-value is computed using a chi-squared
7183-
distribution with ``k - 1 - ddof`` degrees of freedom, where `k`
7184-
is the number of observed frequencies. The default value of `ddof`
7185-
is 0.
7192+
distribution with ``k - 1 - ddof`` degrees of freedom, where ``k``
7193+
is the number of categories. The default value of `ddof` is 0.
71867194
axis : int or None, optional
71877195
The axis of the broadcast result of `f_obs` and `f_exp` along which to
71887196
apply the test. If axis is None, all values in `f_obs` are treated
71897197
as a single data set. Default is 0.
7198+
sum_check : bool, optional
7199+
Whether to perform a check that ``sum(f_obs) - sum(f_exp) == 0``. If True,
7200+
(default) raise an error when the relative difference exceeds the square root
7201+
of the precision of the data type. See Notes for rationale and possible
7202+
exceptions.
71907203
71917204
Returns
71927205
-------
@@ -7212,15 +7225,11 @@ def chisquare(f_obs, f_exp=None, ddof=0, axis=0):
72127225
-----
72137226
This test is invalid when the observed or expected frequencies in each
72147227
category are too small. A typical rule is that all of the observed
7215-
and expected frequencies should be at least 5. According to [3]_, the
7216-
total number of samples is recommended to be greater than 13,
7228+
and expected frequencies should be at least 5. According to [2]_, the
7229+
total number of observations is recommended to be greater than 13,
72177230
otherwise exact tests (such as Barnard's Exact test) should be used
72187231
because they do not overreject.
72197232
7220-
Also, the sum of the observed and expected frequencies must be the same
7221-
for the test to be valid; `chisquare` raises an error if the sums do not
7222-
agree within a relative tolerance of ``1e-8``.
7223-
72247233
The default degrees of freedom, k-1, are for the case when no parameters
72257234
of the distribution are estimated. If p parameters are estimated by
72267235
efficient maximum likelihood then the correct degrees of freedom are
@@ -7229,13 +7238,23 @@ def chisquare(f_obs, f_exp=None, ddof=0, axis=0):
72297238
the asymptotic distribution is not chi-square, in which case this test
72307239
is not appropriate.
72317240
7241+
For Pearson's chi-squared test, the total observed and expected counts must match
7242+
for the p-value to accurately reflect the probability of observing such an extreme
7243+
value of the statistic under the null hypothesis.
7244+
This function may be used to perform other statistical tests that do not require
7245+
the total counts to be equal. For instance, to test the null hypothesis that
7246+
``f_obs[i]`` is Poisson-distributed with expectation ``f_exp[i]``, set ``ddof=-1``
7247+
and ``sum_check=False``. This test follows from the fact that a Poisson random
7248+
variable with mean and variance ``f_exp[i]`` is approximately normal with the
7249+
same mean and variance; the chi-squared statistic standardizes, squares, and sums
7250+
the observations; and the sum of ``n`` squared standard normal variables follows
7251+
the chi-squared distribution with ``n`` degrees of freedom.
7252+
72327253
References
72337254
----------
7234-
.. [1] Lowry, Richard. "Concepts and Applications of Inferential
7235-
Statistics". Chapter 8.
7236-
https://web.archive.org/web/20171022032306/http://vassarstats.net:80/textbook/ch8pt1.html
7237-
.. [2] "Chi-squared test", https://en.wikipedia.org/wiki/Chi-squared_test
7238-
.. [3] Pearson, Karl. "On the criterion that a given system of deviations from the probable
7255+
.. [1] "Pearson's chi-squared test".
7256+
*Wikipedia*. https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test
7257+
.. [2] Pearson, Karl. "On the criterion that a given system of deviations from the probable
72397258
in the case of a correlated system of variables is such that it can be reasonably
72407259
supposed to have arisen from random sampling", Philosophical Magazine. Series 5. 50
72417260
(1900), pp. 157-175.
@@ -7295,8 +7314,8 @@ def chisquare(f_obs, f_exp=None, ddof=0, axis=0):
72957314
72967315
For a more detailed example, see :ref:`hypothesis_chisquare`.
72977316
""" # noqa: E501
7298-
return power_divergence(f_obs, f_exp=f_exp, ddof=ddof, axis=axis,
7299-
lambda_="pearson")
7317+
return _power_divergence(f_obs, f_exp=f_exp, ddof=ddof, axis=axis,
7318+
lambda_="pearson", sum_check=sum_check)
73007319

73017320

73027321
KstestResult = _make_tuple_bunch('KstestResult', ['statistic', 'pvalue'],

scipy/stats/tests/test_stats.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
too_small_nd_omit, too_small_nd_not_omit,
3939
too_small_1d_omit, too_small_1d_not_omit)
4040
from scipy.stats._stats_py import (_permutation_distribution_t, _chk_asarray, _moment,
41-
LinregressResult, _xp_mean, _xp_var)
41+
LinregressResult, _xp_mean, _xp_var, _SimpleChi2)
4242
from scipy._lib._util import AxisError
4343
from scipy.conftest import array_api_compatible, skip_xp_invalid_arg
4444
from scipy._lib._array_api import (array_namespace, xp_copy, is_numpy,
@@ -4327,7 +4327,7 @@ def test_power_divergence_against_cressie_read_data(self, xp):
43274327

43284328
@array_api_compatible
43294329
class TestChisquare:
4330-
def test_gh_chisquare_12282(self, xp):
4330+
def test_chisquare_12282a(self, xp):
43314331
# Currently `chisquare` is implemented via power_divergence
43324332
# in case that ever changes, perform a basic test like
43334333
# test_power_divergence_gh_12282
@@ -4336,6 +4336,25 @@ def test_gh_chisquare_12282(self, xp):
43364336
f_exp = xp.asarray([30., 60.])
43374337
stats.chisquare(f_obs=f_obs, f_exp=f_exp)
43384338

4339+
def test_chisquare_12282b(self, xp):
4340+
# Check that users can now disable the sum check tested in
4341+
# test_chisquare_12282a. Also, confirm that statistic and p-value
4342+
# are as expected.
4343+
rng = np.random.default_rng(3843874358728234)
4344+
n = 10
4345+
lam = rng.uniform(1000, 2000, size=n)
4346+
x = rng.poisson(lam)
4347+
lam = xp.asarray(lam)
4348+
x = xp.asarray(x, dtype=lam.dtype)
4349+
res = stats.chisquare(f_obs=x, f_exp=lam, ddof=-1, sum_check=False)
4350+
# Poisson is approximately normal with mean and variance lam
4351+
z = (x - lam) / xp.sqrt(lam)
4352+
statistic = xp.sum(z**2)
4353+
xp_assert_close(res.statistic, statistic)
4354+
# Sum of `n` squared standard normal variables follows chi2 with `n` DoF
4355+
X2 = _SimpleChi2(xp.asarray(n, dtype=statistic.dtype))
4356+
xp_assert_close(res.pvalue, X2.sf(statistic))
4357+
43394358
@pytest.mark.parametrize("n, dtype", [(200, 'uint8'), (1000000, 'int32')])
43404359
def test_chiquare_data_types_attributes(self, n, dtype, xp):
43414360
# Regression test for gh-10159 and gh-18368

0 commit comments

Comments
 (0)