contingency.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. """
  2. Contingency table functions (:mod:`scipy.stats.contingency`)
  3. ============================================================
  4. Functions for creating and analyzing contingency tables.
  5. .. currentmodule:: scipy.stats.contingency
  6. .. autosummary::
  7. :toctree: generated/
  8. chi2_contingency
  9. relative_risk
  10. odds_ratio
  11. crosstab
  12. association
  13. expected_freq
  14. margins
  15. """
  16. from functools import reduce
  17. import math
  18. import numpy as np
  19. from ._stats_py import power_divergence, _untabulate
  20. from ._relative_risk import relative_risk
  21. from ._crosstab import crosstab
  22. from ._odds_ratio import odds_ratio
  23. from scipy._lib._array_api import xp_capabilities
  24. from scipy._lib._bunch import _make_tuple_bunch
  25. from scipy import stats
  26. __all__ = ['margins', 'expected_freq', 'chi2_contingency', 'crosstab',
  27. 'association', 'relative_risk', 'odds_ratio']
  28. @xp_capabilities(np_only=True)
  29. def margins(a):
  30. """Return a list of the marginal sums of the array `a`.
  31. Parameters
  32. ----------
  33. a : ndarray
  34. The array for which to compute the marginal sums.
  35. Returns
  36. -------
  37. margsums : list of ndarrays
  38. A list of length `a.ndim`. `margsums[k]` is the result
  39. of summing `a` over all axes except `k`; it has the same
  40. number of dimensions as `a`, but the length of each axis
  41. except axis `k` will be 1.
  42. Examples
  43. --------
  44. >>> import numpy as np
  45. >>> from scipy.stats.contingency import margins
  46. >>> a = np.arange(12).reshape(2, 6)
  47. >>> a
  48. array([[ 0, 1, 2, 3, 4, 5],
  49. [ 6, 7, 8, 9, 10, 11]])
  50. >>> m0, m1 = margins(a)
  51. >>> m0
  52. array([[15],
  53. [51]])
  54. >>> m1
  55. array([[ 6, 8, 10, 12, 14, 16]])
  56. >>> b = np.arange(24).reshape(2,3,4)
  57. >>> m0, m1, m2 = margins(b)
  58. >>> m0
  59. array([[[ 66]],
  60. [[210]]])
  61. >>> m1
  62. array([[[ 60],
  63. [ 92],
  64. [124]]])
  65. >>> m2
  66. array([[[60, 66, 72, 78]]])
  67. """
  68. margsums = []
  69. ranged = list(range(a.ndim))
  70. for k in ranged:
  71. marg = np.apply_over_axes(np.sum, a, [j for j in ranged if j != k])
  72. margsums.append(marg)
  73. return margsums
  74. @xp_capabilities(np_only=True)
  75. def expected_freq(observed):
  76. """
  77. Compute the expected frequencies from a contingency table.
  78. Given an n-dimensional contingency table of observed frequencies,
  79. compute the expected frequencies for the table based on the marginal
  80. sums under the assumption that the groups associated with each
  81. dimension are independent.
  82. Parameters
  83. ----------
  84. observed : array_like
  85. The table of observed frequencies. (While this function can handle
  86. a 1-D array, that case is trivial. Generally `observed` is at
  87. least 2-D.)
  88. Returns
  89. -------
  90. expected : ndarray of float64
  91. The expected frequencies, based on the marginal sums of the table.
  92. Same shape as `observed`.
  93. Examples
  94. --------
  95. >>> import numpy as np
  96. >>> from scipy.stats.contingency import expected_freq
  97. >>> observed = np.array([[10, 10, 20],[20, 20, 20]])
  98. >>> expected_freq(observed)
  99. array([[ 12., 12., 16.],
  100. [ 18., 18., 24.]])
  101. """
  102. # Typically `observed` is an integer array. If `observed` has a large
  103. # number of dimensions or holds large values, some of the following
  104. # computations may overflow, so we first switch to floating point.
  105. observed = np.asarray(observed, dtype=np.float64)
  106. # Create a list of the marginal sums.
  107. margsums = margins(observed)
  108. # Create the array of expected frequencies. The shapes of the
  109. # marginal sums returned by apply_over_axes() are just what we
  110. # need for broadcasting in the following product.
  111. d = observed.ndim
  112. expected = reduce(np.multiply, margsums) / observed.sum() ** (d - 1)
  113. return expected
  114. Chi2ContingencyResult = _make_tuple_bunch(
  115. 'Chi2ContingencyResult',
  116. ['statistic', 'pvalue', 'dof', 'expected_freq'], []
  117. )
  118. @xp_capabilities(np_only=True)
  119. def chi2_contingency(observed, correction=True, lambda_=None, *, method=None):
  120. """Chi-square test of independence of variables in a contingency table.
  121. This function computes the chi-square statistic and p-value for the
  122. hypothesis test of independence of the observed frequencies in the
  123. contingency table [1]_ `observed`. The expected frequencies are computed
  124. based on the marginal sums under the assumption of independence; see
  125. `scipy.stats.contingency.expected_freq`. The number of degrees of
  126. freedom is (expressed using numpy functions and attributes)::
  127. dof = observed.size - sum(observed.shape) + observed.ndim - 1
  128. Parameters
  129. ----------
  130. observed : array_like
  131. The contingency table. The table contains the observed frequencies
  132. (i.e. number of occurrences) in each category. In the two-dimensional
  133. case, the table is often described as an "R x C table".
  134. correction : bool, optional
  135. If True, *and* the degrees of freedom is 1, apply Yates' correction
  136. for continuity. The effect of the correction is to adjust each
  137. observed value by 0.5 towards the corresponding expected value.
  138. lambda_ : float or str, optional
  139. By default, the statistic computed in this test is Pearson's
  140. chi-squared statistic [2]_. `lambda_` allows a statistic from the
  141. Cressie-Read power divergence family [3]_ to be used instead. See
  142. `scipy.stats.power_divergence` for details.
  143. method : ResamplingMethod, optional
  144. Defines the method used to compute the p-value. Compatible only with
  145. `correction=False`, default `lambda_`, and two-way tables.
  146. If `method` is an instance of `PermutationMethod`/`MonteCarloMethod`,
  147. the p-value is computed using
  148. `scipy.stats.permutation_test`/`scipy.stats.monte_carlo_test` with the
  149. provided configuration options and other appropriate settings.
  150. Otherwise, the p-value is computed as documented in the notes.
  151. Note that if `method` is an instance of `MonteCarloMethod`, the ``rvs``
  152. attribute must be left unspecified; Monte Carlo samples are always drawn
  153. using the ``rvs`` method of `scipy.stats.random_table`.
  154. .. versionadded:: 1.15.0
  155. Returns
  156. -------
  157. res : Chi2ContingencyResult
  158. An object containing attributes:
  159. statistic : float
  160. The test statistic.
  161. pvalue : float
  162. The p-value of the test.
  163. dof : int
  164. The degrees of freedom. NaN if `method` is not ``None``.
  165. expected_freq : ndarray, same shape as `observed`
  166. The expected frequencies, based on the marginal sums of the table.
  167. See Also
  168. --------
  169. scipy.stats.contingency.expected_freq
  170. scipy.stats.fisher_exact
  171. scipy.stats.chisquare
  172. scipy.stats.power_divergence
  173. scipy.stats.barnard_exact
  174. scipy.stats.boschloo_exact
  175. :ref:`hypothesis_chi2_contingency` : Extended example
  176. Notes
  177. -----
  178. An often quoted guideline for the validity of this calculation is that
  179. the test should be used only if the observed and expected frequencies
  180. in each cell are at least 5.
  181. This is a test for the independence of different categories of a
  182. population. The test is only meaningful when the dimension of
  183. `observed` is two or more. Applying the test to a one-dimensional
  184. table will always result in `expected` equal to `observed` and a
  185. chi-square statistic equal to 0.
  186. This function does not handle masked arrays, because the calculation
  187. does not make sense with missing values.
  188. Like `scipy.stats.chisquare`, this function computes a chi-square
  189. statistic; the convenience this function provides is to figure out the
  190. expected frequencies and degrees of freedom from the given contingency
  191. table. If these were already known, and if the Yates' correction was not
  192. required, one could use `scipy.stats.chisquare`. That is, if one calls::
  193. res = chi2_contingency(obs, correction=False)
  194. then the following is true::
  195. (res.statistic, res.pvalue) == stats.chisquare(obs.ravel(),
  196. f_exp=ex.ravel(),
  197. ddof=obs.size - 1 - dof)
  198. The `lambda_` argument was added in version 0.13.0 of scipy.
  199. References
  200. ----------
  201. .. [1] "Contingency table",
  202. https://en.wikipedia.org/wiki/Contingency_table
  203. .. [2] "Pearson's chi-squared test",
  204. https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test
  205. .. [3] Cressie, N. and Read, T. R. C., "Multinomial Goodness-of-Fit
  206. Tests", J. Royal Stat. Soc. Series B, Vol. 46, No. 3 (1984),
  207. pp. 440-464.
  208. Examples
  209. --------
  210. A two-way example (2 x 3):
  211. >>> import numpy as np
  212. >>> from scipy.stats import chi2_contingency
  213. >>> obs = np.array([[10, 10, 20], [20, 20, 20]])
  214. >>> res = chi2_contingency(obs)
  215. >>> res.statistic
  216. 2.7777777777777777
  217. >>> res.pvalue
  218. 0.24935220877729619
  219. >>> res.dof
  220. 2
  221. >>> res.expected_freq
  222. array([[ 12., 12., 16.],
  223. [ 18., 18., 24.]])
  224. Perform the test using the log-likelihood ratio (i.e. the "G-test")
  225. instead of Pearson's chi-squared statistic.
  226. >>> res = chi2_contingency(obs, lambda_="log-likelihood")
  227. >>> res.statistic
  228. 2.7688587616781319
  229. >>> res.pvalue
  230. 0.25046668010954165
  231. A four-way example (2 x 2 x 2 x 2):
  232. >>> obs = np.array(
  233. ... [[[[12, 17],
  234. ... [11, 16]],
  235. ... [[11, 12],
  236. ... [15, 16]]],
  237. ... [[[23, 15],
  238. ... [30, 22]],
  239. ... [[14, 17],
  240. ... [15, 16]]]])
  241. >>> res = chi2_contingency(obs)
  242. >>> res.statistic
  243. 8.7584514426741897
  244. >>> res.pvalue
  245. 0.64417725029295503
  246. When the sum of the elements in a two-way table is small, the p-value
  247. produced by the default asymptotic approximation may be inaccurate.
  248. Consider passing a `PermutationMethod` or `MonteCarloMethod` as the
  249. `method` parameter with `correction=False`.
  250. >>> from scipy.stats import PermutationMethod
  251. >>> obs = np.asarray([[12, 3],
  252. ... [17, 16]])
  253. >>> res = chi2_contingency(obs, correction=False)
  254. >>> ref = chi2_contingency(obs, correction=False, method=PermutationMethod())
  255. >>> res.pvalue, ref.pvalue
  256. (0.0614122539870913, 0.1074) # may vary
  257. For a more detailed example, see :ref:`hypothesis_chi2_contingency`.
  258. """
  259. observed = np.asarray(observed)
  260. if np.any(observed < 0):
  261. raise ValueError("All values in `observed` must be nonnegative.")
  262. if observed.size == 0:
  263. raise ValueError("No data; `observed` has size 0.")
  264. expected = expected_freq(observed)
  265. if np.any(expected == 0):
  266. # Include one of the positions where expected is zero in
  267. # the exception message.
  268. zeropos = list(zip(*np.nonzero(expected == 0)))[0]
  269. raise ValueError("The internally computed table of expected "
  270. f"frequencies has a zero element at {zeropos}.")
  271. if method is not None:
  272. return _chi2_resampling_methods(observed, expected, correction, lambda_, method)
  273. # The degrees of freedom
  274. dof = expected.size - sum(expected.shape) + expected.ndim - 1
  275. if dof == 0:
  276. # Degenerate case; this occurs when `observed` is 1D (or, more
  277. # generally, when it has only one nontrivial dimension). In this
  278. # case, we also have observed == expected, so chi2 is 0.
  279. chi2 = 0.0
  280. p = 1.0
  281. else:
  282. if dof == 1 and correction:
  283. # Adjust `observed` according to Yates' correction for continuity.
  284. # Magnitude of correction no bigger than difference; see gh-13875
  285. diff = expected - observed
  286. direction = np.sign(diff)
  287. magnitude = np.minimum(0.5, np.abs(diff))
  288. observed = observed + magnitude * direction
  289. chi2, p = power_divergence(observed, expected,
  290. ddof=observed.size - 1 - dof, axis=None,
  291. lambda_=lambda_)
  292. return Chi2ContingencyResult(chi2, p, dof, expected)
  293. def _chi2_resampling_methods(observed, expected, correction, lambda_, method):
  294. if observed.ndim != 2:
  295. message = 'Use of `method` is only compatible with two-way tables.'
  296. raise ValueError(message)
  297. if correction:
  298. message = f'`{correction=}` is not compatible with `{method=}.`'
  299. raise ValueError(message)
  300. if lambda_ is not None:
  301. message = f'`{lambda_=}` is not compatible with `{method=}.`'
  302. raise ValueError(message)
  303. if isinstance(method, stats.PermutationMethod):
  304. res = _chi2_permutation_method(observed, expected, method)
  305. elif isinstance(method, stats.MonteCarloMethod):
  306. res = _chi2_monte_carlo_method(observed, expected, method)
  307. else:
  308. message = (f'`{method=}` not recognized; if provided, `method` must be an '
  309. 'instance of `PermutationMethod` or `MonteCarloMethod`.')
  310. raise ValueError(message)
  311. return Chi2ContingencyResult(res.statistic, res.pvalue, np.nan, expected)
  312. def _chi2_permutation_method(observed, expected, method):
  313. x, y = _untabulate(observed)
  314. # `permutation_test` with `permutation_type='pairings' permutes the order of `x`,
  315. # which pairs observations in `x` with different observations in `y`.
  316. def statistic(x):
  317. # crosstab the resample and compute the statistic
  318. table = crosstab(x, y)[1]
  319. return np.sum((table - expected)**2/expected)
  320. return stats.permutation_test((x,), statistic, permutation_type='pairings',
  321. alternative='greater', **method._asdict())
  322. def _chi2_monte_carlo_method(observed, expected, method):
  323. method = method._asdict()
  324. if method.pop('rvs', None) is not None:
  325. message = ('If the `method` argument of `chi2_contingency` is an '
  326. 'instance of `MonteCarloMethod`, its `rvs` attribute '
  327. 'must be unspecified. Use the `MonteCarloMethod` `rng` argument '
  328. 'to control the random state.')
  329. raise ValueError(message)
  330. rng = np.random.default_rng(method.pop('rng', None))
  331. # `random_table.rvs` produces random contingency tables with the given marginals
  332. # under the null hypothesis of independence
  333. rowsums, colsums = stats.contingency.margins(observed)
  334. X = stats.random_table(rowsums.ravel(), colsums.ravel(), seed=rng)
  335. def rvs(size):
  336. n_resamples = size[0]
  337. return X.rvs(size=n_resamples).reshape(size)
  338. expected = expected.ravel()
  339. def statistic(table, axis):
  340. return np.sum((table - expected)**2/expected, axis=axis)
  341. return stats.monte_carlo_test(observed.ravel(), rvs, statistic,
  342. alternative='greater', **method)
  343. @xp_capabilities(np_only=True)
  344. def association(observed, method="cramer", correction=False, lambda_=None):
  345. """Calculates degree of association between two nominal variables.
  346. The function provides the option for computing one of three measures of
  347. association between two nominal variables from the data given in a 2d
  348. contingency table: Tschuprow's T, Pearson's Contingency Coefficient
  349. and Cramer's V.
  350. Parameters
  351. ----------
  352. observed : array-like
  353. The array of observed values
  354. method : {"cramer", "tschuprow", "pearson"} (default = "cramer")
  355. The association test statistic.
  356. correction : bool, optional
  357. Inherited from `scipy.stats.contingency.chi2_contingency()`
  358. lambda_ : float or str, optional
  359. Inherited from `scipy.stats.contingency.chi2_contingency()`
  360. Returns
  361. -------
  362. statistic : float
  363. Value of the test statistic
  364. Notes
  365. -----
  366. Cramer's V, Tschuprow's T and Pearson's Contingency Coefficient, all
  367. measure the degree to which two nominal or ordinal variables are related,
  368. or the level of their association. This differs from correlation, although
  369. many often mistakenly consider them equivalent. Correlation measures in
  370. what way two variables are related, whereas, association measures how
  371. related the variables are. As such, association does not subsume
  372. independent variables, and is rather a test of independence. A value of
  373. 1.0 indicates perfect association, and 0.0 means the variables have no
  374. association.
  375. Both the Cramer's V and Tschuprow's T are extensions of the phi
  376. coefficient. Moreover, due to the close relationship between the
  377. Cramer's V and Tschuprow's T the returned values can often be similar
  378. or even equivalent. They are likely to diverge more as the array shape
  379. diverges from a 2x2.
  380. References
  381. ----------
  382. .. [1] "Tschuprow's T",
  383. https://en.wikipedia.org/wiki/Tschuprow's_T
  384. .. [2] Tschuprow, A. A. (1939)
  385. Principles of the Mathematical Theory of Correlation;
  386. translated by M. Kantorowitsch. W. Hodge & Co.
  387. .. [3] "Cramer's V", https://en.wikipedia.org/wiki/Cramer's_V
  388. .. [4] "Nominal Association: Phi and Cramer's V",
  389. http://www.people.vcu.edu/~pdattalo/702SuppRead/MeasAssoc/NominalAssoc.html
  390. .. [5] Gingrich, Paul, "Association Between Variables",
  391. http://uregina.ca/~gingrich/ch11a.pdf
  392. Examples
  393. --------
  394. An example with a 4x2 contingency table:
  395. >>> import numpy as np
  396. >>> from scipy.stats.contingency import association
  397. >>> obs4x2 = np.array([[100, 150], [203, 322], [420, 700], [320, 210]])
  398. Pearson's contingency coefficient
  399. >>> association(obs4x2, method="pearson")
  400. 0.18303298140595667
  401. Cramer's V
  402. >>> association(obs4x2, method="cramer")
  403. 0.18617813077483678
  404. Tschuprow's T
  405. >>> association(obs4x2, method="tschuprow")
  406. 0.14146478765062995
  407. """
  408. arr = np.asarray(observed)
  409. if not np.issubdtype(arr.dtype, np.integer):
  410. raise ValueError("`observed` must be an integer array.")
  411. if len(arr.shape) != 2:
  412. raise ValueError("method only accepts 2d arrays")
  413. chi2_stat = chi2_contingency(arr, correction=correction,
  414. lambda_=lambda_)
  415. phi2 = chi2_stat.statistic / arr.sum()
  416. n_rows, n_cols = arr.shape
  417. if method == "cramer":
  418. value = phi2 / min(n_cols - 1, n_rows - 1)
  419. elif method == "tschuprow":
  420. value = phi2 / math.sqrt((n_rows - 1) * (n_cols - 1))
  421. elif method == 'pearson':
  422. value = phi2 / (1 + phi2)
  423. else:
  424. raise ValueError("Invalid argument value: 'method' argument must "
  425. "be 'cramer', 'tschuprow', or 'pearson'")
  426. return math.sqrt(value)