Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/groupby/indexing.py: 26%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

109 statements  

1from __future__ import annotations 

2 

3from typing import ( 

4 TYPE_CHECKING, 

5 Literal, 

6 cast, 

7) 

8import warnings 

9 

10import numpy as np 

11 

12from pandas.errors import Pandas4Warning 

13from pandas.util._decorators import cache_readonly 

14from pandas.util._exceptions import find_stack_level 

15 

16from pandas.core.dtypes.common import ( 

17 is_integer, 

18 is_list_like, 

19) 

20 

21if TYPE_CHECKING: 

22 from collections.abc import Iterable 

23 

24 from pandas._typing import PositionalIndexer 

25 

26 from pandas import ( 

27 DataFrame, 

28 Series, 

29 ) 

30 from pandas.core.groupby import groupby 

31 

32 

33class GroupByIndexingMixin: 

34 """ 

35 Mixin for adding ._positional_selector to GroupBy. 

36 """ 

37 

38 @cache_readonly 

39 def _positional_selector(self) -> GroupByPositionalSelector: 

40 """ 

41 Return positional selection for each group. 

42 

43 ``groupby._positional_selector[i:j]`` is similar to 

44 ``groupby.apply(lambda x: x.iloc[i:j])`` 

45 but much faster and preserves the original index and order. 

46 

47 ``_positional_selector[]`` is compatible with and extends :meth:`~GroupBy.head` 

48 and :meth:`~GroupBy.tail`. For example: 

49 

50 - ``head(5)`` 

51 - ``_positional_selector[5:-5]`` 

52 - ``tail(5)`` 

53 

54 together return all the rows. 

55 

56 Allowed inputs for the index are: 

57 

58 - An integer valued iterable, e.g. ``range(2, 4)``. 

59 - A comma separated list of integers and slices, e.g. ``5``, ``2, 4``, ``2:4``. 

60 

61 The output format is the same as :meth:`~GroupBy.head` and 

62 :meth:`~GroupBy.tail`, namely 

63 a subset of the ``DataFrame`` or ``Series`` with the index and order preserved. 

64 

65 Returns 

66 ------- 

67 Series 

68 The filtered subset of the original Series. 

69 DataFrame 

70 The filtered subset of the original DataFrame. 

71 

72 See Also 

73 -------- 

74 DataFrame.iloc : Purely integer-location based indexing for selection by 

75 position. 

76 GroupBy.head : Return first n rows of each group. 

77 GroupBy.tail : Return last n rows of each group. 

78 GroupBy.nth : Take the nth row from each group if n is an int, or a 

79 subset of rows, if n is a list of ints. 

80 

81 Notes 

82 ----- 

83 - The slice step cannot be negative. 

84 - If the index specification results in overlaps, the item is not duplicated. 

85 - If the index specification changes the order of items, then 

86 they are returned in their original order. 

87 By contrast, ``DataFrame.iloc`` can change the row order. 

88 - ``groupby()`` parameters such as as_index and dropna are ignored. 

89 

90 The differences between ``_positional_selector[]`` and :meth:`~GroupBy.nth` 

91 with ``as_index=False`` are: 

92 

93 - Input to ``_positional_selector`` can include 

94 one or more slices whereas ``nth`` 

95 just handles an integer or a list of integers. 

96 - ``_positional_selector`` can accept a slice relative to the 

97 last row of each group. 

98 - ``_positional_selector`` does not have an equivalent to the 

99 ``nth()`` ``dropna`` parameter. 

100 

101 Examples 

102 -------- 

103 >>> df = pd.DataFrame( 

104 ... [["a", 1], ["a", 2], ["a", 3], ["b", 4], ["b", 5]], columns=["A", "B"] 

105 ... ) 

106 >>> df.groupby("A")._positional_selector[1:2] 

107 A B 

108 1 a 2 

109 4 b 5 

110 

111 >>> df.groupby("A")._positional_selector[1, -1] 

112 A B 

113 1 a 2 

114 2 a 3 

115 4 b 5 

116 """ 

117 if TYPE_CHECKING: 

118 groupby_self = cast("groupby.GroupBy", self) 

119 else: 

120 groupby_self = self 

121 

122 return GroupByPositionalSelector(groupby_self) 

123 

124 def _make_mask_from_positional_indexer( 

125 self, 

126 arg: PositionalIndexer | tuple, 

127 ) -> np.ndarray: 

128 if is_list_like(arg): 

129 if all(is_integer(i) for i in cast("Iterable", arg)): 

130 mask = self._make_mask_from_list(cast("Iterable[int]", arg)) 

131 else: 

132 mask = self._make_mask_from_tuple(cast("tuple", arg)) 

133 

134 elif isinstance(arg, slice): 

135 mask = self._make_mask_from_slice(arg) 

136 elif is_integer(arg): 

137 mask = self._make_mask_from_int(cast("int", arg)) 

138 else: 

139 raise TypeError( 

140 f"Invalid index {type(arg)}. " 

141 "Must be integer, list-like, slice or a tuple of " 

142 "integers and slices" 

143 ) 

144 

145 if isinstance(mask, bool): 

146 if mask: 

147 mask = self._ascending_count >= 0 

148 else: 

149 mask = self._ascending_count < 0 

150 

151 return cast("np.ndarray", mask) 

152 

153 def _make_mask_from_int(self, arg: int) -> np.ndarray: 

154 if arg >= 0: 

155 return self._ascending_count == arg 

156 else: 

157 return self._descending_count == (-arg - 1) 

158 

159 def _make_mask_from_list(self, args: Iterable[int]) -> bool | np.ndarray: 

160 positive = [arg for arg in args if arg >= 0] 

161 negative = [-arg - 1 for arg in args if arg < 0] 

162 

163 mask: bool | np.ndarray = False 

164 

165 if positive: 

166 mask |= np.isin(self._ascending_count, positive) 

167 

168 if negative: 

169 mask |= np.isin(self._descending_count, negative) 

170 

171 return mask 

172 

173 def _make_mask_from_tuple(self, args: tuple) -> bool | np.ndarray: 

174 mask: bool | np.ndarray = False 

175 

176 for arg in args: 

177 if is_integer(arg): 

178 mask |= self._make_mask_from_int(cast("int", arg)) 

179 elif isinstance(arg, slice): 

180 mask |= self._make_mask_from_slice(arg) 

181 else: 

182 raise ValueError( 

183 f"Invalid argument {type(arg)}. Should be int or slice." 

184 ) 

185 

186 return mask 

187 

188 def _make_mask_from_slice(self, arg: slice) -> bool | np.ndarray: 

189 start = arg.start 

190 stop = arg.stop 

191 step = arg.step 

192 

193 if step is not None and step < 0: 

194 raise ValueError(f"Invalid step {step}. Must be non-negative") 

195 

196 mask: bool | np.ndarray = True 

197 

198 if step is None: 

199 step = 1 

200 

201 if start is None: 

202 if step > 1: 

203 mask &= self._ascending_count % step == 0 

204 

205 elif start >= 0: 

206 mask &= self._ascending_count >= start 

207 

208 if step > 1: 

209 mask &= (self._ascending_count - start) % step == 0 

210 

211 else: 

212 mask &= self._descending_count < -start 

213 

214 offset_array = self._descending_count + start + 1 

215 limit_array = ( 

216 self._ascending_count + self._descending_count + (start + 1) 

217 ) < 0 

218 offset_array = np.where(limit_array, self._ascending_count, offset_array) 

219 

220 mask &= offset_array % step == 0 

221 

222 if stop is not None: 

223 if stop >= 0: 

224 mask &= self._ascending_count < stop 

225 else: 

226 mask &= self._descending_count >= -stop 

227 

228 return mask 

229 

230 @cache_readonly 

231 def _ascending_count(self) -> np.ndarray: 

232 if TYPE_CHECKING: 

233 groupby_self = cast("groupby.GroupBy", self) 

234 else: 

235 groupby_self = self 

236 

237 return groupby_self._cumcount_array() 

238 

239 @cache_readonly 

240 def _descending_count(self) -> np.ndarray: 

241 if TYPE_CHECKING: 

242 groupby_self = cast("groupby.GroupBy", self) 

243 else: 

244 groupby_self = self 

245 

246 return groupby_self._cumcount_array(ascending=False) 

247 

248 

249class GroupByPositionalSelector: 

250 """ 

251 Return positional selection for each group. 

252 

253 ``groupby._positional_selector[i:j]`` is similar to 

254 ``groupby.apply(lambda x: x.iloc[i:j])`` 

255 but much faster and preserves the original index and order. 

256 

257 ``_positional_selector[]`` is compatible with and extends :meth:`~GroupBy.head` 

258 and :meth:`~GroupBy.tail`. For example: 

259 

260 - ``head(5)`` 

261 - ``_positional_selector[5:-5]`` 

262 - ``tail(5)`` 

263 

264 together return all the rows. 

265 

266 Allowed inputs for the index are: 

267 

268 - An integer valued iterable, e.g. ``range(2, 4)``. 

269 - A comma separated list of integers and slices, e.g. ``5``, ``2, 4``, ``2:4``. 

270 

271 The output format is the same as :meth:`~GroupBy.head` and 

272 :meth:`~GroupBy.tail`, namely 

273 a subset of the ``DataFrame`` or ``Series`` with the index and order preserved. 

274 

275 Returns 

276 ------- 

277 Series 

278 The filtered subset of the original Series. 

279 DataFrame 

280 The filtered subset of the original DataFrame. 

281 

282 See Also 

283 -------- 

284 DataFrame.iloc : Purely integer-location based indexing for selection by 

285 position. 

286 GroupBy.head : Return first n rows of each group. 

287 GroupBy.tail : Return last n rows of each group. 

288 GroupBy.nth : Take the nth row from each group if n is an int, or a 

289 subset of rows, if n is a list of ints. 

290 

291 Notes 

292 ----- 

293 - The slice step cannot be negative. 

294 - If the index specification results in overlaps, the item is not duplicated. 

295 - If the index specification changes the order of items, then 

296 they are returned in their original order. 

297 By contrast, ``DataFrame.iloc`` can change the row order. 

298 - ``groupby()`` parameters such as as_index and dropna are ignored. 

299 

300 The differences between ``_positional_selector[]`` and :meth:`~GroupBy.nth` 

301 with ``as_index=False`` are: 

302 

303 - Input to ``_positional_selector`` can include 

304 one or more slices whereas ``nth`` 

305 just handles an integer or a list of integers. 

306 - ``_positional_selector`` can accept a slice relative to the 

307 last row of each group. 

308 - ``_positional_selector`` does not have an equivalent to the 

309 ``nth()`` ``dropna`` parameter. 

310 

311 Examples 

312 -------- 

313 >>> df = pd.DataFrame( 

314 ... [["a", 1], ["a", 2], ["a", 3], ["b", 4], ["b", 5]], columns=["A", "B"] 

315 ... ) 

316 >>> df.groupby("A")._positional_selector[1:2] 

317 A B 

318 1 a 2 

319 4 b 5 

320 

321 >>> df.groupby("A")._positional_selector[1, -1] 

322 A B 

323 1 a 2 

324 2 a 3 

325 4 b 5 

326 """ 

327 

328 def __init__(self, groupby_object: groupby.GroupBy) -> None: 

329 self.groupby_object = groupby_object 

330 

331 def __getitem__(self, arg: PositionalIndexer | tuple) -> DataFrame | Series: 

332 """ 

333 Select by positional index per group. 

334 

335 Implements GroupBy._positional_selector 

336 

337 Parameters 

338 ---------- 

339 arg : PositionalIndexer | tuple 

340 Allowed values are: 

341 - int 

342 - int valued iterable such as list or range 

343 - slice with step either None or positive 

344 - tuple of integers and slices 

345 

346 Returns 

347 ------- 

348 Series 

349 The filtered subset of the original groupby Series. 

350 DataFrame 

351 The filtered subset of the original groupby DataFrame. 

352 

353 See Also 

354 -------- 

355 DataFrame.iloc : Integer-location based indexing for selection by position. 

356 GroupBy.head : Return first n rows of each group. 

357 GroupBy.tail : Return last n rows of each group. 

358 GroupBy._positional_selector : Return positional selection for each group. 

359 GroupBy.nth : Take the nth row from each group if n is an int, or a 

360 subset of rows, if n is a list of ints. 

361 """ 

362 mask = self.groupby_object._make_mask_from_positional_indexer(arg) 

363 return self.groupby_object._mask_selected_obj(mask) 

364 

365 

366class GroupByNthSelector: 

367 """ 

368 Dynamically substituted for GroupBy.nth to enable both call and index 

369 """ 

370 

371 def __init__(self, groupby_object: groupby.GroupBy) -> None: 

372 self.groupby_object = groupby_object 

373 

374 def __call__( 

375 self, 

376 n: PositionalIndexer | tuple, 

377 dropna: Literal["any", "all"] | None = None, 

378 ) -> DataFrame | Series: 

379 return self.groupby_object._nth(n, dropna) 

380 

381 def __getitem__(self, n: PositionalIndexer | tuple) -> DataFrame | Series: 

382 warnings.warn( 

383 "GroupBy.nth[...] is deprecated and will be removed in a future " 

384 "version of pandas. Use GroupBy.nth(...) instead.", 

385 Pandas4Warning, 

386 stacklevel=find_stack_level(), 

387 ) 

388 return self.groupby_object._nth(n)