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

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

357 statements  

1""" 

2Provide user facing operators for doing the split part of the 

3split-apply-combine paradigm. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import ( 

9 TYPE_CHECKING, 

10 final, 

11) 

12 

13import numpy as np 

14 

15from pandas._libs import ( 

16 algos as libalgos, 

17) 

18from pandas._libs.tslibs import OutOfBoundsDatetime 

19from pandas.errors import InvalidIndexError 

20from pandas.util._decorators import ( 

21 cache_readonly, 

22 set_module, 

23) 

24 

25from pandas.core.dtypes.common import ( 

26 ensure_int64, 

27 ensure_platform_int, 

28 is_list_like, 

29 is_scalar, 

30) 

31from pandas.core.dtypes.dtypes import CategoricalDtype 

32 

33from pandas.core import algorithms 

34from pandas.core.arrays import ( 

35 Categorical, 

36 ExtensionArray, 

37) 

38import pandas.core.common as com 

39from pandas.core.frame import DataFrame 

40from pandas.core.groupby import ops 

41from pandas.core.groupby.categorical import recode_for_groupby 

42from pandas.core.indexes.api import ( 

43 Index, 

44 MultiIndex, 

45 default_index, 

46) 

47from pandas.core.series import Series 

48 

49from pandas.io.formats.printing import ( 

50 PrettyDict, 

51 pprint_thing, 

52) 

53 

54if TYPE_CHECKING: 

55 from collections.abc import ( 

56 Hashable, 

57 Iterator, 

58 ) 

59 

60 from pandas._typing import ( 

61 ArrayLike, 

62 NDFrameT, 

63 npt, 

64 ) 

65 

66 from pandas.core.generic import NDFrame 

67 

68 

69@set_module("pandas") 

70class Grouper: 

71 """ 

72 A Grouper allows the user to specify a groupby instruction for an object. 

73 

74 This specification will select a column via the key parameter, or if the 

75 level parameter is given, a level of the index of the target 

76 object. 

77 

78 If ``level`` is passed as a keyword to both `Grouper` and 

79 `groupby`, the values passed to `Grouper` take precedence. 

80 

81 Parameters 

82 ---------- 

83 *args 

84 Currently unused, reserved for future use. 

85 **kwargs 

86 Dictionary of the keyword arguments to pass to Grouper. 

87 

88 Attributes 

89 ---------- 

90 key : str, defaults to None 

91 Groupby key, which selects the grouping column of the target. 

92 level : name/number, defaults to None 

93 The level for the target index. 

94 freq : str / frequency object, defaults to None 

95 This will groupby the specified frequency if the target selection 

96 (via key or level) is a datetime-like object. For full specification 

97 of available frequencies, please see :ref:`here<timeseries.offset_aliases>`. 

98 sort : bool, default to False 

99 Whether to sort the resulting labels. 

100 closed : {'left' or 'right'} 

101 Closed end of interval. Only when `freq` parameter is passed. 

102 label : {'left' or 'right'} 

103 Interval boundary to use for labeling. 

104 Only when `freq` parameter is passed. 

105 convention : {'start', 'end', 'e', 's'} 

106 If grouper is PeriodIndex and `freq` parameter is passed. 

107 

108 origin : Timestamp or str, default 'start_day' 

109 The timestamp on which to adjust the grouping. The timezone of origin must 

110 match the timezone of the index. 

111 If string, must be one of the following: 

112 

113 - 'epoch': `origin` is 1970-01-01 

114 - 'start': `origin` is the first value of the timeseries 

115 - 'start_day': `origin` is the first day at midnight of the timeseries 

116 

117 - 'end': `origin` is the last value of the timeseries 

118 - 'end_day': `origin` is the ceiling midnight of the last day 

119 

120 offset : Timedelta or str, default is None 

121 An offset timedelta added to the origin. 

122 

123 dropna : bool, default True 

124 If True, and if group keys contain NA values, NA values together with 

125 row/column will be dropped. If False, NA values will also be treated as 

126 the key in groups. 

127 

128 Returns 

129 ------- 

130 Grouper or pandas.api.typing.TimeGrouper 

131 A TimeGrouper is returned if ``freq`` is not ``None``. Otherwise, a Grouper 

132 is returned. 

133 

134 See Also 

135 -------- 

136 Series.groupby : Apply a function groupby to a Series. 

137 DataFrame.groupby : Apply a function groupby. 

138 

139 Examples 

140 -------- 

141 ``df.groupby(pd.Grouper(key="Animal"))`` is equivalent to ``df.groupby('Animal')`` 

142 

143 >>> df = pd.DataFrame( 

144 ... { 

145 ... "Animal": ["Falcon", "Parrot", "Falcon", "Falcon", "Parrot"], 

146 ... "Speed": [100, 5, 200, 300, 15], 

147 ... } 

148 ... ) 

149 >>> df 

150 Animal Speed 

151 0 Falcon 100 

152 1 Parrot 5 

153 2 Falcon 200 

154 3 Falcon 300 

155 4 Parrot 15 

156 >>> df.groupby(pd.Grouper(key="Animal")).mean() 

157 Speed 

158 Animal 

159 Falcon 200.0 

160 Parrot 10.0 

161 

162 Specify a resample operation on the column 'Publish date' 

163 

164 >>> df = pd.DataFrame( 

165 ... { 

166 ... "Publish date": [ 

167 ... pd.Timestamp("2000-01-02"), 

168 ... pd.Timestamp("2000-01-02"), 

169 ... pd.Timestamp("2000-01-09"), 

170 ... pd.Timestamp("2000-01-16"), 

171 ... ], 

172 ... "ID": [0, 1, 2, 3], 

173 ... "Price": [10, 20, 30, 40], 

174 ... } 

175 ... ) 

176 >>> df 

177 Publish date ID Price 

178 0 2000-01-02 0 10 

179 1 2000-01-02 1 20 

180 2 2000-01-09 2 30 

181 3 2000-01-16 3 40 

182 >>> df.groupby(pd.Grouper(key="Publish date", freq="1W")).mean() 

183 ID Price 

184 Publish date 

185 2000-01-02 0.5 15.0 

186 2000-01-09 2.0 30.0 

187 2000-01-16 3.0 40.0 

188 

189 If you want to adjust the start of the bins based on a fixed timestamp: 

190 

191 >>> start, end = "2000-10-01 23:30:00", "2000-10-02 00:30:00" 

192 >>> rng = pd.date_range(start, end, freq="7min") 

193 >>> ts = pd.Series(np.arange(len(rng)) * 3, index=rng) 

194 >>> ts 

195 2000-10-01 23:30:00 0 

196 2000-10-01 23:37:00 3 

197 2000-10-01 23:44:00 6 

198 2000-10-01 23:51:00 9 

199 2000-10-01 23:58:00 12 

200 2000-10-02 00:05:00 15 

201 2000-10-02 00:12:00 18 

202 2000-10-02 00:19:00 21 

203 2000-10-02 00:26:00 24 

204 Freq: 7min, dtype: int64 

205 

206 >>> ts.groupby(pd.Grouper(freq="17min")).sum() 

207 2000-10-01 23:14:00 0 

208 2000-10-01 23:31:00 9 

209 2000-10-01 23:48:00 21 

210 2000-10-02 00:05:00 54 

211 2000-10-02 00:22:00 24 

212 Freq: 17min, dtype: int64 

213 

214 >>> ts.groupby(pd.Grouper(freq="17min", origin="epoch")).sum() 

215 2000-10-01 23:18:00 0 

216 2000-10-01 23:35:00 18 

217 2000-10-01 23:52:00 27 

218 2000-10-02 00:09:00 39 

219 2000-10-02 00:26:00 24 

220 Freq: 17min, dtype: int64 

221 

222 >>> ts.groupby(pd.Grouper(freq="17min", origin="2000-01-01")).sum() 

223 2000-10-01 23:24:00 3 

224 2000-10-01 23:41:00 15 

225 2000-10-01 23:58:00 45 

226 2000-10-02 00:15:00 45 

227 Freq: 17min, dtype: int64 

228 

229 If you want to adjust the start of the bins with an `offset` Timedelta, the two 

230 following lines are equivalent: 

231 

232 >>> ts.groupby(pd.Grouper(freq="17min", origin="start")).sum() 

233 2000-10-01 23:30:00 9 

234 2000-10-01 23:47:00 21 

235 2000-10-02 00:04:00 54 

236 2000-10-02 00:21:00 24 

237 Freq: 17min, dtype: int64 

238 

239 >>> ts.groupby(pd.Grouper(freq="17min", offset="23h30min")).sum() 

240 2000-10-01 23:30:00 9 

241 2000-10-01 23:47:00 21 

242 2000-10-02 00:04:00 54 

243 2000-10-02 00:21:00 24 

244 Freq: 17min, dtype: int64 

245 

246 To replace the use of the deprecated `base` argument, you can now use `offset`, 

247 in this example it is equivalent to have `base=2`: 

248 

249 >>> ts.groupby(pd.Grouper(freq="17min", offset="2min")).sum() 

250 2000-10-01 23:16:00 0 

251 2000-10-01 23:33:00 9 

252 2000-10-01 23:50:00 36 

253 2000-10-02 00:07:00 39 

254 2000-10-02 00:24:00 24 

255 Freq: 17min, dtype: int64 

256 """ 

257 

258 sort: bool 

259 dropna: bool 

260 _grouper: Index | None 

261 

262 _attributes: tuple[str, ...] = ("key", "level", "freq", "sort", "dropna") 

263 

264 def __new__(cls, *args, **kwargs): 

265 if kwargs.get("freq") is not None: 

266 from pandas.core.resample import TimeGrouper 

267 

268 cls = TimeGrouper 

269 return super().__new__(cls) 

270 

271 def __init__( 

272 self, 

273 key=None, 

274 level=None, 

275 freq=None, 

276 sort: bool = False, 

277 dropna: bool = True, 

278 ) -> None: 

279 self.key = key 

280 self.level = level 

281 self.freq = freq 

282 self.sort = sort 

283 self.dropna = dropna 

284 

285 self._indexer_deprecated: npt.NDArray[np.intp] | None = None 

286 self.binner = None 

287 self._grouper = None 

288 self._indexer: npt.NDArray[np.intp] | None = None 

289 

290 def _get_grouper( 

291 self, obj: NDFrameT, validate: bool = True, observed: bool = True 

292 ) -> tuple[ops.BaseGrouper, NDFrameT]: 

293 """ 

294 Parameters 

295 ---------- 

296 obj : Series or DataFrame 

297 Object being grouped. 

298 validate : bool, default True 

299 If True, validate the grouper. 

300 observed : bool, default True 

301 Whether only observed groups should be in the result. Only 

302 has an impact when grouping on categorical data. 

303 

304 Returns 

305 ------- 

306 A tuple of grouper, obj (possibly sorted) 

307 """ 

308 obj, _, _ = self._set_grouper(obj) 

309 grouper, _, obj = get_grouper( 

310 obj, 

311 [self.key], 

312 level=self.level, 

313 sort=self.sort, 

314 validate=validate, 

315 dropna=self.dropna, 

316 observed=observed, 

317 ) 

318 

319 return grouper, obj 

320 

321 def _set_grouper( 

322 self, obj: NDFrameT, sort: bool = False, *, gpr_index: Index | None = None 

323 ) -> tuple[NDFrameT, Index, npt.NDArray[np.intp] | None]: 

324 """ 

325 given an object and the specifications, setup the internal grouper 

326 for this particular specification 

327 

328 Parameters 

329 ---------- 

330 obj : Series or DataFrame 

331 sort : bool, default False 

332 whether the resulting grouper should be sorted 

333 gpr_index : Index or None, default None 

334 

335 Returns 

336 ------- 

337 NDFrame 

338 Index 

339 np.ndarray[np.intp] | None 

340 """ 

341 assert obj is not None 

342 

343 if self.key is not None and self.level is not None: 

344 raise ValueError("The Grouper cannot specify both a key and a level!") 

345 

346 # Keep self._grouper value before overriding 

347 if self._grouper is None: 

348 # TODO: What are we assuming about subsequent calls? 

349 self._grouper = gpr_index 

350 self._indexer = self._indexer_deprecated 

351 

352 # the key must be a valid info item 

353 if self.key is not None: 

354 key = self.key 

355 # The 'on' is already defined 

356 if getattr(gpr_index, "name", None) == key and isinstance(obj, Series): 

357 # Sometimes self._grouper will have been resorted while 

358 # obj has not. In this case there is a mismatch when we 

359 # call self._grouper.take(obj.index) so we need to undo the sorting 

360 # before we call _grouper.take. 

361 assert self._grouper is not None 

362 if self._indexer is not None: 

363 reverse_indexer = self._indexer.argsort() 

364 unsorted_ax = self._grouper.take(reverse_indexer) 

365 ax = unsorted_ax.take(obj.index) 

366 else: 

367 ax = self._grouper.take(obj.index) 

368 else: 

369 if key not in obj._info_axis: 

370 raise KeyError(f"The grouper name {key} is not found") 

371 ax = Index(obj[key], name=key) 

372 

373 else: 

374 ax = obj.index 

375 if self.level is not None: 

376 level = self.level 

377 

378 # if a level is given it must be a mi level or 

379 # equivalent to the axis name 

380 if isinstance(ax, MultiIndex): 

381 level = ax._get_level_number(level) 

382 ax = Index(ax._get_level_values(level), name=ax.names[level]) 

383 

384 elif level not in (0, ax.name): 

385 raise ValueError(f"The level {level} is not valid") 

386 

387 # possibly sort 

388 indexer: npt.NDArray[np.intp] | None = None 

389 if (self.sort or sort) and not ax.is_monotonic_increasing: 

390 # use stable sort to support first, last, nth 

391 # TODO: why does putting na_position="first" fix datetimelike cases? 

392 indexer = self._indexer_deprecated = ax.array.argsort( 

393 kind="mergesort", na_position="first" 

394 ) 

395 ax = ax.take(indexer) 

396 obj = obj.take(indexer, axis=0) 

397 

398 return obj, ax, indexer 

399 

400 @final 

401 def __repr__(self) -> str: 

402 attrs_list = ( 

403 f"{attr_name}={getattr(self, attr_name)!r}" 

404 for attr_name in self._attributes 

405 if getattr(self, attr_name) is not None 

406 ) 

407 attrs = ", ".join(attrs_list) 

408 cls_name = type(self).__name__ 

409 return f"{cls_name}({attrs})" 

410 

411 

412@final 

413class Grouping: 

414 """ 

415 Holds the grouping information for a single key 

416 

417 Parameters 

418 ---------- 

419 index : Index 

420 grouper : 

421 obj : DataFrame or Series 

422 name : Label 

423 level : 

424 observed : bool, default False 

425 If we are a Categorical, use the observed values 

426 in_axis : if the Grouping is a column in self.obj and hence among 

427 Groupby.exclusions list 

428 dropna : bool, default True 

429 Whether to drop NA groups. 

430 uniques : Array-like, optional 

431 When specified, will be used for unique values. Enables including empty groups 

432 in the result for a BinGrouper. Must not contain duplicates. 

433 

434 Attributes 

435 ------- 

436 indices : dict 

437 Mapping of {group -> index_list} 

438 codes : ndarray 

439 Group codes 

440 group_index : Index or None 

441 unique groups 

442 groups : dict 

443 Mapping of {group -> label_list} 

444 """ 

445 

446 _codes: npt.NDArray[np.signedinteger] | None = None 

447 _orig_cats: Index | None 

448 _index: Index 

449 

450 def __init__( 

451 self, 

452 index: Index, 

453 grouper=None, 

454 obj: NDFrame | None = None, 

455 level=None, 

456 sort: bool = True, 

457 observed: bool = False, 

458 in_axis: bool = False, 

459 dropna: bool = True, 

460 uniques: ArrayLike | None = None, 

461 ) -> None: 

462 if isinstance(grouper, Series): 

463 grouper = grouper.copy(deep=False) 

464 self.level = level 

465 self._orig_grouper = grouper 

466 grouping_vector = _convert_grouper(index, grouper) 

467 self._orig_cats = None 

468 self._index = index 

469 self._sort = sort 

470 self.obj = obj 

471 self._observed = observed 

472 self.in_axis = in_axis 

473 self._dropna = dropna 

474 self._uniques = uniques 

475 

476 # we have a single grouper which may be a myriad of things, 

477 # some of which are dependent on the passing in level 

478 

479 ilevel = self._ilevel 

480 if ilevel is not None: 

481 # In extant tests, the new self.grouping_vector matches 

482 # `index.get_level_values(ilevel)` whenever 

483 # mapper is None and isinstance(index, MultiIndex) 

484 if isinstance(index, MultiIndex): 

485 index_level = index.get_level_values(ilevel) 

486 else: 

487 index_level = index 

488 

489 if grouping_vector is None: 

490 grouping_vector = index_level 

491 else: 

492 mapper = grouping_vector 

493 grouping_vector = index_level.map(mapper) 

494 

495 # a passed Grouper like, directly get the grouper in the same way 

496 # as single grouper groupby, use the group_info to get codes 

497 elif isinstance(grouping_vector, Grouper): 

498 # get the new grouper; we already have disambiguated 

499 # what key/level refer to exactly, don't need to 

500 # check again as we have by this point converted these 

501 # to an actual value (rather than a pd.Grouper) 

502 assert self.obj is not None # for mypy 

503 newgrouper, newobj = grouping_vector._get_grouper(self.obj, validate=False) 

504 self.obj = newobj 

505 

506 if isinstance(newgrouper, ops.BinGrouper): 

507 # TODO: can we unwrap this and get a tighter typing 

508 # for self.grouping_vector? 

509 grouping_vector = newgrouper 

510 else: 

511 # ops.BaseGrouper 

512 # TODO: 2023-02-03 no test cases with len(newgrouper.groupings) > 1. 

513 # If that were to occur, would we be throwing out information? 

514 # error: Cannot determine type of "grouping_vector" [has-type] 

515 ng = newgrouper.groupings[0].grouping_vector # type: ignore[has-type] 

516 # use Index instead of ndarray so we can recover the name 

517 grouping_vector = Index( 

518 ng, name=newgrouper.result_index.name, copy=False 

519 ) 

520 

521 elif not isinstance( 

522 grouping_vector, (Series, Index, ExtensionArray, np.ndarray) 

523 ): 

524 # no level passed 

525 if getattr(grouping_vector, "ndim", 1) != 1: 

526 t = str(type(grouping_vector)) 

527 raise ValueError(f"Grouper for '{t}' not 1-dimensional") 

528 

529 grouping_vector = index.map(grouping_vector) 

530 

531 if not ( 

532 hasattr(grouping_vector, "__len__") 

533 and len(grouping_vector) == len(index) 

534 ): 

535 grper = pprint_thing(grouping_vector) 

536 errmsg = ( 

537 f"Grouper result violates len(labels) == len(data)\nresult: {grper}" 

538 ) 

539 raise AssertionError(errmsg) 

540 

541 if isinstance(grouping_vector, np.ndarray): 

542 if grouping_vector.dtype.kind in "mM": 

543 # if we have a date/time-like grouper, make sure that we have 

544 # Timestamps like 

545 # TODO 2022-10-08 we only have one test that gets here and 

546 # values are already in nanoseconds in that case. 

547 grouping_vector = Series(grouping_vector).to_numpy() 

548 elif isinstance(getattr(grouping_vector, "dtype", None), CategoricalDtype): 

549 # a passed Categorical 

550 self._orig_cats = grouping_vector.categories 

551 grouping_vector = recode_for_groupby(grouping_vector, sort, observed) 

552 

553 self.grouping_vector = grouping_vector 

554 

555 def __repr__(self) -> str: 

556 return f"Grouping({self.name})" 

557 

558 def __iter__(self) -> Iterator: 

559 return iter(self.indices) 

560 

561 @cache_readonly 

562 def _passed_categorical(self) -> bool: 

563 dtype = getattr(self.grouping_vector, "dtype", None) 

564 return isinstance(dtype, CategoricalDtype) 

565 

566 @cache_readonly 

567 def name(self) -> Hashable: 

568 ilevel = self._ilevel 

569 if ilevel is not None: 

570 return self._index.names[ilevel] 

571 

572 if isinstance(self._orig_grouper, (Index, Series)): 

573 return self._orig_grouper.name 

574 

575 elif isinstance(self.grouping_vector, ops.BaseGrouper): 

576 return self.grouping_vector.result_index.name 

577 

578 elif isinstance(self.grouping_vector, Index): 

579 return self.grouping_vector.name 

580 

581 # otherwise we have ndarray or ExtensionArray -> no name 

582 return None 

583 

584 @cache_readonly 

585 def _ilevel(self) -> int | None: 

586 """ 

587 If necessary, converted index level name to index level position. 

588 """ 

589 level = self.level 

590 if level is None: 

591 return None 

592 if not isinstance(level, int): 

593 index = self._index 

594 if level not in index.names: 

595 raise AssertionError(f"Level {level} not in index") 

596 return index.names.index(level) 

597 return level 

598 

599 @property 

600 def ngroups(self) -> int: 

601 return len(self.uniques) 

602 

603 @cache_readonly 

604 def indices(self) -> dict[Hashable, npt.NDArray[np.intp]]: 

605 # we have a list of groupers 

606 if isinstance(self.grouping_vector, ops.BaseGrouper): 

607 return self.grouping_vector.indices 

608 

609 values = Categorical(self.grouping_vector) 

610 return values._reverse_indexer() 

611 

612 @property 

613 def codes(self) -> npt.NDArray[np.signedinteger]: 

614 return self._codes_and_uniques[0] 

615 

616 @property 

617 def uniques(self) -> ArrayLike: 

618 return self._codes_and_uniques[1] 

619 

620 @cache_readonly 

621 def _codes_and_uniques(self) -> tuple[npt.NDArray[np.signedinteger], ArrayLike]: 

622 uniques: ArrayLike 

623 if self._passed_categorical: 

624 # we make a CategoricalIndex out of the cat grouper 

625 # preserving the categories / ordered attributes; 

626 # doesn't (yet - GH#46909) handle dropna=False 

627 cat = self.grouping_vector 

628 categories = cat.categories 

629 

630 if self._observed: 

631 ucodes = algorithms.unique1d(cat.codes) 

632 ucodes = ucodes[ucodes != -1] 

633 if self._sort: 

634 ucodes = np.sort(ucodes) 

635 else: 

636 ucodes = np.arange(len(categories)) 

637 

638 has_dropped_na = False 

639 if not self._dropna: 

640 na_mask = cat.isna() 

641 if np.any(na_mask): 

642 has_dropped_na = True 

643 if self._sort: 

644 # NA goes at the end, gets `largest non-NA code + 1` 

645 na_code = len(categories) 

646 else: 

647 # Insert NA in result based on first appearance, need 

648 # the number of unique codes prior 

649 na_idx = na_mask.argmax() 

650 na_code = algorithms.nunique_ints(cat.codes[:na_idx]) 

651 ucodes = np.insert(ucodes, na_code, -1) 

652 

653 uniques = Categorical.from_codes( 

654 codes=ucodes, categories=categories, ordered=cat.ordered, validate=False 

655 ) 

656 codes = cat.codes 

657 

658 if has_dropped_na: 

659 if not self._sort: 

660 # NA code is based on first appearance, increment higher codes 

661 codes = np.where(codes >= na_code, codes + 1, codes) 

662 codes = np.where(na_mask, na_code, codes) 

663 

664 return codes, uniques 

665 

666 elif isinstance(self.grouping_vector, ops.BaseGrouper): 

667 # we have a list of groupers 

668 codes = self.grouping_vector.codes_info 

669 uniques = self.grouping_vector.result_index._values 

670 elif self._uniques is not None: 

671 # GH#50486 Code grouping_vector using _uniques; allows 

672 # including uniques that are not present in grouping_vector. 

673 cat = Categorical(self.grouping_vector, categories=self._uniques) 

674 codes = cat.codes 

675 uniques = self._uniques 

676 else: 

677 # GH35667, replace dropna=False with use_na_sentinel=False 

678 # error: Incompatible types in assignment (expression has type "Union[ 

679 # ndarray[Any, Any], Index]", variable has type "Categorical") 

680 codes, uniques = algorithms.factorize( # type: ignore[assignment] 

681 self.grouping_vector, sort=self._sort, use_na_sentinel=self._dropna 

682 ) 

683 return codes, uniques 

684 

685 @cache_readonly 

686 def groups(self) -> dict[Hashable, Index]: 

687 codes, uniques = self._codes_and_uniques 

688 uniques = Index._with_infer(uniques, name=self.name, copy=False) 

689 

690 r, counts = libalgos.groupsort_indexer(ensure_platform_int(codes), len(uniques)) 

691 counts = ensure_int64(counts).cumsum() 

692 _result = (r[start:end] for start, end in zip(counts, counts[1:], strict=False)) 

693 # map to the label 

694 result = {k: self._index.take(v) for k, v in zip(uniques, _result, strict=True)} 

695 

696 return PrettyDict(result) 

697 

698 @property 

699 def observed_grouping(self) -> Grouping: 

700 if self._observed: 

701 return self 

702 

703 return self._observed_grouping 

704 

705 @cache_readonly 

706 def _observed_grouping(self) -> Grouping: 

707 grouping = Grouping( 

708 self._index, 

709 self._orig_grouper, 

710 obj=self.obj, 

711 level=self.level, 

712 sort=self._sort, 

713 observed=True, 

714 in_axis=self.in_axis, 

715 dropna=self._dropna, 

716 uniques=self._uniques, 

717 ) 

718 return grouping 

719 

720 

721def get_grouper( 

722 obj: NDFrameT, 

723 key=None, 

724 level=None, 

725 sort: bool = True, 

726 observed: bool = False, 

727 validate: bool = True, 

728 dropna: bool = True, 

729) -> tuple[ops.BaseGrouper, frozenset[Hashable], NDFrameT]: 

730 """ 

731 Create and return a BaseGrouper, which is an internal 

732 mapping of how to create the grouper indexers. 

733 This may be composed of multiple Grouping objects, indicating 

734 multiple groupers 

735 

736 Groupers are ultimately index mappings. They can originate as: 

737 index mappings, keys to columns, functions, or Groupers 

738 

739 Groupers enable local references to level,sort, while 

740 the passed in level, and sort are 'global'. 

741 

742 This routine tries to figure out what the passing in references 

743 are and then creates a Grouping for each one, combined into 

744 a BaseGrouper. 

745 

746 If observed & we have a categorical grouper, only show the observed 

747 values. 

748 

749 If validate, then check for key/level overlaps. 

750 

751 """ 

752 group_axis = obj.index 

753 

754 # validate that the passed single level is compatible with the passed 

755 # index of the object 

756 if level is not None: 

757 # TODO: These if-block and else-block are almost same. 

758 # MultiIndex instance check is removable, but it seems that there are 

759 # some processes only for non-MultiIndex in else-block, 

760 # eg. `obj.index.name != level`. We have to consider carefully whether 

761 # these are applicable for MultiIndex. Even if these are applicable, 

762 # we need to check if it makes no side effect to subsequent processes 

763 # on the outside of this condition. 

764 # (GH 17621) 

765 if isinstance(group_axis, MultiIndex): 

766 if is_list_like(level) and len(level) == 1: 

767 level = level[0] 

768 

769 if key is None and is_scalar(level): 

770 # Get the level values from group_axis 

771 key = group_axis.get_level_values(level) 

772 level = None 

773 

774 else: 

775 # allow level to be a length-one list-like object 

776 # (e.g., level=[0]) 

777 # GH 13901 

778 if is_list_like(level): 

779 nlevels = len(level) 

780 if nlevels == 1: 

781 level = level[0] 

782 elif nlevels == 0: 

783 raise ValueError("No group keys passed!") 

784 else: 

785 raise ValueError("multiple levels only valid with MultiIndex") 

786 

787 if isinstance(level, str): 

788 if obj.index.name != level: 

789 raise ValueError(f"level name {level} is not the name of the index") 

790 elif level > 0 or level < -1: 

791 raise ValueError("level > 0 or level < -1 only valid with MultiIndex") 

792 

793 # NOTE: `group_axis` and `group_axis.get_level_values(level)` 

794 # are same in this section. 

795 level = None 

796 key = group_axis 

797 

798 # a passed-in Grouper, directly convert 

799 if isinstance(key, Grouper): 

800 grouper, obj = key._get_grouper(obj, validate=False, observed=observed) 

801 if key.key is None: 

802 return grouper, frozenset(), obj 

803 else: 

804 return grouper, frozenset({key.key}), obj 

805 

806 # already have a BaseGrouper, just return it 

807 elif isinstance(key, ops.BaseGrouper): 

808 return key, frozenset(), obj 

809 

810 if not isinstance(key, list): 

811 keys = [key] 

812 match_axis_length = False 

813 else: 

814 keys = key 

815 match_axis_length = len(keys) == len(group_axis) 

816 

817 # what are we after, exactly? 

818 any_callable = any(callable(g) or isinstance(g, dict) for g in keys) 

819 any_groupers = any(isinstance(g, (Grouper, Grouping)) for g in keys) 

820 any_arraylike = any( 

821 isinstance(g, (list, tuple, Series, Index, np.ndarray)) for g in keys 

822 ) 

823 

824 # is this an index replacement? 

825 if ( 

826 not any_callable 

827 and not any_arraylike 

828 and not any_groupers 

829 and match_axis_length 

830 and level is None 

831 ): 

832 if isinstance(obj, DataFrame): 

833 all_in_columns_index = all( 

834 g in obj.columns or g in obj.index.names for g in keys 

835 ) 

836 else: 

837 assert isinstance(obj, Series) 

838 all_in_columns_index = all(g in obj.index.names for g in keys) 

839 

840 if not all_in_columns_index: 

841 keys = [com.asarray_tuplesafe(keys)] 

842 

843 if isinstance(level, (tuple, list)): 

844 if key is None: 

845 keys = [None] * len(level) 

846 levels = level 

847 else: 

848 levels = [level] * len(keys) 

849 

850 groupings: list[Grouping] = [] 

851 exclusions: set[Hashable] = set() 

852 

853 # if the actual grouper should be obj[key] 

854 def is_in_axis(key) -> bool: 

855 if not _is_label_like(key): 

856 if obj.ndim == 1: 

857 return False 

858 

859 # items -> .columns for DataFrame, .index for Series 

860 items = obj.axes[-1] 

861 try: 

862 items.get_loc(key) 

863 except (KeyError, TypeError, InvalidIndexError): 

864 # TypeError shows up here if we pass e.g. an Index 

865 return False 

866 

867 return True 

868 

869 # if the grouper is obj[name] 

870 def is_in_obj(gpr) -> bool: 

871 if not hasattr(gpr, "name"): 

872 return False 

873 # We check the references to determine if the 

874 # series is part of the object 

875 try: 

876 obj_gpr_column = obj[gpr.name] 

877 except (KeyError, IndexError, InvalidIndexError, OutOfBoundsDatetime): 

878 return False 

879 if isinstance(gpr, Series) and isinstance(obj_gpr_column, Series): 

880 return gpr._mgr.references_same_values(obj_gpr_column._mgr, 0) 

881 return False 

882 

883 for gpr, level in zip(keys, levels, strict=True): 

884 if is_in_obj(gpr): # df.groupby(df['name']) 

885 in_axis = True 

886 exclusions.add(gpr.name) 

887 

888 elif is_in_axis(gpr): # df.groupby('name') 

889 if obj.ndim != 1 and gpr in obj: 

890 if validate: 

891 obj._check_label_or_level_ambiguity(gpr, axis=0) 

892 in_axis, name, gpr = True, gpr, obj[gpr] 

893 if gpr.ndim != 1: 

894 # non-unique columns; raise here to get the name in the 

895 # exception message 

896 raise ValueError(f"Grouper for '{name}' not 1-dimensional") 

897 exclusions.add(name) 

898 elif obj._is_level_reference(gpr, axis=0): 

899 in_axis, level, gpr = False, gpr, None 

900 else: 

901 raise KeyError(gpr) 

902 elif isinstance(gpr, Grouper) and gpr.key is not None: 

903 # Add key to exclusions 

904 exclusions.add(gpr.key) 

905 in_axis = True 

906 else: 

907 in_axis = False 

908 

909 # create the Grouping 

910 # allow us to passing the actual Grouping as the gpr 

911 ping = ( 

912 Grouping( 

913 group_axis, 

914 gpr, 

915 obj=obj, 

916 level=level, 

917 sort=sort, 

918 observed=observed, 

919 in_axis=in_axis, 

920 dropna=dropna, 

921 ) 

922 if not isinstance(gpr, Grouping) 

923 else gpr 

924 ) 

925 

926 groupings.append(ping) 

927 

928 if len(groupings) == 0 and len(obj): 

929 raise ValueError("No group keys passed!") 

930 if len(groupings) == 0: 

931 groupings.append(Grouping(default_index(0), np.array([], dtype=np.intp))) 

932 

933 # create the internals grouper 

934 grouper = ops.BaseGrouper(group_axis, groupings, sort=sort, dropna=dropna) 

935 return grouper, frozenset(exclusions), obj 

936 

937 

938def _is_label_like(val) -> bool: 

939 return isinstance(val, (str, tuple)) or (val is not None and is_scalar(val)) 

940 

941 

942def _convert_grouper(axis: Index, grouper): 

943 if isinstance(grouper, dict): 

944 return grouper.get 

945 elif isinstance(grouper, Series): 

946 if grouper.index.equals(axis): 

947 return grouper._values 

948 else: 

949 return grouper.reindex(axis)._values 

950 elif isinstance(grouper, MultiIndex): 

951 return grouper._values 

952 elif isinstance(grouper, (list, tuple, Index, Categorical, np.ndarray)): 

953 if len(grouper) != len(axis): 

954 raise ValueError("Grouper and axis must be same length") 

955 

956 if isinstance(grouper, (list, tuple)): 

957 grouper = com.asarray_tuplesafe(grouper) 

958 return grouper 

959 else: 

960 return grouper