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

1import numpy as np 

2from scipy.sparse import coo_matrix 

3from scipy._lib._bunch import _make_tuple_bunch 

4 

5 

6CrosstabResult = _make_tuple_bunch( 

7 "CrosstabResult", ["elements", "count"] 

8) 

9 

10 

11def crosstab(*args, levels=None, sparse=False): 

12 """ 

13 Return table of counts for each possible unique combination in ``*args``. 

14 

15 When ``len(args) > 1``, the array computed by this function is 

16 often referred to as a *contingency table* [1]_. 

17 

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]``. 

22 

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. 

41 

42 Returns 

43 ------- 

44 res : CrosstabResult 

45 An object containing the following attributes: 

46 

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``. 

56 

57 See Also 

58 -------- 

59 numpy.unique 

60 

61 Notes 

62 ----- 

63 .. versionadded:: 1.7.0 

64 

65 References 

66 ---------- 

67 .. [1] "Contingency table", http://en.wikipedia.org/wiki/Contingency_table 

68 

69 Examples 

70 -------- 

71 >>> from scipy.stats.contingency import crosstab 

72 

73 Given the lists `a` and `x`, create a contingency table that counts the 

74 frequencies of the corresponding pairs. 

75 

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]]) 

87 

88 So `('A', 'X')` occurs twice, `('A', 'Y')` occurs three times, etc. 

89 

90 Higher dimensional contingency tables can be created. 

91 

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) 

103 

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. 

107 

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. 

111 

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]]) 

121 

122 If `levels` is given, but an element of `levels` is None, the unique values 

123 of the corresponding argument are used. For example, 

124 

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]]) 

132 

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: 

135 

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]]) 

143 

144 Finally, let's repeat the first example, but return a sparse matrix: 

145 

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]]) 

153 

154 """ 

155 nargs = len(args) 

156 if nargs == 0: 

157 raise TypeError("At least one input sequence is required.") 

158 

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.") 

162 

163 if sparse and nargs != 2: 

164 raise ValueError("When `sparse` is True, only two input sequences " 

165 "are allowed.") 

166 

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') 

176 

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) 

191 

192 mask_all = mask.all(axis=0) 

193 indices = tuple(inv[:, mask_all]) 

194 

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) 

203 

204 return CrosstabResult(actual_levels, count)