Coverage for /usr/lib/python3/dist-packages/scipy/stats/_crosstab.py: 13%
39 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1import numpy as np
2from scipy.sparse import coo_matrix
3from scipy._lib._bunch import _make_tuple_bunch
6CrosstabResult = _make_tuple_bunch(
7 "CrosstabResult", ["elements", "count"]
8)
11def crosstab(*args, levels=None, sparse=False):
12 """
13 Return table of counts for each possible unique combination in ``*args``.
15 When ``len(args) > 1``, the array computed by this function is
16 often referred to as a *contingency table* [1]_.
18 The arguments must be sequences with the same length. The second return
19 value, `count`, is an integer array with ``len(args)`` dimensions. If
20 `levels` is None, the shape of `count` is ``(n0, n1, ...)``, where ``nk``
21 is the number of unique elements in ``args[k]``.
23 Parameters
24 ----------
25 *args : sequences
26 A sequence of sequences whose unique aligned elements are to be
27 counted. The sequences in args must all be the same length.
28 levels : sequence, optional
29 If `levels` is given, it must be a sequence that is the same length as
30 `args`. Each element in `levels` is either a sequence or None. If it
31 is a sequence, it gives the values in the corresponding sequence in
32 `args` that are to be counted. If any value in the sequences in `args`
33 does not occur in the corresponding sequence in `levels`, that value
34 is ignored and not counted in the returned array `count`. The default
35 value of `levels` for ``args[i]`` is ``np.unique(args[i])``
36 sparse : bool, optional
37 If True, return a sparse matrix. The matrix will be an instance of
38 the `scipy.sparse.coo_matrix` class. Because SciPy's sparse matrices
39 must be 2-d, only two input sequences are allowed when `sparse` is
40 True. Default is False.
42 Returns
43 -------
44 res : CrosstabResult
45 An object containing the following attributes:
47 elements : tuple of numpy.ndarrays.
48 Tuple of length ``len(args)`` containing the arrays of elements
49 that are counted in `count`. These can be interpreted as the
50 labels of the corresponding dimensions of `count`. If `levels` was
51 given, then if ``levels[i]`` is not None, ``elements[i]`` will
52 hold the values given in ``levels[i]``.
53 count : numpy.ndarray or scipy.sparse.coo_matrix
54 Counts of the unique elements in ``zip(*args)``, stored in an
55 array. Also known as a *contingency table* when ``len(args) > 1``.
57 See Also
58 --------
59 numpy.unique
61 Notes
62 -----
63 .. versionadded:: 1.7.0
65 References
66 ----------
67 .. [1] "Contingency table", http://en.wikipedia.org/wiki/Contingency_table
69 Examples
70 --------
71 >>> from scipy.stats.contingency import crosstab
73 Given the lists `a` and `x`, create a contingency table that counts the
74 frequencies of the corresponding pairs.
76 >>> a = ['A', 'B', 'A', 'A', 'B', 'B', 'A', 'A', 'B', 'B']
77 >>> x = ['X', 'X', 'X', 'Y', 'Z', 'Z', 'Y', 'Y', 'Z', 'Z']
78 >>> res = crosstab(a, x)
79 >>> avals, xvals = res.elements
80 >>> avals
81 array(['A', 'B'], dtype='<U1')
82 >>> xvals
83 array(['X', 'Y', 'Z'], dtype='<U1')
84 >>> res.count
85 array([[2, 3, 0],
86 [1, 0, 4]])
88 So `('A', 'X')` occurs twice, `('A', 'Y')` occurs three times, etc.
90 Higher dimensional contingency tables can be created.
92 >>> p = [0, 0, 0, 0, 1, 1, 1, 0, 0, 1]
93 >>> res = crosstab(a, x, p)
94 >>> res.count
95 array([[[2, 0],
96 [2, 1],
97 [0, 0]],
98 [[1, 0],
99 [0, 0],
100 [1, 3]]])
101 >>> res.count.shape
102 (2, 3, 2)
104 The values to be counted can be set by using the `levels` argument.
105 It allows the elements of interest in each input sequence to be
106 given explicitly instead finding the unique elements of the sequence.
108 For example, suppose one of the arguments is an array containing the
109 answers to a survey question, with integer values 1 to 4. Even if the
110 value 1 does not occur in the data, we want an entry for it in the table.
112 >>> q1 = [2, 3, 3, 2, 4, 4, 2, 3, 4, 4, 4, 3, 3, 3, 4] # 1 does not occur.
113 >>> q2 = [4, 4, 2, 2, 2, 4, 1, 1, 2, 2, 4, 2, 2, 2, 4] # 3 does not occur.
114 >>> options = [1, 2, 3, 4]
115 >>> res = crosstab(q1, q2, levels=(options, options))
116 >>> res.count
117 array([[0, 0, 0, 0],
118 [1, 1, 0, 1],
119 [1, 4, 0, 1],
120 [0, 3, 0, 3]])
122 If `levels` is given, but an element of `levels` is None, the unique values
123 of the corresponding argument are used. For example,
125 >>> res = crosstab(q1, q2, levels=(None, options))
126 >>> res.elements
127 [array([2, 3, 4]), [1, 2, 3, 4]]
128 >>> res.count
129 array([[1, 1, 0, 1],
130 [1, 4, 0, 1],
131 [0, 3, 0, 3]])
133 If we want to ignore the pairs where 4 occurs in ``q2``, we can
134 give just the values [1, 2] to `levels`, and the 4 will be ignored:
136 >>> res = crosstab(q1, q2, levels=(None, [1, 2]))
137 >>> res.elements
138 [array([2, 3, 4]), [1, 2]]
139 >>> res.count
140 array([[1, 1],
141 [1, 4],
142 [0, 3]])
144 Finally, let's repeat the first example, but return a sparse matrix:
146 >>> res = crosstab(a, x, sparse=True)
147 >>> res.count
148 <2x3 sparse matrix of type '<class 'numpy.int64'>'
149 with 4 stored elements in COOrdinate format>
150 >>> res.count.A
151 array([[2, 3, 0],
152 [1, 0, 4]])
154 """
155 nargs = len(args)
156 if nargs == 0:
157 raise TypeError("At least one input sequence is required.")
159 len0 = len(args[0])
160 if not all(len(a) == len0 for a in args[1:]):
161 raise ValueError("All input sequences must have the same length.")
163 if sparse and nargs != 2:
164 raise ValueError("When `sparse` is True, only two input sequences "
165 "are allowed.")
167 if levels is None:
168 # Call np.unique with return_inverse=True on each argument.
169 actual_levels, indices = zip(*[np.unique(a, return_inverse=True)
170 for a in args])
171 else:
172 # `levels` is not None...
173 if len(levels) != nargs:
174 raise ValueError('len(levels) must equal the number of input '
175 'sequences')
177 args = [np.asarray(arg) for arg in args]
178 mask = np.zeros((nargs, len0), dtype=np.bool_)
179 inv = np.zeros((nargs, len0), dtype=np.intp)
180 actual_levels = []
181 for k, (levels_list, arg) in enumerate(zip(levels, args)):
182 if levels_list is None:
183 levels_list, inv[k, :] = np.unique(arg, return_inverse=True)
184 mask[k, :] = True
185 else:
186 q = arg == np.asarray(levels_list).reshape(-1, 1)
187 mask[k, :] = np.any(q, axis=0)
188 qnz = q.T.nonzero()
189 inv[k, qnz[0]] = qnz[1]
190 actual_levels.append(levels_list)
192 mask_all = mask.all(axis=0)
193 indices = tuple(inv[:, mask_all])
195 if sparse:
196 count = coo_matrix((np.ones(len(indices[0]), dtype=int),
197 (indices[0], indices[1])))
198 count.sum_duplicates()
199 else:
200 shape = [len(u) for u in actual_levels]
201 count = np.zeros(shape, dtype=int)
202 np.add.at(count, indices, 1)
204 return CrosstabResult(actual_levels, count)