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

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

1049 statements  

1""" 

2Provide the groupby split-apply-combine paradigm. Define the GroupBy 

3class providing the base-class of operations. 

4 

5The SeriesGroupBy and DataFrameGroupBy sub-class 

6(defined in pandas.core.groupby.generic) 

7expose these user-facing objects to provide specific functionality. 

8""" 

9 

10from __future__ import annotations 

11 

12from collections.abc import ( 

13 Callable, 

14 Hashable, 

15 Iterable, 

16 Iterator, 

17 Mapping, 

18 Sequence, 

19) 

20import datetime 

21from functools import ( 

22 partial, 

23 wraps, 

24) 

25from typing import ( 

26 TYPE_CHECKING, 

27 Concatenate, 

28 Literal, 

29 Self, 

30 TypeAlias, 

31 TypeVar, 

32 cast, 

33 final, 

34 overload, 

35) 

36import warnings 

37 

38import numpy as np 

39 

40from pandas._libs import ( 

41 Timestamp, 

42 lib, 

43) 

44from pandas._libs.algos import rank_1d 

45import pandas._libs.groupby as libgroupby 

46from pandas._libs.missing import NA 

47from pandas._typing import ( 

48 AnyArrayLike, 

49 ArrayLike, 

50 IndexLabel, 

51 IntervalClosedType, 

52 ListLike, 

53 NDFrameT, 

54 PositionalIndexer, 

55 RandomState, 

56 RankMethod, 

57 RankNaOption, 

58 npt, 

59) 

60from pandas.compat.numpy import function as nv 

61from pandas.errors import ( 

62 AbstractMethodError, 

63 DataError, 

64 Pandas4Warning, 

65) 

66from pandas.util._decorators import cache_readonly 

67from pandas.util._exceptions import find_stack_level 

68 

69from pandas.core.dtypes.cast import ( 

70 coerce_indexer_dtype, 

71 ensure_dtype_can_hold_na, 

72) 

73from pandas.core.dtypes.common import ( 

74 is_bool, 

75 is_bool_dtype, 

76 is_hashable, 

77 is_integer, 

78 is_integer_dtype, 

79 is_list_like, 

80 is_numeric_dtype, 

81 is_object_dtype, 

82 is_scalar, 

83 is_string_dtype, 

84 pandas_dtype, 

85) 

86from pandas.core.dtypes.missing import ( 

87 isna, 

88 na_value_for_dtype, 

89 notna, 

90) 

91 

92from pandas.core import ( 

93 algorithms, 

94 sample, 

95) 

96from pandas.core._numba import executor 

97from pandas.core.arrays import ( 

98 ArrowExtensionArray, 

99 BaseMaskedArray, 

100 ExtensionArray, 

101 IntegerArray, 

102) 

103from pandas.core.arrays.string_ import StringDtype 

104from pandas.core.arrays.string_arrow import ArrowStringArray 

105from pandas.core.base import ( 

106 PandasObject, 

107 SelectionMixin, 

108) 

109import pandas.core.common as com 

110from pandas.core.frame import DataFrame 

111from pandas.core.generic import NDFrame 

112from pandas.core.groupby import ( 

113 base, 

114 numba_, 

115 ops, 

116) 

117from pandas.core.groupby.grouper import get_grouper 

118from pandas.core.groupby.indexing import ( 

119 GroupByIndexingMixin, 

120 GroupByNthSelector, 

121) 

122from pandas.core.indexes.api import ( 

123 Index, 

124 MultiIndex, 

125 default_index, 

126) 

127from pandas.core.internals.blocks import ensure_block_shape 

128from pandas.core.series import Series 

129from pandas.core.sorting import get_group_index_sorter 

130from pandas.core.util.numba_ import ( 

131 get_jit_arguments, 

132 maybe_use_numba, 

133 prepare_function_arguments, 

134) 

135 

136if TYPE_CHECKING: 

137 from pandas._libs.tslibs import BaseOffset 

138 from pandas._libs.tslibs.timedeltas import Timedelta 

139 from pandas._typing import ( 

140 Any, 

141 Level, 

142 P, 

143 T, 

144 TimedeltaConvertibleTypes, 

145 TimestampConvertibleTypes, 

146 ) 

147 

148 from pandas.core.indexers.objects import BaseIndexer 

149 from pandas.core.resample import Resampler 

150 from pandas.core.window import ( 

151 ExpandingGroupBy, 

152 ExponentialMovingWindowGroupBy, 

153 RollingGroupBy, 

154 ) 

155 

156 

157@final 

158class GroupByPlot(PandasObject): 

159 """ 

160 Class implementing the .plot attribute for groupby objects. 

161 """ 

162 

163 def __init__(self, groupby: GroupBy) -> None: 

164 self._groupby = groupby 

165 

166 def __call__(self, *args, **kwargs): 

167 def f(self, key): 

168 return self.plot(*args, **kwargs) 

169 

170 f.__name__ = "plot" 

171 

172 return self._groupby._python_apply_plot(f) 

173 

174 def __getattr__(self, name: str): 

175 def attr(*args, **kwargs): 

176 def f(self, key): 

177 local_kwargs = kwargs.copy() 

178 

179 if name == "scatter" and kwargs.get("legend"): 

180 local_kwargs.setdefault("label", key) 

181 

182 return getattr(self.plot, name)(*args, **local_kwargs) 

183 

184 return self._groupby._python_apply_plot(f) 

185 

186 return attr 

187 

188 

189_KeysArgType: TypeAlias = ( 

190 Hashable 

191 | list[Hashable] 

192 | Callable[[Hashable], Hashable] 

193 | list[Callable[[Hashable], Hashable]] 

194 | Mapping[Hashable, Hashable] 

195) 

196 

197 

198class BaseGroupBy(PandasObject, SelectionMixin[NDFrameT], GroupByIndexingMixin): 

199 _hidden_attrs = PandasObject._hidden_attrs | { 

200 "as_index", 

201 "dropna", 

202 "exclusions", 

203 "grouper", 

204 "group_keys", 

205 "keys", 

206 "level", 

207 "obj", 

208 "observed", 

209 "sort", 

210 } 

211 

212 _grouper: ops.BaseGrouper 

213 keys: _KeysArgType | None = None 

214 level: IndexLabel | None = None 

215 group_keys: bool 

216 

217 @final 

218 def __len__(self) -> int: 

219 return self._grouper.ngroups 

220 

221 @final 

222 def __repr__(self) -> str: 

223 # TODO: Better repr for GroupBy object 

224 return object.__repr__(self) 

225 

226 @final 

227 @property 

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

229 """ 

230 Dict {group name -> group labels}. 

231 

232 This property provides a dictionary representation of the groupings formed 

233 during a groupby operation, where each key represents a unique group value from 

234 the specified column(s), and each value is a list of index labels 

235 that belong to that group. 

236 

237 See Also 

238 -------- 

239 core.groupby.DataFrameGroupBy.get_group : Retrieve group from a 

240 ``DataFrameGroupBy`` object with provided name. 

241 core.groupby.SeriesGroupBy.get_group : Retrieve group from a 

242 ``SeriesGroupBy`` object with provided name. 

243 core.resample.Resampler.get_group : Retrieve group from a 

244 ``Resampler`` object with provided name. 

245 

246 Examples 

247 -------- 

248 

249 For SeriesGroupBy: 

250 

251 >>> lst = ["a", "a", "b"] 

252 >>> ser = pd.Series([1, 2, 3], index=lst) 

253 >>> ser 

254 a 1 

255 a 2 

256 b 3 

257 dtype: int64 

258 >>> ser.groupby(level=0).groups 

259 {'a': ['a', 'a'], 'b': ['b']} 

260 

261 For DataFrameGroupBy: 

262 

263 >>> data = [[1, 2, 3], [1, 5, 6], [7, 8, 9]] 

264 >>> df = pd.DataFrame(data, columns=["a", "b", "c"]) 

265 >>> df 

266 a b c 

267 0 1 2 3 

268 1 1 5 6 

269 2 7 8 9 

270 >>> df.groupby(by="a").groups 

271 {1: [0, 1], 7: [2]} 

272 

273 For Resampler: 

274 

275 >>> ser = pd.Series( 

276 ... [1, 2, 3, 4], 

277 ... index=pd.DatetimeIndex( 

278 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"] 

279 ... ), 

280 ... ) 

281 >>> ser 

282 2023-01-01 1 

283 2023-01-15 2 

284 2023-02-01 3 

285 2023-02-15 4 

286 dtype: int64 

287 >>> ser.resample("MS").groups 

288 {Timestamp('2023-01-01 00:00:00'): np.int64(2), 

289 Timestamp('2023-02-01 00:00:00'): np.int64(4)} 

290 """ 

291 if isinstance(self.keys, list) and len(self.keys) == 1: 

292 warnings.warn( 

293 "In a future version, the keys of `groups` will be a " 

294 f"tuple with a single element, e.g. ({self.keys[0]},) , " 

295 f"instead of a scalar, e.g. {self.keys[0]}, when grouping " 

296 "by a list with a single element. Use ``df.groupby(by='a').groups`` " 

297 "instead of ``df.groupby(by=['a']).groups`` to avoid this warning", 

298 Pandas4Warning, 

299 stacklevel=find_stack_level(), 

300 ) 

301 return self._grouper.groups 

302 

303 @final 

304 @property 

305 def ngroups(self) -> int: 

306 return self._grouper.ngroups 

307 

308 @final 

309 @property 

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

311 """ 

312 Dict {group name -> group indices}. 

313 

314 The dictionary keys represent the group labels (e.g., timestamps for a 

315 time-based resampling operation), and the values are arrays of integer 

316 positions indicating where the elements of each group are located in the 

317 original data. This property is particularly useful when working with 

318 resampled data, as it provides insight into how the original time-series data 

319 has been grouped. 

320 

321 See Also 

322 -------- 

323 core.groupby.DataFrameGroupBy.indices : Provides a mapping of group rows to 

324 positions of the elements. 

325 core.groupby.SeriesGroupBy.indices : Provides a mapping of group rows to 

326 positions of the elements. 

327 core.resample.Resampler.indices : Provides a mapping of group rows to 

328 positions of the elements. 

329 

330 Examples 

331 -------- 

332 

333 For SeriesGroupBy: 

334 

335 >>> lst = ["a", "a", "b"] 

336 >>> ser = pd.Series([1, 2, 3], index=lst) 

337 >>> ser 

338 a 1 

339 a 2 

340 b 3 

341 dtype: int64 

342 >>> ser.groupby(level=0).indices 

343 {'a': array([0, 1]), 'b': array([2])} 

344 

345 For DataFrameGroupBy: 

346 

347 >>> data = [[1, 2, 3], [1, 5, 6], [7, 8, 9]] 

348 >>> df = pd.DataFrame( 

349 ... data, columns=["a", "b", "c"], index=["owl", "toucan", "eagle"] 

350 ... ) 

351 >>> df 

352 a b c 

353 owl 1 2 3 

354 toucan 1 5 6 

355 eagle 7 8 9 

356 >>> df.groupby(by=["a"]).indices 

357 {np.int64(1): array([0, 1]), np.int64(7): array([2])} 

358 

359 For Resampler: 

360 

361 >>> ser = pd.Series( 

362 ... [1, 2, 3, 4], 

363 ... index=pd.DatetimeIndex( 

364 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"] 

365 ... ), 

366 ... ) 

367 >>> ser 

368 2023-01-01 1 

369 2023-01-15 2 

370 2023-02-01 3 

371 2023-02-15 4 

372 dtype: int64 

373 >>> ser.resample("MS").indices 

374 defaultdict(<class 'list'>, {Timestamp('2023-01-01 00:00:00'): [0, 1], 

375 Timestamp('2023-02-01 00:00:00'): [2, 3]}) 

376 """ 

377 return self._grouper.indices 

378 

379 @final 

380 def _get_index(self, name): 

381 """ 

382 Safe get multiple indices, translate keys for 

383 datelike to underlying repr. 

384 """ 

385 

386 def get_converter(s): 

387 # possibly convert to the actual key types 

388 # in the indices, could be a Timestamp or an np.datetime64 

389 if isinstance(s, datetime.datetime): 

390 return lambda key: Timestamp(key) 

391 elif isinstance(s, np.datetime64): 

392 return lambda key: Timestamp(key).asm8 

393 else: 

394 return lambda key: key 

395 

396 if isna(name): 

397 return self.indices.get(np.nan, []) 

398 if isinstance(name, tuple): 

399 name = tuple(np.nan if isna(comp) else comp for comp in name) 

400 

401 if len(self.indices) > 0: 

402 index_sample = next(iter(self.indices)) 

403 else: 

404 index_sample = None # Dummy sample 

405 

406 if isinstance(index_sample, tuple): 

407 if not isinstance(name, tuple): 

408 msg = "must supply a tuple to get_group with multiple grouping keys" 

409 raise ValueError(msg) 

410 if not len(name) == len(index_sample): 

411 try: 

412 # If the original grouper was a tuple 

413 return self.indices[name] 

414 except KeyError as err: 

415 # turns out it wasn't a tuple 

416 msg = ( 

417 "must supply a same-length tuple to get_group " 

418 "with multiple grouping keys" 

419 ) 

420 raise ValueError(msg) from err 

421 

422 converters = (get_converter(s) for s in index_sample) 

423 name = tuple(f(n) for f, n in zip(converters, name, strict=True)) 

424 else: 

425 converter = get_converter(index_sample) 

426 name = converter(name) 

427 

428 return self.indices.get(name, []) 

429 

430 @final 

431 @cache_readonly 

432 def _selected_obj(self): 

433 # Note: _selected_obj is always just `self.obj` for SeriesGroupBy 

434 if isinstance(self.obj, Series): 

435 return self.obj 

436 

437 if self._selection is not None: 

438 if is_hashable(self._selection): 

439 # i.e. a single key, so selecting it will return a Series. 

440 # In this case, _obj_with_exclusions would wrap the key 

441 # in a list and return a single-column DataFrame. 

442 return self.obj[self._selection] 

443 

444 # Otherwise _selection is equivalent to _selection_list, so 

445 # _selected_obj matches _obj_with_exclusions, so we can reuse 

446 # that and avoid making a copy. 

447 return self._obj_with_exclusions 

448 

449 return self.obj 

450 

451 @final 

452 def _dir_additions(self) -> set[str]: 

453 return self.obj._dir_additions() 

454 

455 @overload 

456 def pipe( 

457 self, 

458 func: Callable[Concatenate[Self, P], T], 

459 *args: P.args, 

460 **kwargs: P.kwargs, 

461 ) -> T: ... 

462 

463 @overload 

464 def pipe( 

465 self, 

466 func: tuple[Callable[..., T], str], 

467 *args: Any, 

468 **kwargs: Any, 

469 ) -> T: ... 

470 

471 def pipe( 

472 self, 

473 func: Callable[Concatenate[Self, P], T] | tuple[Callable[..., T], str], 

474 *args: Any, 

475 **kwargs: Any, 

476 ) -> T: 

477 """ 

478 Apply a ``func`` with arguments to this GroupBy object and return its result. 

479 

480 Use `.pipe` when you want to improve readability by chaining together 

481 functions that expect Series, DataFrames, GroupBy or Resampler objects. 

482 Instead of writing 

483 

484 >>> h = lambda x, arg2, arg3: x + 1 - arg2 * arg3 

485 >>> g = lambda x, arg1: x * 5 / arg1 

486 >>> f = lambda x: x**4 

487 >>> df = pd.DataFrame([["a", 4], ["b", 5]], columns=["group", "value"]) 

488 >>> h(g(f(df.groupby("group")), arg1=1), arg2=2, arg3=3) # doctest: +SKIP 

489 

490 You can write 

491 

492 >>> ( 

493 ... df.groupby("group").pipe(f).pipe(g, arg1=1).pipe(h, arg2=2, arg3=3) 

494 ... ) # doctest: +SKIP 

495 

496 which is much more readable. 

497 

498 Parameters 

499 ---------- 

500 func : callable or tuple of (callable, str) 

501 Function to apply to this GroupBy object or, alternatively, 

502 a `(callable, data_keyword)` tuple where `data_keyword` is a 

503 string indicating the keyword of `callable` that expects the 

504 GroupBy object. 

505 *args : iterable, optional 

506 Positional arguments passed into `func`. 

507 **kwargs : dict, optional 

508 A dictionary of keyword arguments passed into `func`. 

509 

510 Returns 

511 ------- 

512 GroupBy 

513 The return type of `func`. 

514 

515 See Also 

516 -------- 

517 Series.pipe : Apply a function with arguments to a series. 

518 DataFrame.pipe : Apply a function with arguments to a dataframe. 

519 apply : Apply function to each group instead of to the 

520 full GroupBy object. 

521 

522 Notes 

523 ----- 

524 See more `here 

525 <https://pandas.pydata.org/pandas-docs/stable/user_guide/groupby.html#piping-function-calls>`_ 

526 

527 Examples 

528 -------- 

529 >>> df = pd.DataFrame({"A": "a b a b".split(), "B": [1, 2, 3, 4]}) 

530 >>> df 

531 A B 

532 0 a 1 

533 1 b 2 

534 2 a 3 

535 3 b 4 

536 

537 To get the difference between each groups maximum and minimum value in one 

538 pass, you can do 

539 

540 >>> df.groupby("A").pipe(lambda x: x.max() - x.min()) 

541 B 

542 A 

543 a 2 

544 b 2 

545 """ 

546 return com.pipe(self, func, *args, **kwargs) 

547 

548 @final 

549 def get_group(self, name) -> DataFrame | Series: 

550 """ 

551 Construct DataFrame from group with provided name. 

552 

553 This method retrieves all rows belonging to a specific group identified 

554 by the given name. 

555 

556 Parameters 

557 ---------- 

558 name : object 

559 The name of the group to get as a DataFrame. 

560 

561 Returns 

562 ------- 

563 Series or DataFrame 

564 Get the respective Series or DataFrame corresponding to the group provided. 

565 

566 See Also 

567 -------- 

568 DataFrameGroupBy.groups: Dictionary representation of the groupings formed 

569 during a groupby operation. 

570 DataFrameGroupBy.indices: Provides a mapping of group rows to positions 

571 of the elements. 

572 SeriesGroupBy.groups: Dictionary representation of the groupings formed 

573 during a groupby operation. 

574 SeriesGroupBy.indices: Provides a mapping of group rows to positions 

575 of the elements. 

576 

577 Examples 

578 -------- 

579 

580 For SeriesGroupBy: 

581 

582 >>> lst = ["a", "a", "b"] 

583 >>> ser = pd.Series([1, 2, 3], index=lst) 

584 >>> ser 

585 a 1 

586 a 2 

587 b 3 

588 dtype: int64 

589 >>> ser.groupby(level=0).get_group("a") 

590 a 1 

591 a 2 

592 dtype: int64 

593 

594 For DataFrameGroupBy: 

595 

596 >>> data = [[1, 2, 3], [1, 5, 6], [7, 8, 9]] 

597 >>> df = pd.DataFrame( 

598 ... data, columns=["a", "b", "c"], index=["owl", "toucan", "eagle"] 

599 ... ) 

600 >>> df 

601 a b c 

602 owl 1 2 3 

603 toucan 1 5 6 

604 eagle 7 8 9 

605 >>> df.groupby(by=["a"]).get_group((1,)) 

606 a b c 

607 owl 1 2 3 

608 toucan 1 5 6 

609 

610 For Resampler: 

611 

612 >>> ser = pd.Series( 

613 ... [1, 2, 3, 4], 

614 ... index=pd.DatetimeIndex( 

615 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"] 

616 ... ), 

617 ... ) 

618 >>> ser 

619 2023-01-01 1 

620 2023-01-15 2 

621 2023-02-01 3 

622 2023-02-15 4 

623 dtype: int64 

624 >>> ser.resample("MS").get_group("2023-01-01") 

625 2023-01-01 1 

626 2023-01-15 2 

627 dtype: int64 

628 """ 

629 keys = self.keys 

630 level = self.level 

631 # mypy doesn't recognize level/keys as being sized when passed to len 

632 if (is_list_like(level) and len(level) == 1) or ( # type: ignore[arg-type] 

633 is_list_like(keys) and len(keys) == 1 # type: ignore[arg-type] 

634 ): 

635 # GH#25971 

636 if isinstance(name, tuple) and len(name) == 1: 

637 name = name[0] 

638 else: 

639 raise KeyError(name) 

640 

641 inds = self._get_index(name) 

642 if not len(inds): 

643 raise KeyError(name) 

644 return self._selected_obj.iloc[inds] 

645 

646 @final 

647 def __iter__(self) -> Iterator[tuple[Hashable, NDFrameT]]: 

648 """ 

649 Groupby iterator. 

650 

651 This method provides an iterator over the groups created by the ``resample`` 

652 or ``groupby`` operation on the object. The method yields tuples where 

653 the first element is the label (group key) corresponding to each group or 

654 resampled bin, and the second element is the subset of the data that falls 

655 within that group or bin. 

656 

657 Returns 

658 ------- 

659 Iterator 

660 Generator yielding a sequence of (name, subsetted object) 

661 for each group. 

662 

663 See Also 

664 -------- 

665 Series.groupby : Group data by a specific key or column. 

666 DataFrame.groupby : Group DataFrame using mapper or by columns. 

667 DataFrame.resample : Resample a DataFrame. 

668 Series.resample : Resample a Series. 

669 

670 Examples 

671 -------- 

672 

673 For SeriesGroupBy: 

674 

675 >>> lst = ["a", "a", "b"] 

676 >>> ser = pd.Series([1, 2, 3], index=lst) 

677 >>> ser 

678 a 1 

679 a 2 

680 b 3 

681 dtype: int64 

682 >>> for x, y in ser.groupby(level=0): 

683 ... print(f"{x}\\n{y}\\n") 

684 a 

685 a 1 

686 a 2 

687 dtype: int64 

688 b 

689 b 3 

690 dtype: int64 

691 

692 For DataFrameGroupBy: 

693 

694 >>> data = [[1, 2, 3], [1, 5, 6], [7, 8, 9]] 

695 >>> df = pd.DataFrame(data, columns=["a", "b", "c"]) 

696 >>> df 

697 a b c 

698 0 1 2 3 

699 1 1 5 6 

700 2 7 8 9 

701 >>> for x, y in df.groupby(by=["a"]): 

702 ... print(f"{x}\\n{y}\\n") 

703 (1,) 

704 a b c 

705 0 1 2 3 

706 1 1 5 6 

707 (7,) 

708 a b c 

709 2 7 8 9 

710 

711 For Resampler: 

712 

713 >>> ser = pd.Series( 

714 ... [1, 2, 3, 4], 

715 ... index=pd.DatetimeIndex( 

716 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"] 

717 ... ), 

718 ... ) 

719 >>> ser 

720 2023-01-01 1 

721 2023-01-15 2 

722 2023-02-01 3 

723 2023-02-15 4 

724 dtype: int64 

725 >>> for x, y in ser.resample("MS"): 

726 ... print(f"{x}\\n{y}\\n") 

727 2023-01-01 00:00:00 

728 2023-01-01 1 

729 2023-01-15 2 

730 dtype: int64 

731 2023-02-01 00:00:00 

732 2023-02-01 3 

733 2023-02-15 4 

734 dtype: int64 

735 """ 

736 keys = self.keys 

737 level = self.level 

738 result = self._grouper.get_iterator(self._selected_obj) 

739 # mypy: Argument 1 to "len" has incompatible type "Hashable"; expected "Sized" 

740 if (is_list_like(level) and len(level) == 1) or ( # type: ignore[arg-type] 

741 isinstance(keys, list) and len(keys) == 1 

742 ): 

743 # GH#42795 - when keys is a list, return tuples even when length is 1 

744 result = (((key,), group) for key, group in result) 

745 return result 

746 

747 

748# To track operations that expand dimensions, like ohlc 

749OutputFrameOrSeries = TypeVar("OutputFrameOrSeries", bound=NDFrame) 

750 

751 

752class GroupBy(BaseGroupBy[NDFrameT]): 

753 """ 

754 Class for grouping and aggregating relational data. 

755 

756 See aggregate, transform, and apply functions on this object. 

757 

758 It's easiest to use obj.groupby(...) to use GroupBy, but you can also do: 

759 

760 :: 

761 

762 grouped = groupby(obj, ...) 

763 

764 Parameters 

765 ---------- 

766 obj : pandas object 

767 level : int, default None 

768 Level of MultiIndex 

769 groupings : list of Grouping objects 

770 Most users should ignore this 

771 exclusions : array-like, optional 

772 List of columns to exclude 

773 name : str 

774 Most users should ignore this 

775 

776 Returns 

777 ------- 

778 **Attributes** 

779 groups : dict 

780 {group name -> group labels} 

781 len(grouped) : int 

782 Number of groups 

783 

784 Notes 

785 ----- 

786 After grouping, see aggregate, apply, and transform functions. Here are 

787 some other brief notes about usage. When grouping by multiple groups, the 

788 result index will be a MultiIndex (hierarchical) by default. 

789 

790 Iteration produces (key, group) tuples, i.e. chunking the data by group. So 

791 you can write code like: 

792 

793 :: 

794 

795 grouped = obj.groupby(keys) 

796 for key, group in grouped: 

797 # do something with the data 

798 

799 Function calls on GroupBy, if not specially implemented, "dispatch" to the 

800 grouped data. So if you group a DataFrame and wish to invoke the std() 

801 method on each group, you can simply do: 

802 

803 :: 

804 

805 df.groupby(mapper).std() 

806 

807 rather than 

808 

809 :: 

810 

811 df.groupby(mapper).aggregate(np.std) 

812 

813 You can pass arguments to these "wrapped" functions, too. 

814 

815 See the online documentation for full exposition on these topics and much 

816 more 

817 """ 

818 

819 _grouper: ops.BaseGrouper 

820 as_index: bool 

821 

822 @final 

823 def __init__( 

824 self, 

825 obj: NDFrameT, 

826 keys: _KeysArgType | None = None, 

827 level: IndexLabel | None = None, 

828 grouper: ops.BaseGrouper | None = None, 

829 exclusions: frozenset[Hashable] | None = None, 

830 selection: IndexLabel | None = None, 

831 as_index: bool = True, 

832 sort: bool = True, 

833 group_keys: bool = True, 

834 observed: bool = True, 

835 dropna: bool = True, 

836 ) -> None: 

837 self._selection = selection 

838 

839 assert isinstance(obj, NDFrame), type(obj) 

840 

841 self.level = level 

842 self.as_index = as_index 

843 self.keys = keys 

844 self.sort = sort 

845 self.group_keys = group_keys 

846 self.dropna = dropna 

847 

848 if grouper is None: 

849 grouper, exclusions, obj = get_grouper( 

850 obj, 

851 keys, 

852 level=level, 

853 sort=sort, 

854 observed=observed, 

855 dropna=self.dropna, 

856 ) 

857 

858 self.observed = observed 

859 self.obj = obj 

860 self._grouper = grouper 

861 self.exclusions = frozenset(exclusions) if exclusions else frozenset() 

862 

863 def __getattr__(self, attr: str): 

864 if attr in self._internal_names_set: 

865 return object.__getattribute__(self, attr) 

866 if attr in self.obj: 

867 return self[attr] 

868 

869 raise AttributeError( 

870 f"'{type(self).__name__}' object has no attribute '{attr}'" 

871 ) 

872 

873 @final 

874 def _op_via_apply(self, name: str, *args, **kwargs): 

875 """Compute the result of an operation by using GroupBy's apply.""" 

876 f = getattr(type(self._obj_with_exclusions), name) 

877 

878 def curried(x, key=None): 

879 return f(x, *args, **kwargs) 

880 

881 # preserve the name so we can detect it when calling plot methods, 

882 # to avoid duplicates 

883 curried.__name__ = name 

884 

885 # special case otherwise extra plots are created when catching the 

886 # exception below 

887 if name in base.plotting_methods: 

888 return self._python_apply_plot(curried) 

889 

890 is_transform = name in base.transformation_kernels 

891 result = self._python_apply_general( 

892 curried, 

893 self._obj_with_exclusions, 

894 is_transform=is_transform, 

895 not_indexed_same=not is_transform, 

896 ) 

897 

898 if self._grouper.has_dropped_na and is_transform: 

899 # result will have dropped rows due to nans, fill with null 

900 # and ensure index is ordered same as the input 

901 result = self._set_result_index_ordered(result) 

902 return result 

903 

904 # ----------------------------------------------------------------- 

905 # Dispatch/Wrapping 

906 

907 @final 

908 def _concat_objects( 

909 self, 

910 values, 

911 not_indexed_same: bool = False, 

912 is_transform: bool = False, 

913 ): 

914 from pandas.core.reshape.concat import concat 

915 

916 if self.group_keys and not is_transform: 

917 if self.as_index: 

918 # possible MI return case 

919 group_keys = self._grouper.result_index 

920 group_levels = self._grouper.levels 

921 group_names = self._grouper.names 

922 

923 result = concat( 

924 values, 

925 axis=0, 

926 keys=group_keys, 

927 levels=group_levels, 

928 names=group_names, 

929 sort=False, 

930 ) 

931 else: 

932 result = concat(values, axis=0) 

933 

934 elif not not_indexed_same: 

935 result = concat(values, axis=0) 

936 

937 ax = self._selected_obj.index 

938 if self.dropna: 

939 labels = self._grouper.ids 

940 mask = labels != -1 

941 ax = ax[mask] 

942 

943 # this is a very unfortunate situation 

944 # we can't use reindex to restore the original order 

945 # when the ax has duplicates 

946 # so we resort to this 

947 # GH 14776, 30667 

948 # TODO: can we reuse e.g. _reindex_non_unique? 

949 if ax.has_duplicates and not result.axes[0].equals(ax): 

950 # e.g. test_category_order_transformer 

951 target = algorithms.unique1d(ax._values) 

952 indexer, _ = result.index.get_indexer_non_unique(target) 

953 result = result.take(indexer, axis=0) 

954 else: 

955 result = result.reindex(ax, axis=0) 

956 

957 else: 

958 result = concat(values, axis=0) 

959 

960 if self.obj.ndim == 1: 

961 name = self.obj.name 

962 elif is_hashable(self._selection): 

963 name = self._selection 

964 else: 

965 name = None 

966 

967 if isinstance(result, Series) and name is not None: 

968 result.name = name 

969 

970 return result.__finalize__(self.obj, method="groupby") 

971 

972 @final 

973 def _set_result_index_ordered( 

974 self, result: OutputFrameOrSeries 

975 ) -> OutputFrameOrSeries: 

976 # set the result index on the passed values object and 

977 # return the new object, xref 8046 

978 

979 index = self.obj.index 

980 

981 if self._grouper.is_monotonic and not self._grouper.has_dropped_na: 

982 # shortcut if we have an already ordered grouper 

983 result = result.set_axis(index, axis=0) 

984 return result 

985 

986 # row order is scrambled => sort the rows by position in original index 

987 original_positions = Index(self._grouper.result_ilocs, copy=False) 

988 result = result.set_axis(original_positions, axis=0) 

989 result = result.sort_index(axis=0) 

990 if self._grouper.has_dropped_na: 

991 # Add back in any missing rows due to dropna - index here is integral 

992 # with values referring to the row of the input so can use RangeIndex 

993 result = result.reindex(default_index(len(index)), axis=0) 

994 result = result.set_axis(index, axis=0) 

995 

996 return result 

997 

998 @final 

999 def _insert_inaxis_grouper( 

1000 self, result: Series | DataFrame, qs: npt.NDArray[np.float64] | None = None 

1001 ) -> DataFrame: 

1002 if isinstance(result, Series): 

1003 result = result.to_frame() 

1004 

1005 n_groupings = len(self._grouper.groupings) 

1006 

1007 def full_key(name: Hashable) -> Hashable: 

1008 # GH#17024 expanding MultiIndex columns with a partial key is 

1009 # deprecated; pad the key to full length ourselves 

1010 if isinstance(result.columns, MultiIndex) and not isinstance(name, tuple): 

1011 return (name,) + ("",) * (result.columns.nlevels - 1) 

1012 return name 

1013 

1014 if qs is not None: 

1015 result.insert( 

1016 0, full_key(f"level_{n_groupings}"), np.tile(qs, len(result) // len(qs)) 

1017 ) 

1018 

1019 # zip in reverse so we can always insert at loc 0 

1020 for level, (name, lev) in enumerate( 

1021 zip( 

1022 reversed(self._grouper.names), 

1023 self._grouper.get_group_levels(), 

1024 strict=True, 

1025 ) 

1026 ): 

1027 if name is None: 

1028 # Behave the same as .reset_index() when a level is unnamed 

1029 name = ( 

1030 "index" 

1031 if n_groupings == 1 and qs is None 

1032 else f"level_{n_groupings - level - 1}" 

1033 ) 

1034 

1035 # GH #28549 

1036 # When using .apply(-), name will be in columns already 

1037 if name not in result.columns: 

1038 # if in_axis: 

1039 if qs is None: 

1040 result.insert(0, full_key(name), lev) 

1041 else: 

1042 result.insert( 

1043 0, full_key(name), Index(np.repeat(lev, len(qs)), copy=False) 

1044 ) 

1045 

1046 return result 

1047 

1048 @final 

1049 def _wrap_aggregated_output( 

1050 self, 

1051 result: Series | DataFrame, 

1052 qs: npt.NDArray[np.float64] | None = None, 

1053 ): 

1054 """ 

1055 Wraps the output of GroupBy aggregations into the expected result. 

1056 

1057 Parameters 

1058 ---------- 

1059 result : Series, DataFrame 

1060 

1061 Returns 

1062 ------- 

1063 Series or DataFrame 

1064 """ 

1065 # ATM we do not get here for SeriesGroupBy; when we do, we will 

1066 # need to require that result.name already match self.obj.name 

1067 

1068 if not self.as_index: 

1069 # `not self.as_index` is only relevant for DataFrameGroupBy, 

1070 # enforced in __init__ 

1071 result = self._insert_inaxis_grouper(result, qs=qs) 

1072 result = result._consolidate() 

1073 result.index = default_index(len(result)) 

1074 

1075 else: 

1076 index = self._grouper.result_index 

1077 if qs is not None: 

1078 # We get here with len(qs) != 1 and not self.as_index 

1079 # in test_pass_args_kwargs 

1080 index = _insert_quantile_level(index, qs) 

1081 result.index = index 

1082 

1083 return result 

1084 

1085 def _wrap_applied_output( 

1086 self, 

1087 data, 

1088 values: list, 

1089 not_indexed_same: bool = False, 

1090 is_transform: bool = False, 

1091 ): 

1092 raise AbstractMethodError(self) 

1093 

1094 # ----------------------------------------------------------------- 

1095 # numba 

1096 

1097 @final 

1098 def _numba_prep(self, data: DataFrame): 

1099 ngroups = self._grouper.ngroups 

1100 sorted_index = self._grouper.result_ilocs 

1101 sorted_ids = self._grouper._sorted_ids 

1102 

1103 sorted_data = data.take(sorted_index, axis=0).to_numpy() 

1104 # GH 46867 

1105 index_data = data.index 

1106 if isinstance(index_data, MultiIndex): 

1107 if len(self._grouper.groupings) > 1: 

1108 raise NotImplementedError( 

1109 "Grouping with more than 1 grouping labels and " 

1110 "a MultiIndex is not supported with engine='numba'" 

1111 ) 

1112 group_key = self._grouper.groupings[0].name 

1113 index_data = index_data.get_level_values(group_key) 

1114 sorted_index_data = index_data.take(sorted_index).to_numpy() 

1115 

1116 starts, ends = lib.generate_slices(sorted_ids, ngroups) 

1117 return ( 

1118 starts, 

1119 ends, 

1120 sorted_index_data, 

1121 sorted_data, 

1122 ) 

1123 

1124 def _numba_agg_general( 

1125 self, 

1126 func: Callable, 

1127 dtype_mapping: dict[np.dtype, Any], 

1128 engine_kwargs: dict[str, bool] | None, 

1129 **aggregator_kwargs, 

1130 ): 

1131 """ 

1132 Perform groupby with a standard numerical aggregation function (e.g. mean) 

1133 with Numba. 

1134 """ 

1135 if not self.as_index: 

1136 raise NotImplementedError( 

1137 "as_index=False is not supported. Use .reset_index() instead." 

1138 ) 

1139 

1140 data = self._obj_with_exclusions 

1141 df = data if data.ndim == 2 else data.to_frame() 

1142 

1143 aggregator = executor.generate_shared_aggregator( 

1144 func, 

1145 dtype_mapping, 

1146 True, # is_grouped_kernel 

1147 **get_jit_arguments(engine_kwargs), 

1148 ) 

1149 # Pass group ids to kernel directly if it can handle it 

1150 # (This is faster since it doesn't require a sort) 

1151 ids = self._grouper.ids 

1152 ngroups = self._grouper.ngroups 

1153 

1154 res_mgr = df._mgr.apply( 

1155 aggregator, labels=ids, ngroups=ngroups, **aggregator_kwargs 

1156 ) 

1157 res_mgr.axes[1] = self._grouper.result_index 

1158 result = df._constructor_from_mgr(res_mgr, axes=res_mgr.axes) 

1159 

1160 if data.ndim == 1: 

1161 result = result.squeeze("columns") 

1162 result.name = data.name 

1163 else: 

1164 result.columns = data.columns 

1165 return result 

1166 

1167 @final 

1168 def _transform_with_numba(self, func, *args, engine_kwargs=None, **kwargs): 

1169 """ 

1170 Perform groupby transform routine with the numba engine. 

1171 

1172 This routine mimics the data splitting routine of the DataSplitter class 

1173 to generate the indices of each group in the sorted data and then passes the 

1174 data and indices into a Numba jitted function. 

1175 """ 

1176 data = self._obj_with_exclusions 

1177 index_sorting = self._grouper.result_ilocs 

1178 df = data if data.ndim == 2 else data.to_frame() 

1179 

1180 starts, ends, sorted_index, sorted_data = self._numba_prep(df) 

1181 numba_.validate_udf(func) 

1182 args, kwargs = prepare_function_arguments( 

1183 func, args, kwargs, num_required_args=2 

1184 ) 

1185 numba_transform_func = numba_.generate_numba_transform_func( 

1186 func, **get_jit_arguments(engine_kwargs) 

1187 ) 

1188 result = numba_transform_func( 

1189 sorted_data, 

1190 sorted_index, 

1191 starts, 

1192 ends, 

1193 len(df.columns), 

1194 *args, 

1195 ) 

1196 # result values needs to be resorted to their original positions since we 

1197 # evaluated the data sorted by group 

1198 result = result.take(np.argsort(index_sorting), axis=0) 

1199 index = data.index 

1200 if data.ndim == 1: 

1201 result_kwargs = {"name": data.name} 

1202 result = result.ravel() 

1203 else: 

1204 result_kwargs = {"columns": data.columns} 

1205 return data._constructor(result, index=index, **result_kwargs) 

1206 

1207 @final 

1208 def _aggregate_with_numba(self, func, *args, engine_kwargs=None, **kwargs): 

1209 """ 

1210 Perform groupby aggregation routine with the numba engine. 

1211 

1212 This routine mimics the data splitting routine of the DataSplitter class 

1213 to generate the indices of each group in the sorted data and then passes the 

1214 data and indices into a Numba jitted function. 

1215 """ 

1216 data = self._obj_with_exclusions 

1217 df = data if data.ndim == 2 else data.to_frame() 

1218 

1219 starts, ends, sorted_index, sorted_data = self._numba_prep(df) 

1220 numba_.validate_udf(func) 

1221 args, kwargs = prepare_function_arguments( 

1222 func, args, kwargs, num_required_args=2 

1223 ) 

1224 numba_agg_func = numba_.generate_numba_agg_func( 

1225 func, **get_jit_arguments(engine_kwargs) 

1226 ) 

1227 result = numba_agg_func( 

1228 sorted_data, 

1229 sorted_index, 

1230 starts, 

1231 ends, 

1232 len(df.columns), 

1233 *args, 

1234 ) 

1235 index = self._grouper.result_index 

1236 if data.ndim == 1: 

1237 result_kwargs = {"name": data.name} 

1238 result = result.ravel() 

1239 else: 

1240 result_kwargs = {"columns": data.columns} 

1241 res = data._constructor(result, index=index, **result_kwargs) 

1242 if not self.as_index: 

1243 res = self._insert_inaxis_grouper(res) 

1244 res.index = default_index(len(res)) 

1245 return res 

1246 

1247 # ----------------------------------------------------------------- 

1248 # apply/agg/transform 

1249 

1250 def apply(self, func, *args, include_groups: bool = False, **kwargs) -> NDFrameT: 

1251 """ 

1252 Apply function ``func`` group-wise and combine the results together. 

1253 

1254 The function passed to ``apply`` must take a dataframe as its first 

1255 argument and return a DataFrame, Series or scalar. ``apply`` will 

1256 then take care of combining the results back together into a single 

1257 dataframe or series. ``apply`` is therefore a highly flexible 

1258 grouping method. 

1259 

1260 While ``apply`` is a very flexible method, its downside is that 

1261 using it can be quite a bit slower than using more specific methods 

1262 like ``agg`` or ``transform``. Pandas offers a wide range of method that will 

1263 be much faster than using ``apply`` for their specific purposes, so try to 

1264 use them before reaching for ``apply``. 

1265 

1266 Parameters 

1267 ---------- 

1268 func : callable 

1269 A callable that takes a dataframe as its first argument, and 

1270 returns a dataframe, a series or a scalar. In addition the 

1271 callable may take positional and keyword arguments. 

1272 

1273 *args : tuple 

1274 Optional positional arguments to pass to ``func``. 

1275 

1276 include_groups : bool, default False 

1277 When True, will attempt to apply ``func`` to the groupings in 

1278 the case that they are columns of the DataFrame. If this raises a 

1279 TypeError, the result will be computed with the groupings excluded. 

1280 When False, the groupings will be excluded when applying ``func``. 

1281 

1282 .. versionadded:: 2.2.0 

1283 

1284 .. versionchanged:: 3.0.0 

1285 

1286 The default changed from True to False, and True is no longer allowed. 

1287 

1288 **kwargs : dict 

1289 Optional keyword arguments to pass to ``func``. 

1290 

1291 Returns 

1292 ------- 

1293 Series or DataFrame 

1294 A pandas object with the result of applying ``func`` to each group. 

1295 

1296 See Also 

1297 -------- 

1298 pipe : Apply function to the full GroupBy object instead of to each 

1299 group. 

1300 aggregate : Apply aggregate function to the GroupBy object. 

1301 transform : Apply function column-by-column to the GroupBy object. 

1302 Series.apply : Apply a function to a Series. 

1303 DataFrame.apply : Apply a function to each row or column of a DataFrame. 

1304 

1305 Notes 

1306 ----- 

1307 See :ref:`groupby.apply` in the User Guide for more details and examples. 

1308 

1309 The resulting dtype will reflect the return value of the passed ``func``, 

1310 see the examples below. 

1311 

1312 Functions that mutate the passed object can produce unexpected 

1313 behavior or errors and are not supported. See :ref:`gotchas.udf-mutation` 

1314 for more details. 

1315 

1316 Each group passed to ``func`` has a ``name`` attribute set to the group 

1317 key (the value of the grouping for that group). This is useful for 

1318 identifying the current group, for example when grouping by an external 

1319 grouper rather than by a column of the object. 

1320 

1321 Examples 

1322 -------- 

1323 >>> df = pd.DataFrame({"A": "a a b".split(), "B": [1, 2, 3], "C": [4, 6, 5]}) 

1324 >>> g1 = df.groupby("A", group_keys=False) 

1325 >>> g2 = df.groupby("A", group_keys=True) 

1326 

1327 Notice that ``g1`` and ``g2`` have two groups, ``a`` and ``b``, and only 

1328 differ in their ``group_keys`` argument. Calling `apply` in various ways, 

1329 we can get different grouping results: 

1330 

1331 Example 1: below the function passed to `apply` takes a DataFrame as 

1332 its argument and returns a DataFrame. `apply` combines the result for 

1333 each group together into a new DataFrame: 

1334 

1335 >>> g1[["B", "C"]].apply(lambda x: x / x.sum()) 

1336 B C 

1337 0 0.333333 0.4 

1338 1 0.666667 0.6 

1339 2 1.000000 1.0 

1340 

1341 In the above, the groups are not part of the index. We can have them included 

1342 by using ``g2`` where ``group_keys=True``: 

1343 

1344 >>> g2[["B", "C"]].apply(lambda x: x / x.sum()) 

1345 B C 

1346 A 

1347 a 0 0.333333 0.4 

1348 1 0.666667 0.6 

1349 b 2 1.000000 1.0 

1350 

1351 Example 2: The function passed to `apply` takes a DataFrame as 

1352 its argument and returns a Series. `apply` combines the result for 

1353 each group together into a new DataFrame. 

1354 

1355 The resulting dtype will reflect the return value of the passed ``func``. 

1356 

1357 >>> g1[["B", "C"]].apply(lambda x: x.astype(float).max() - x.min()) 

1358 B C 

1359 A 

1360 a 1.0 2.0 

1361 b 0.0 0.0 

1362 

1363 >>> g2[["B", "C"]].apply(lambda x: x.astype(float).max() - x.min()) 

1364 B C 

1365 A 

1366 a 1.0 2.0 

1367 b 0.0 0.0 

1368 

1369 The ``group_keys`` argument has no effect here because the result is not 

1370 like-indexed (i.e. :ref:`a transform <groupby.transform>`) when compared 

1371 to the input. 

1372 

1373 Example 3: The function passed to `apply` takes a DataFrame as 

1374 its argument and returns a scalar. `apply` combines the result for 

1375 each group together into a Series, including setting the index as 

1376 appropriate: 

1377 

1378 >>> g1.apply(lambda x: x.C.max() - x.B.min()) 

1379 A 

1380 a 5 

1381 b 2 

1382 dtype: int64 

1383 

1384 Example 4: The function passed to ``apply`` returns ``None`` for one of the 

1385 group. This group is filtered from the result: 

1386 

1387 >>> g1.apply(lambda x: None if x.iloc[0, 0] == 3 else x) 

1388 B C 

1389 0 1 4 

1390 1 2 6 

1391 """ 

1392 if include_groups: 

1393 raise ValueError("include_groups=True is no longer allowed.") 

1394 if isinstance(func, str): 

1395 if hasattr(self, func): 

1396 res = getattr(self, func) 

1397 if callable(res): 

1398 return res(*args, **kwargs) 

1399 elif args or kwargs: 

1400 raise ValueError(f"Cannot pass arguments to property {func}") 

1401 return res 

1402 

1403 else: 

1404 raise TypeError(f"apply func should be callable, not '{func}'") 

1405 

1406 elif args or kwargs: 

1407 if callable(func): 

1408 

1409 @wraps(func) 

1410 def f(g): 

1411 return func(g, *args, **kwargs) 

1412 

1413 else: 

1414 raise ValueError( 

1415 "func must be a callable if args or kwargs are supplied" 

1416 ) 

1417 else: 

1418 f = func 

1419 

1420 return self._python_apply_general(f, self._obj_with_exclusions) 

1421 

1422 @final 

1423 def _python_apply_general( 

1424 self, 

1425 f: Callable, 

1426 data: DataFrame | Series, 

1427 not_indexed_same: bool | None = None, 

1428 is_transform: bool = False, 

1429 ) -> NDFrameT: 

1430 """ 

1431 Apply function f in python space 

1432 

1433 Parameters 

1434 ---------- 

1435 f : callable 

1436 Function to apply 

1437 data : Series or DataFrame 

1438 Data to apply f to 

1439 not_indexed_same: bool, optional 

1440 When specified, overrides the value of not_indexed_same. Apply behaves 

1441 differently when the result index is equal to the input index, but 

1442 this can be coincidental leading to value-dependent behavior. 

1443 is_transform : bool, default False 

1444 Indicator for whether the function is actually a transform 

1445 and should not have group keys prepended. 

1446 

1447 Returns 

1448 ------- 

1449 Series or DataFrame 

1450 data after applying f 

1451 """ 

1452 values, mutated = self._grouper.apply_groupwise(f, data) 

1453 if not_indexed_same is None: 

1454 not_indexed_same = mutated 

1455 

1456 return self._wrap_applied_output( 

1457 data, 

1458 values, 

1459 not_indexed_same, 

1460 is_transform, 

1461 ) 

1462 

1463 @final 

1464 def _python_apply_plot(self, f: Callable) -> NDFrameT: 

1465 """ 

1466 Apply a plotting function f group-wise. 

1467 

1468 Each Series group gets its group key as the name, which plotting 

1469 methods use for legend labels. Unlike the general apply path, this 

1470 labels the group explicitly rather than relying on the name attribute 

1471 being pinned to the group key as a side effect (GH#41090). 

1472 """ 

1473 data = self._selected_obj 

1474 values = [] 

1475 for key, group in self._grouper.get_iterator(data): 

1476 if group.ndim == 1: 

1477 group.name = key 

1478 values.append(f(group, key)) 

1479 # plotting functions return matplotlib objects, never something 

1480 # indexed like the group, so this is always not_indexed_same 

1481 return self._wrap_applied_output(data, values, not_indexed_same=True) 

1482 

1483 def _agg_py_fallback( 

1484 self, how: str, values: ArrayLike, ndim: int, alt: Callable 

1485 ) -> ArrayLike: 

1486 """ 

1487 Fallback to pure-python aggregation if _cython_operation raises 

1488 NotImplementedError. 

1489 """ 

1490 # We get here with a) EADtypes and b) object dtype 

1491 assert alt is not None 

1492 

1493 if values.ndim == 1: 

1494 # For DataFrameGroupBy we only get here with ExtensionArray 

1495 ser = Series(values, copy=False) 

1496 else: 

1497 # We only get here with values.dtype == object 

1498 df = DataFrame(values.T, dtype=values.dtype) 

1499 # bc we split object blocks in grouped_reduce, we have only 1 col 

1500 # otherwise we'd have to worry about block-splitting GH#39329 

1501 assert df.shape[1] == 1 

1502 # Avoid call to self.values that can occur in DataFrame 

1503 # reductions; see GH#28949 

1504 ser = df.iloc[:, 0] 

1505 

1506 # We do not get here with UDFs, so we know that our dtype 

1507 # should always be preserved by the implemented aggregations 

1508 # TODO: Is this exactly right; see WrappedCythonOp get_result_dtype? 

1509 try: 

1510 res_values = self._grouper.agg_series(ser, alt) 

1511 except Exception as err: 

1512 msg = f"agg function failed [how->{how},dtype->{ser.dtype}]" 

1513 # preserve the kind of exception that raised 

1514 raise type(err)(msg) from err 

1515 

1516 dtype = ser.dtype 

1517 if dtype == object: 

1518 res_values = res_values.astype(object, copy=False) 

1519 elif is_string_dtype(dtype): 

1520 # mypy doesn't infer dtype is an ExtensionDtype 

1521 string_array_cls = dtype.construct_array_type() # type: ignore[union-attr] 

1522 res_values = string_array_cls._from_sequence(res_values, dtype=dtype) 

1523 

1524 # If we are DataFrameGroupBy and went through a SeriesGroupByPath 

1525 # then we need to reshape 

1526 # GH#32223 includes case with IntegerArray values, ndarray res_values 

1527 # test_groupby_duplicate_columns with object dtype values 

1528 return ensure_block_shape(res_values, ndim=ndim) 

1529 

1530 @final 

1531 def _cython_agg_general( 

1532 self, 

1533 how: str, 

1534 alt: Callable | None = None, 

1535 numeric_only: bool = False, 

1536 min_count: int = -1, 

1537 **kwargs, 

1538 ): 

1539 # Note: we never get here with how="ohlc" for DataFrameGroupBy; 

1540 # that goes through SeriesGroupBy 

1541 

1542 if not is_bool(numeric_only): 

1543 raise ValueError("numeric_only accepts only Boolean values") 

1544 

1545 data = self._get_data_to_aggregate(numeric_only=numeric_only, name=how) 

1546 

1547 def array_func(values: ArrayLike) -> ArrayLike: 

1548 # GH#37850 For numpy boolean data, any==max and all==min. 

1549 # The min/max Cython path is faster (fused types, no mask overhead). 

1550 # Excludes nullable BooleanDtype which needs Kleene logic. 

1551 use_bool_fastpath = how in ["any", "all"] and values.dtype == np.dtype( 

1552 "bool" 

1553 ) 

1554 if use_bool_fastpath: 

1555 _how = "max" if how == "any" else "min" 

1556 else: 

1557 _how = how 

1558 try: 

1559 result = self._grouper._cython_operation( 

1560 values, 

1561 _how, 

1562 axis=data.ndim - 1, 

1563 min_count=min_count, 

1564 **kwargs, 

1565 ) 

1566 except NotImplementedError as err: 

1567 # generally if we have numeric_only=False 

1568 # and non-applicable functions 

1569 # try to python agg 

1570 # TODO: shouldn't min_count matter? 

1571 if alt is None or how in ["any", "all", "std", "sem"]: 

1572 raise TypeError( 

1573 f"{how} is not supported for {values.dtype} dtype" 

1574 ) from err 

1575 else: 

1576 if use_bool_fastpath and result.dtype.kind == "f": 

1577 fill = 0.0 if how == "any" else 1.0 

1578 result = np.where( 

1579 np.isnan(result), fill, np.asarray(result) 

1580 ).astype(bool) 

1581 return result 

1582 

1583 assert alt is not None 

1584 result = self._agg_py_fallback(how, values, ndim=data.ndim, alt=alt) 

1585 return result 

1586 

1587 new_mgr = data.grouped_reduce(array_func) 

1588 res = self._wrap_agged_manager(new_mgr) 

1589 if how in ["idxmin", "idxmax"]: 

1590 # mypy expects how to be Literal["idxmin", "idxmax"]. 

1591 res = self._wrap_idxmax_idxmin(res, how=how, skipna=kwargs["skipna"]) # type: ignore[arg-type] 

1592 out = self._wrap_aggregated_output(res) 

1593 return out.__finalize__(self.obj, method="groupby") 

1594 

1595 def _cython_transform(self, how: str, numeric_only: bool = False, **kwargs): 

1596 raise AbstractMethodError(self) 

1597 

1598 @final 

1599 def _transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs): 

1600 # optimized transforms 

1601 if not isinstance(func, str): 

1602 return self._transform_general(func, engine, engine_kwargs, *args, **kwargs) 

1603 

1604 elif func not in base.transform_kernel_allowlist: 

1605 msg = f"'{func}' is not a valid function name for transform(name)" 

1606 raise ValueError(msg) 

1607 elif func in base.cythonized_kernels or func in base.transformation_kernels: 

1608 # cythonized transform or canned "agg+broadcast" 

1609 if engine is not None: 

1610 kwargs["engine"] = engine 

1611 kwargs["engine_kwargs"] = engine_kwargs 

1612 return getattr(self, func)(*args, **kwargs) 

1613 

1614 else: 

1615 # i.e. func in base.reduction_kernels 

1616 if self.observed: 

1617 return self._reduction_kernel_transform( 

1618 func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs 

1619 ) 

1620 

1621 with ( 

1622 com.temp_setattr(self, "observed", True), 

1623 com.temp_setattr(self, "_grouper", self._grouper.observed_grouper), 

1624 ): 

1625 return self._reduction_kernel_transform( 

1626 func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs 

1627 ) 

1628 

1629 @final 

1630 def _reduction_kernel_transform( 

1631 self, func, *args, engine=None, engine_kwargs=None, **kwargs 

1632 ): 

1633 # GH#30918 Use _transform_fast only when we know func is an aggregation 

1634 # If func is a reduction, we need to broadcast the 

1635 # result to the whole group. Compute func result 

1636 # and deal with possible broadcasting below. 

1637 with com.temp_setattr(self, "as_index", True): 

1638 # GH#49834 - result needs groups in the index for 

1639 # _wrap_transform_fast_result 

1640 if func in ["idxmin", "idxmax"]: 

1641 func = cast("Literal['idxmin', 'idxmax']", func) 

1642 result = self._idxmax_idxmin(func, True, *args, **kwargs) 

1643 else: 

1644 if engine is not None: 

1645 kwargs["engine"] = engine 

1646 kwargs["engine_kwargs"] = engine_kwargs 

1647 result = getattr(self, func)(*args, **kwargs) 

1648 

1649 return self._wrap_transform_fast_result(result) 

1650 

1651 @final 

1652 def _wrap_transform_fast_result(self, result: NDFrameT) -> NDFrameT: 

1653 """ 

1654 Fast transform path for aggregations. 

1655 """ 

1656 obj = self._obj_with_exclusions 

1657 

1658 # for each col, reshape to size of original frame by take operation 

1659 ids = self._grouper.ids 

1660 result = result.reindex(self._grouper.result_index, axis=0) 

1661 

1662 if self.obj.ndim == 1: 

1663 # i.e. SeriesGroupBy 

1664 out = algorithms.take_nd( 

1665 result._values, ids, allow_fill=self._grouper.has_dropped_na 

1666 ) 

1667 output = obj._constructor(out, index=obj.index, name=obj.name) 

1668 else: 

1669 # `.size()` gives Series output on DataFrame input, need axis 0 

1670 # GH#46209 

1671 # Don't convert indices: negative indices need to give rise 

1672 # to null values in the result 

1673 new_ax = result.index.take(ids) 

1674 output = result._reindex_with_indexers({0: (new_ax, ids)}, allow_dups=True) 

1675 output = output.set_axis(obj.index, axis=0) 

1676 return output 

1677 

1678 # ----------------------------------------------------------------- 

1679 # Utilities 

1680 

1681 @final 

1682 def _apply_filter(self, indices, dropna): 

1683 if len(indices) == 0: 

1684 indices = np.array([], dtype="int64") 

1685 else: 

1686 indices = np.sort(np.concatenate(indices)) 

1687 if dropna: 

1688 filtered = self._selected_obj.take(indices, axis=0) 

1689 else: 

1690 mask = np.empty(len(self._selected_obj.index), dtype=bool) 

1691 mask.fill(False) 

1692 mask[indices.astype(int)] = True 

1693 # mask fails to broadcast when passed to where; broadcast manually. 

1694 mask = np.tile(mask, [*self._selected_obj.shape[1:], 1]).T 

1695 filtered = self._selected_obj.where(mask) # Fill with NaNs. 

1696 return filtered 

1697 

1698 @final 

1699 def _cumcount_array(self, ascending: bool = True) -> np.ndarray: 

1700 """ 

1701 Parameters 

1702 ---------- 

1703 ascending : bool, default True 

1704 If False, number in reverse, from length of group - 1 to 0. 

1705 

1706 Notes 

1707 ----- 

1708 this is currently implementing sort=False 

1709 (though the default is sort=True) for groupby in general 

1710 """ 

1711 ids = self._grouper.ids 

1712 ngroups = self._grouper.ngroups 

1713 sorter = get_group_index_sorter(ids, ngroups) 

1714 ids, count = ids[sorter], len(ids) 

1715 

1716 if count == 0: 

1717 return np.empty(0, dtype=np.int64) 

1718 

1719 run = np.r_[True, ids[:-1] != ids[1:]] 

1720 rep = np.diff(np.r_[np.nonzero(run)[0], count]) 

1721 out = (~run).cumsum() 

1722 

1723 if ascending: 

1724 out -= np.repeat(out[run], rep) 

1725 else: 

1726 out = np.repeat(out[np.r_[run[1:], True]], rep) - out 

1727 

1728 if self._grouper.has_dropped_na: 

1729 out = np.where(ids == -1, np.nan, out.astype(np.float64, copy=False)) 

1730 else: 

1731 out = out.astype(np.int64, copy=False) 

1732 

1733 rev = np.empty(count, dtype=np.intp) 

1734 rev[sorter] = np.arange(count, dtype=np.intp) 

1735 return out[rev] 

1736 

1737 # ----------------------------------------------------------------- 

1738 

1739 @final 

1740 @property 

1741 def _obj_1d_constructor(self) -> Callable: 

1742 # GH28330 preserve subclassed Series/DataFrames 

1743 if isinstance(self.obj, DataFrame): 

1744 return self.obj._constructor_sliced 

1745 assert isinstance(self.obj, Series) 

1746 return self.obj._constructor 

1747 

1748 @final 

1749 def any(self, skipna: bool = True) -> NDFrameT: 

1750 """ 

1751 Return True if any value in the group is truthful, else False. 

1752 

1753 This is equivalent to calling ``bool`` on each value in the group and 

1754 returning ``True`` if at least one is truthful. 

1755 

1756 Parameters 

1757 ---------- 

1758 skipna : bool, default True 

1759 Flag to ignore nan values during truth testing. 

1760 

1761 Returns 

1762 ------- 

1763 Series or DataFrame 

1764 DataFrame or Series of boolean values, where a value is True if any element 

1765 is True within its respective group, False otherwise. 

1766 

1767 See Also 

1768 -------- 

1769 Series.any : Apply function any to a Series. 

1770 DataFrame.any : Apply function any to each row or column of a DataFrame. 

1771 

1772 Examples 

1773 -------- 

1774 For SeriesGroupBy: 

1775 

1776 >>> lst = ["a", "a", "b"] 

1777 >>> ser = pd.Series([1, 2, 0], index=lst) 

1778 >>> ser 

1779 a 1 

1780 a 2 

1781 b 0 

1782 dtype: int64 

1783 >>> ser.groupby(level=0).any() 

1784 a True 

1785 b False 

1786 dtype: bool 

1787 

1788 For DataFrameGroupBy: 

1789 

1790 >>> data = [[1, 0, 3], [1, 0, 6], [7, 1, 9]] 

1791 >>> df = pd.DataFrame( 

1792 ... data, columns=["a", "b", "c"], index=["ostrich", "penguin", "parrot"] 

1793 ... ) 

1794 >>> df 

1795 a b c 

1796 ostrich 1 0 3 

1797 penguin 1 0 6 

1798 parrot 7 1 9 

1799 >>> df.groupby(by=["a"]).any() 

1800 b c 

1801 a 

1802 1 False True 

1803 7 True True 

1804 """ 

1805 return self._cython_agg_general( 

1806 "any", 

1807 alt=lambda x: Series(x, copy=False).any(skipna=skipna), 

1808 skipna=skipna, 

1809 ) 

1810 

1811 @final 

1812 def all(self, skipna: bool = True) -> NDFrameT: 

1813 """ 

1814 Return True if all values in the group are truthful, else False. 

1815 

1816 This is equivalent to calling ``bool`` on each value in the group and 

1817 returning ``True`` only if all are truthful. 

1818 

1819 Parameters 

1820 ---------- 

1821 skipna : bool, default True 

1822 Flag to ignore nan values during truth testing. 

1823 

1824 Returns 

1825 ------- 

1826 Series or DataFrame 

1827 DataFrame or Series of boolean values, where a value is True if all elements 

1828 are True within its respective group, False otherwise. 

1829 

1830 See Also 

1831 -------- 

1832 Series.all : Apply function all to a Series. 

1833 DataFrame.all : Apply function all to each row or column of a DataFrame. 

1834 

1835 Examples 

1836 -------- 

1837 

1838 For SeriesGroupBy: 

1839 

1840 >>> lst = ["a", "a", "b"] 

1841 >>> ser = pd.Series([1, 2, 0], index=lst) 

1842 >>> ser 

1843 a 1 

1844 a 2 

1845 b 0 

1846 dtype: int64 

1847 >>> ser.groupby(level=0).all() 

1848 a True 

1849 b False 

1850 dtype: bool 

1851 

1852 For DataFrameGroupBy: 

1853 

1854 >>> data = [[1, 0, 3], [1, 5, 6], [7, 8, 9]] 

1855 >>> df = pd.DataFrame( 

1856 ... data, columns=["a", "b", "c"], index=["ostrich", "penguin", "parrot"] 

1857 ... ) 

1858 >>> df 

1859 a b c 

1860 ostrich 1 0 3 

1861 penguin 1 5 6 

1862 parrot 7 8 9 

1863 >>> df.groupby(by=["a"]).all() 

1864 b c 

1865 a 

1866 1 False True 

1867 7 True True 

1868 """ 

1869 return self._cython_agg_general( 

1870 "all", 

1871 alt=lambda x: Series(x, copy=False).all(skipna=skipna), 

1872 skipna=skipna, 

1873 ) 

1874 

1875 @final 

1876 def count(self) -> NDFrameT: 

1877 """ 

1878 Compute count of group, excluding missing values. 

1879 

1880 Returns the number of non-NA/null observations per group. 

1881 

1882 Returns 

1883 ------- 

1884 Series or DataFrame 

1885 Count of values within each group. 

1886 

1887 See Also 

1888 -------- 

1889 Series.count : Apply function count to a Series. 

1890 DataFrame.count : Apply function count to each row or column of a DataFrame. 

1891 

1892 Examples 

1893 -------- 

1894 For SeriesGroupBy: 

1895 

1896 >>> lst = ["a", "a", "b"] 

1897 >>> ser = pd.Series([1, 2, np.nan], index=lst) 

1898 >>> ser 

1899 a 1.0 

1900 a 2.0 

1901 b NaN 

1902 dtype: float64 

1903 >>> ser.groupby(level=0).count() 

1904 a 2 

1905 b 0 

1906 dtype: int64 

1907 

1908 For DataFrameGroupBy: 

1909 

1910 >>> data = [[1, np.nan, 3], [1, np.nan, 6], [7, 8, 9]] 

1911 >>> df = pd.DataFrame( 

1912 ... data, columns=["a", "b", "c"], index=["cow", "horse", "bull"] 

1913 ... ) 

1914 >>> df 

1915 a b c 

1916 cow 1 NaN 3 

1917 horse 1 NaN 6 

1918 bull 7 8.0 9 

1919 >>> df.groupby("a").count() 

1920 b c 

1921 a 

1922 1 0 2 

1923 7 1 1 

1924 

1925 For Resampler: 

1926 

1927 >>> ser = pd.Series( 

1928 ... [1, 2, 3, 4], 

1929 ... index=pd.DatetimeIndex( 

1930 ... ["2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15"] 

1931 ... ), 

1932 ... ) 

1933 >>> ser 

1934 2023-01-01 1 

1935 2023-01-15 2 

1936 2023-02-01 3 

1937 2023-02-15 4 

1938 dtype: int64 

1939 >>> ser.resample("MS").count() 

1940 2023-01-01 2 

1941 2023-02-01 2 

1942 Freq: MS, dtype: int64 

1943 """ 

1944 data = self._get_data_to_aggregate() 

1945 ids = self._grouper.ids 

1946 ngroups = self._grouper.ngroups 

1947 mask = ids != -1 

1948 

1949 is_series = data.ndim == 1 

1950 

1951 def hfunc(bvalues: ArrayLike) -> ArrayLike: 

1952 # TODO(EA2D): reshape would not be necessary with 2D EAs 

1953 if bvalues.ndim == 1: 

1954 # EA 

1955 masked = mask & ~isna(bvalues).reshape(1, -1) 

1956 else: 

1957 masked = mask & ~isna(bvalues) 

1958 

1959 counted = lib.count_level_2d(masked, labels=ids, max_bin=ngroups) 

1960 if isinstance(bvalues, BaseMaskedArray): 

1961 return IntegerArray( 

1962 counted[0], mask=np.zeros(counted.shape[1], dtype=np.bool_) 

1963 ) 

1964 elif isinstance(bvalues, ArrowExtensionArray) and not isinstance( 

1965 bvalues.dtype, StringDtype 

1966 ): 

1967 dtype = pandas_dtype("int64[pyarrow]") 

1968 return type(bvalues)._from_sequence(counted[0], dtype=dtype) 

1969 if is_series: 

1970 assert counted.ndim == 2 

1971 assert counted.shape[0] == 1 

1972 return counted[0] 

1973 return counted 

1974 

1975 new_mgr = data.grouped_reduce(hfunc) 

1976 new_obj = self._wrap_agged_manager(new_mgr) 

1977 result = self._wrap_aggregated_output(new_obj) 

1978 

1979 return result 

1980 

1981 @final 

1982 def mean( 

1983 self, 

1984 numeric_only: bool = False, 

1985 skipna: bool = True, 

1986 engine: Literal["cython", "numba"] | None = None, 

1987 engine_kwargs: dict[str, bool] | None = None, 

1988 ): 

1989 """ 

1990 Compute mean of groups, excluding missing values. 

1991 

1992 Returns the arithmetic mean for each group. Missing values are 

1993 ignored unless the entire group is NA, in which case the result 

1994 for that group is NA. 

1995 

1996 Parameters 

1997 ---------- 

1998 numeric_only : bool, default False 

1999 Include only float, int, boolean columns. 

2000 

2001 .. versionchanged:: 2.0.0 

2002 

2003 numeric_only no longer accepts ``None`` and defaults to ``False``. 

2004 

2005 skipna : bool, default True 

2006 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2007 

2008 engine : str, default None 

2009 * ``'cython'`` : Runs the operation through C-extensions from cython. 

2010 * ``'numba'`` : Runs the operation through JIT compiled code from numba. 

2011 * ``None`` : Defaults to ``'cython'`` or globally setting 

2012 ``compute.use_numba`` 

2013 

2014 engine_kwargs : dict, default None 

2015 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

2016 * For ``'numba'`` engine, the engine can accept ``nogil`` 

2017 and ``parallel`` dictionary keys. The values must either be ``True`` or 

2018 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

2019 ``{'nogil': False, 'parallel': False}`` 

2020 

2021 Returns 

2022 ------- 

2023 pandas.Series or pandas.DataFrame 

2024 Mean of values within each group. Same object type as the caller. 

2025 

2026 See Also 

2027 -------- 

2028 Series.mean : Apply function mean to a Series. 

2029 DataFrame.mean : Apply function mean to each row or column of a DataFrame. 

2030 

2031 Examples 

2032 -------- 

2033 >>> df = pd.DataFrame( 

2034 ... {"A": [1, 1, 2, 1, 2], "B": [np.nan, 2, 3, 4, 5], "C": [1, 2, 1, 1, 2]}, 

2035 ... columns=["A", "B", "C"], 

2036 ... ) 

2037 

2038 Groupby one column and return the mean of the remaining columns in 

2039 each group. 

2040 

2041 >>> df.groupby("A").mean() 

2042 B C 

2043 A 

2044 1 3.0 1.333333 

2045 2 4.0 1.500000 

2046 

2047 Groupby two columns and return the mean of the remaining column. 

2048 

2049 >>> df.groupby(["A", "B"]).mean() 

2050 C 

2051 A B 

2052 1 2.0 2.0 

2053 4.0 1.0 

2054 2 3.0 1.0 

2055 5.0 2.0 

2056 

2057 Groupby one column and return the mean of only particular column in 

2058 the group. 

2059 

2060 >>> df.groupby("A")["B"].mean() 

2061 A 

2062 1 3.0 

2063 2 4.0 

2064 Name: B, dtype: float64 

2065 """ 

2066 

2067 if maybe_use_numba(engine): 

2068 from pandas.core._numba.kernels import grouped_mean 

2069 

2070 return self._numba_agg_general( 

2071 grouped_mean, 

2072 executor.float_dtype_mapping, 

2073 engine_kwargs, 

2074 min_periods=0, 

2075 skipna=skipna, 

2076 ) 

2077 else: 

2078 return self._cython_agg_general( 

2079 "mean", 

2080 alt=lambda x: Series(x, copy=False).mean( 

2081 numeric_only=numeric_only, skipna=skipna 

2082 ), 

2083 numeric_only=numeric_only, 

2084 skipna=skipna, 

2085 ) 

2086 

2087 @final 

2088 def median(self, numeric_only: bool = False, skipna: bool = True) -> NDFrameT: 

2089 """ 

2090 Compute median of groups, excluding missing values. 

2091 

2092 For multiple groupings, the result index will be a MultiIndex 

2093 

2094 Parameters 

2095 ---------- 

2096 numeric_only : bool, default False 

2097 Include only float, int, boolean columns. 

2098 

2099 .. versionchanged:: 2.0.0 

2100 

2101 numeric_only no longer accepts ``None`` and defaults to False. 

2102 

2103 skipna : bool, default True 

2104 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2105 

2106 .. versionadded:: 3.0.0 

2107 

2108 Returns 

2109 ------- 

2110 Series or DataFrame 

2111 Median of values within each group. 

2112 

2113 See Also 

2114 -------- 

2115 Series.median : Apply function median to a Series. 

2116 DataFrame.median : Apply function median to each row or column of a DataFrame. 

2117 

2118 Examples 

2119 -------- 

2120 For SeriesGroupBy: 

2121 

2122 >>> lst = ["a", "a", "a", "b", "b", "b"] 

2123 >>> ser = pd.Series([7, 2, 8, 4, 3, 3], index=lst) 

2124 >>> ser 

2125 a 7 

2126 a 2 

2127 a 8 

2128 b 4 

2129 b 3 

2130 b 3 

2131 dtype: int64 

2132 >>> ser.groupby(level=0).median() 

2133 a 7.0 

2134 b 3.0 

2135 dtype: float64 

2136 

2137 For DataFrameGroupBy: 

2138 

2139 >>> data = {"a": [1, 3, 5, 7, 7, 8, 3], "b": [1, 4, 8, 4, 4, 2, 1]} 

2140 >>> df = pd.DataFrame( 

2141 ... data, index=["dog", "dog", "dog", "mouse", "mouse", "mouse", "mouse"] 

2142 ... ) 

2143 >>> df 

2144 a b 

2145 dog 1 1 

2146 dog 3 4 

2147 dog 5 8 

2148 mouse 7 4 

2149 mouse 7 4 

2150 mouse 8 2 

2151 mouse 3 1 

2152 >>> df.groupby(level=0).median() 

2153 a b 

2154 dog 3.0 4.0 

2155 mouse 7.0 3.0 

2156 

2157 For Resampler: 

2158 

2159 >>> ser = pd.Series( 

2160 ... [1, 2, 3, 3, 4, 5], 

2161 ... index=pd.DatetimeIndex( 

2162 ... [ 

2163 ... "2023-01-01", 

2164 ... "2023-01-10", 

2165 ... "2023-01-15", 

2166 ... "2023-02-01", 

2167 ... "2023-02-10", 

2168 ... "2023-02-15", 

2169 ... ] 

2170 ... ), 

2171 ... ) 

2172 >>> ser.resample("MS").median() 

2173 2023-01-01 2.0 

2174 2023-02-01 4.0 

2175 Freq: MS, dtype: float64 

2176 """ 

2177 return self._cython_agg_general( 

2178 "median", 

2179 alt=lambda x: Series(x, copy=False).median( 

2180 numeric_only=numeric_only, skipna=skipna 

2181 ), 

2182 numeric_only=numeric_only, 

2183 skipna=skipna, 

2184 ) 

2185 

2186 @final 

2187 def std( 

2188 self, 

2189 ddof: int = 1, 

2190 engine: Literal["cython", "numba"] | None = None, 

2191 engine_kwargs: dict[str, bool] | None = None, 

2192 numeric_only: bool = False, 

2193 skipna: bool = True, 

2194 ): 

2195 """ 

2196 Compute standard deviation of groups, excluding missing values. 

2197 

2198 For multiple groupings, the result index will be a MultiIndex. 

2199 

2200 Parameters 

2201 ---------- 

2202 ddof : int, default 1 

2203 Delta Degrees of Freedom. The divisor used in calculations is ``N - ddof``, 

2204 where ``N`` represents the number of elements. 

2205 

2206 engine : str, default None 

2207 * ``'cython'`` : Runs the operation through C-extensions from cython. 

2208 * ``'numba'`` : Runs the operation through JIT compiled code from numba. 

2209 * ``None`` : Defaults to ``'cython'`` or globally setting 

2210 ``compute.use_numba`` 

2211 

2212 engine_kwargs : dict, default None 

2213 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

2214 * For ``'numba'`` engine, the engine can accept ``nogil`` 

2215 and ``parallel`` dictionary keys. The values must either be ``True`` or 

2216 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

2217 ``{'nogil': False, 'parallel': False}`` 

2218 

2219 numeric_only : bool, default False 

2220 Include only `float`, `int` or `boolean` data. 

2221 

2222 .. versionchanged:: 2.0.0 

2223 

2224 numeric_only now defaults to ``False``. 

2225 

2226 skipna : bool, default True 

2227 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2228 

2229 .. versionadded:: 3.0.0 

2230 

2231 Returns 

2232 ------- 

2233 Series or DataFrame 

2234 Standard deviation of values within each group. 

2235 

2236 See Also 

2237 -------- 

2238 Series.std : Apply function std to a Series. 

2239 DataFrame.std : Apply function std to each row or column of a DataFrame. 

2240 

2241 Notes 

2242 ----- 

2243 To have the same behaviour as ``numpy.std``, use ``ddof=0`` (instead of 

2244 the default ``ddof=1``) and ``skipna=False``. 

2245 

2246 Examples 

2247 -------- 

2248 For SeriesGroupBy: 

2249 

2250 >>> lst = ["a", "a", "a", "b", "b", "b"] 

2251 >>> ser = pd.Series([7, 2, 8, 4, 3, 3], index=lst) 

2252 >>> ser 

2253 a 7 

2254 a 2 

2255 a 8 

2256 b 4 

2257 b 3 

2258 b 3 

2259 dtype: int64 

2260 >>> ser.groupby(level=0).std() 

2261 a 3.21455 

2262 b 0.57735 

2263 dtype: float64 

2264 

2265 For DataFrameGroupBy: 

2266 

2267 >>> data = {"a": [1, 3, 5, 7, 7, 8, 3], "b": [1, 4, 8, 4, 4, 2, 1]} 

2268 >>> df = pd.DataFrame( 

2269 ... data, index=["dog", "dog", "dog", "mouse", "mouse", "mouse", "mouse"] 

2270 ... ) 

2271 >>> df 

2272 a b 

2273 dog 1 1 

2274 dog 3 4 

2275 dog 5 8 

2276 mouse 7 4 

2277 mouse 7 4 

2278 mouse 8 2 

2279 mouse 3 1 

2280 >>> df.groupby(level=0).std() 

2281 a b 

2282 dog 2.000000 3.511885 

2283 mouse 2.217356 1.500000 

2284 """ 

2285 if maybe_use_numba(engine): 

2286 from pandas.core._numba.kernels import grouped_var 

2287 

2288 return np.sqrt( 

2289 self._numba_agg_general( 

2290 grouped_var, 

2291 executor.float_dtype_mapping, 

2292 engine_kwargs, 

2293 min_periods=0, 

2294 ddof=ddof, 

2295 skipna=skipna, 

2296 ) 

2297 ) 

2298 else: 

2299 return self._cython_agg_general( 

2300 "std", 

2301 alt=lambda x: Series(x, copy=False).std(ddof=ddof, skipna=skipna), 

2302 numeric_only=numeric_only, 

2303 ddof=ddof, 

2304 skipna=skipna, 

2305 ) 

2306 

2307 @final 

2308 def var( 

2309 self, 

2310 ddof: int = 1, 

2311 engine: Literal["cython", "numba"] | None = None, 

2312 engine_kwargs: dict[str, bool] | None = None, 

2313 numeric_only: bool = False, 

2314 skipna: bool = True, 

2315 ): 

2316 """ 

2317 Compute variance of groups, excluding missing values. 

2318 

2319 For multiple groupings, the result index will be a MultiIndex. 

2320 

2321 Parameters 

2322 ---------- 

2323 ddof : int, default 1 

2324 Degrees of freedom. 

2325 

2326 engine : str, default None 

2327 * ``'cython'`` : Runs the operation through C-extensions from cython. 

2328 * ``'numba'`` : Runs the operation through JIT compiled code from numba. 

2329 * ``None`` : Defaults to ``'cython'`` or globally setting 

2330 ``compute.use_numba`` 

2331 

2332 engine_kwargs : dict, default None 

2333 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

2334 * For ``'numba'`` engine, the engine can accept ``nogil`` 

2335 and ``parallel`` dictionary keys. The values must either be ``True`` or 

2336 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

2337 ``{'nogil': False, 'parallel': False}`` 

2338 

2339 numeric_only : bool, default False 

2340 Include only `float`, `int` or `boolean` data. 

2341 

2342 .. versionchanged:: 2.0.0 

2343 

2344 numeric_only now defaults to ``False``. 

2345 

2346 skipna : bool, default True 

2347 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2348 

2349 .. versionadded:: 3.0.0 

2350 

2351 Returns 

2352 ------- 

2353 Series or DataFrame 

2354 Variance of values within each group. 

2355 

2356 See Also 

2357 -------- 

2358 Series.var : Apply function var to a Series. 

2359 DataFrame.var : Apply function var to each row or column of a DataFrame. 

2360 

2361 Examples 

2362 -------- 

2363 For SeriesGroupBy: 

2364 

2365 >>> lst = ["a", "a", "a", "b", "b", "b"] 

2366 >>> ser = pd.Series([7, 2, 8, 4, 3, 3], index=lst) 

2367 >>> ser 

2368 a 7 

2369 a 2 

2370 a 8 

2371 b 4 

2372 b 3 

2373 b 3 

2374 dtype: int64 

2375 >>> ser.groupby(level=0).var() 

2376 a 10.333333 

2377 b 0.333333 

2378 dtype: float64 

2379 

2380 For DataFrameGroupBy: 

2381 

2382 >>> data = {"a": [1, 3, 5, 7, 7, 8, 3], "b": [1, 4, 8, 4, 4, 2, 1]} 

2383 >>> df = pd.DataFrame( 

2384 ... data, index=["dog", "dog", "dog", "mouse", "mouse", "mouse", "mouse"] 

2385 ... ) 

2386 >>> df 

2387 a b 

2388 dog 1 1 

2389 dog 3 4 

2390 dog 5 8 

2391 mouse 7 4 

2392 mouse 7 4 

2393 mouse 8 2 

2394 mouse 3 1 

2395 >>> df.groupby(level=0).var() 

2396 a b 

2397 dog 4.000000 12.333333 

2398 mouse 4.916667 2.250000 

2399 """ 

2400 if maybe_use_numba(engine): 

2401 from pandas.core._numba.kernels import grouped_var 

2402 

2403 return self._numba_agg_general( 

2404 grouped_var, 

2405 executor.float_dtype_mapping, 

2406 engine_kwargs, 

2407 min_periods=0, 

2408 ddof=ddof, 

2409 skipna=skipna, 

2410 ) 

2411 else: 

2412 return self._cython_agg_general( 

2413 "var", 

2414 alt=lambda x: Series(x, copy=False).var(ddof=ddof, skipna=skipna), 

2415 numeric_only=numeric_only, 

2416 ddof=ddof, 

2417 skipna=skipna, 

2418 ) 

2419 

2420 @final 

2421 def _value_counts( 

2422 self, 

2423 subset: Sequence[Hashable] | None = None, 

2424 normalize: bool = False, 

2425 sort: bool = True, 

2426 ascending: bool = False, 

2427 dropna: bool = True, 

2428 ) -> DataFrame | Series: 

2429 """ 

2430 Shared implementation of value_counts for SeriesGroupBy and DataFrameGroupBy. 

2431 

2432 SeriesGroupBy additionally supports a bins argument. See the docstring of 

2433 DataFrameGroupBy.value_counts for a description of arguments. 

2434 """ 

2435 name = "proportion" if normalize else "count" 

2436 

2437 df = self.obj 

2438 obj = self._obj_with_exclusions 

2439 

2440 in_axis_names = { 

2441 grouping.name for grouping in self._grouper.groupings if grouping.in_axis 

2442 } 

2443 if isinstance(obj, Series): 

2444 _name = obj.name 

2445 keys: Iterable[Series] = [] if _name in in_axis_names else [obj] 

2446 else: 

2447 unique_cols = set(obj.columns) 

2448 if subset is not None: 

2449 subsetted = set(subset) 

2450 clashing = subsetted & set(in_axis_names) 

2451 if clashing: 

2452 raise ValueError( 

2453 f"Keys {clashing} in subset cannot be in " 

2454 "the groupby column keys." 

2455 ) 

2456 doesnt_exist = subsetted - unique_cols 

2457 if doesnt_exist: 

2458 raise ValueError( 

2459 f"Keys {doesnt_exist} in subset do not exist in the DataFrame." 

2460 ) 

2461 else: 

2462 subsetted = unique_cols 

2463 

2464 keys = ( 

2465 # Can't use .values because the column label needs to be preserved 

2466 obj.iloc[:, idx] 

2467 for idx, _name in enumerate(obj.columns) 

2468 if _name not in in_axis_names and _name in subsetted 

2469 ) 

2470 

2471 groupings = list(self._grouper.groupings) 

2472 for key in keys: 

2473 grouper, _, _ = get_grouper( 

2474 df, 

2475 key=key, 

2476 sort=False, 

2477 observed=False, 

2478 dropna=dropna, 

2479 ) 

2480 groupings += list(grouper.groupings) 

2481 

2482 # Take the size of the overall columns 

2483 gb = df.groupby( 

2484 groupings, 

2485 sort=False, 

2486 observed=self.observed, 

2487 dropna=self.dropna, 

2488 ) 

2489 result_series = cast("Series", gb.size()) 

2490 result_series.name = name 

2491 

2492 if sort: 

2493 # Sort by the values 

2494 result_series = result_series.sort_values( 

2495 ascending=ascending, kind="stable" 

2496 ) 

2497 if self.sort: 

2498 # Sort by the groupings 

2499 names = result_series.index.names 

2500 # GH#55951 - Temporarily replace names in case they are integers 

2501 result_series.index.names = range(len(names)) 

2502 index_level = range(len(self._grouper.groupings)) 

2503 result_series = result_series.sort_index( 

2504 level=index_level, sort_remaining=False 

2505 ) 

2506 result_series.index.names = names 

2507 

2508 if normalize: 

2509 # Normalize the results by dividing by the original group sizes. 

2510 # We are guaranteed to have the first N levels be the 

2511 # user-requested grouping. 

2512 levels = list( 

2513 range(len(self._grouper.groupings), result_series.index.nlevels) 

2514 ) 

2515 indexed_group_size = result_series.groupby( 

2516 result_series.index.droplevel(levels), 

2517 sort=self.sort, 

2518 dropna=self.dropna, 

2519 # GH#43999 - deprecation of observed=False 

2520 observed=False, 

2521 ).transform("sum") 

2522 result_series /= indexed_group_size 

2523 

2524 # Handle groups of non-observed categories 

2525 result_series = result_series.fillna(0.0) 

2526 

2527 result: Series | DataFrame 

2528 if self.as_index: 

2529 result = result_series 

2530 else: 

2531 # Convert to frame 

2532 index = result_series.index 

2533 columns = com.fill_missing_names(index.names) 

2534 if name in columns: 

2535 raise ValueError(f"Column label '{name}' is duplicate of result column") 

2536 result_series.name = name 

2537 result_series.index = index.set_names(range(len(columns))) 

2538 result_frame = result_series.reset_index() 

2539 orig_dtype = self._grouper.groupings[0].obj.columns.dtype # type: ignore[union-attr] 

2540 cols = Index(columns, dtype=orig_dtype).insert(len(columns), name) 

2541 result_frame.columns = cols 

2542 result = result_frame 

2543 return result.__finalize__(self.obj, method="value_counts") 

2544 

2545 @final 

2546 def sem( 

2547 self, ddof: int = 1, numeric_only: bool = False, skipna: bool = True 

2548 ) -> NDFrameT: 

2549 """ 

2550 Compute standard error of the mean of groups, excluding missing values. 

2551 

2552 For multiple groupings, the result index will be a MultiIndex. 

2553 

2554 Parameters 

2555 ---------- 

2556 ddof : int, default 1 

2557 Degrees of freedom. 

2558 

2559 numeric_only : bool, default False 

2560 Include only `float`, `int` or `boolean` data. 

2561 

2562 .. versionchanged:: 2.0.0 

2563 

2564 numeric_only now defaults to ``False``. 

2565 

2566 skipna : bool, default True 

2567 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2568 

2569 .. versionadded:: 3.0.0 

2570 

2571 Returns 

2572 ------- 

2573 Series or DataFrame 

2574 Standard error of the mean of values within each group. 

2575 

2576 See Also 

2577 -------- 

2578 DataFrame.sem : Return unbiased standard error of the mean over requested axis. 

2579 Series.sem : Return unbiased standard error of the mean over requested axis. 

2580 

2581 Examples 

2582 -------- 

2583 For SeriesGroupBy: 

2584 

2585 >>> lst = ["a", "a", "b", "b"] 

2586 >>> ser = pd.Series([5, 10, 8, 14], index=lst) 

2587 >>> ser 

2588 a 5 

2589 a 10 

2590 b 8 

2591 b 14 

2592 dtype: int64 

2593 >>> ser.groupby(level=0).sem() 

2594 a 2.5 

2595 b 3.0 

2596 dtype: float64 

2597 

2598 For DataFrameGroupBy: 

2599 

2600 >>> data = [[1, 12, 11], [1, 15, 2], [2, 5, 8], [2, 6, 12]] 

2601 >>> df = pd.DataFrame( 

2602 ... data, 

2603 ... columns=["a", "b", "c"], 

2604 ... index=["tuna", "salmon", "catfish", "goldfish"], 

2605 ... ) 

2606 >>> df 

2607 a b c 

2608 tuna 1 12 11 

2609 salmon 1 15 2 

2610 catfish 2 5 8 

2611 goldfish 2 6 12 

2612 >>> df.groupby("a").sem() 

2613 b c 

2614 a 

2615 1 1.5 4.5 

2616 2 0.5 2.0 

2617 

2618 For Resampler: 

2619 

2620 >>> ser = pd.Series( 

2621 ... [1, 3, 2, 4, 3, 8], 

2622 ... index=pd.DatetimeIndex( 

2623 ... [ 

2624 ... "2023-01-01", 

2625 ... "2023-01-10", 

2626 ... "2023-01-15", 

2627 ... "2023-02-01", 

2628 ... "2023-02-10", 

2629 ... "2023-02-15", 

2630 ... ] 

2631 ... ), 

2632 ... ) 

2633 >>> ser.resample("MS").sem() 

2634 2023-01-01 0.577350 

2635 2023-02-01 1.527525 

2636 Freq: MS, dtype: float64 

2637 """ 

2638 if numeric_only and self.obj.ndim == 1 and not is_numeric_dtype(self.obj.dtype): 

2639 raise TypeError( 

2640 f"{type(self).__name__}.sem called with " 

2641 f"numeric_only={numeric_only} and dtype {self.obj.dtype}" 

2642 ) 

2643 return self._cython_agg_general( 

2644 "sem", 

2645 alt=lambda x: Series(x, copy=False).sem(ddof=ddof, skipna=skipna), 

2646 numeric_only=numeric_only, 

2647 ddof=ddof, 

2648 skipna=skipna, 

2649 ) 

2650 

2651 @final 

2652 def size(self) -> DataFrame | Series: 

2653 """ 

2654 Compute group sizes. 

2655 

2656 Returns the number of rows in each group. This is useful for 

2657 understanding the distribution of data across groups. 

2658 

2659 Returns 

2660 ------- 

2661 DataFrame or Series 

2662 Number of rows in each group as a Series if as_index is True 

2663 or a DataFrame if as_index is False. 

2664 

2665 See Also 

2666 -------- 

2667 Series.size : Apply function size to a Series. 

2668 DataFrame.size : Apply function size to each row or column of a DataFrame. 

2669 

2670 Examples 

2671 -------- 

2672 

2673 For SeriesGroupBy: 

2674 

2675 >>> lst = ["a", "a", "b"] 

2676 >>> ser = pd.Series([1, 2, 3], index=lst) 

2677 >>> ser 

2678 a 1 

2679 a 2 

2680 b 3 

2681 dtype: int64 

2682 >>> ser.groupby(level=0).size() 

2683 a 2 

2684 b 1 

2685 dtype: int64 

2686 

2687 >>> data = [[1, 2, 3], [1, 5, 6], [7, 8, 9]] 

2688 >>> df = pd.DataFrame( 

2689 ... data, columns=["a", "b", "c"], index=["owl", "toucan", "eagle"] 

2690 ... ) 

2691 >>> df 

2692 a b c 

2693 owl 1 2 3 

2694 toucan 1 5 6 

2695 eagle 7 8 9 

2696 >>> df.groupby("a").size() 

2697 a 

2698 1 2 

2699 7 1 

2700 dtype: int64 

2701 

2702 For Resampler: 

2703 

2704 >>> ser = pd.Series( 

2705 ... [1, 2, 3], 

2706 ... index=pd.DatetimeIndex(["2023-01-01", "2023-01-15", "2023-02-01"]), 

2707 ... ) 

2708 >>> ser 

2709 2023-01-01 1 

2710 2023-01-15 2 

2711 2023-02-01 3 

2712 dtype: int64 

2713 >>> ser.resample("MS").size() 

2714 2023-01-01 2 

2715 2023-02-01 1 

2716 Freq: MS, dtype: int64 

2717 """ 

2718 result = self._grouper.size() 

2719 dtype_backend: Literal["pyarrow", "numpy_nullable"] | None = None 

2720 if isinstance(self.obj, Series): 

2721 if isinstance(self.obj.array, ArrowExtensionArray): 

2722 if isinstance(self.obj.array, ArrowStringArray): 

2723 if self.obj.array.dtype.na_value is np.nan: 

2724 dtype_backend = None 

2725 else: 

2726 dtype_backend = "numpy_nullable" 

2727 else: 

2728 dtype_backend = "pyarrow" 

2729 elif isinstance(self.obj.array, BaseMaskedArray): 

2730 dtype_backend = "numpy_nullable" 

2731 # TODO: For DataFrames what if columns are mixed arrow/numpy/masked? 

2732 

2733 # GH28330 preserve subclassed Series/DataFrames through calls 

2734 if isinstance(self.obj, Series): 

2735 result = self._obj_1d_constructor(result, name=self.obj.name) 

2736 else: 

2737 result = self._obj_1d_constructor(result) 

2738 

2739 if dtype_backend is not None: 

2740 result = result.convert_dtypes(dtype_backend=dtype_backend) 

2741 

2742 if not self.as_index: 

2743 result = result.rename("size").reset_index() 

2744 return result 

2745 

2746 @final 

2747 def sum( 

2748 self, 

2749 numeric_only: bool = False, 

2750 min_count: int = 0, 

2751 skipna: bool = True, 

2752 engine: Literal["cython", "numba"] | None = None, 

2753 engine_kwargs: dict[str, bool] | None = None, 

2754 ): 

2755 """ 

2756 Compute sum of group values. 

2757 

2758 Computes the sum for each group. Null values are excluded by 

2759 default unless ``skipna`` is set to ``False``. 

2760 

2761 Parameters 

2762 ---------- 

2763 numeric_only : bool, default False 

2764 Include only float, int, boolean columns. 

2765 

2766 .. versionchanged:: 2.0.0 

2767 

2768 numeric_only no longer accepts ``None``. 

2769 

2770 min_count : int, default 0 

2771 The required number of valid values to perform the operation. If fewer 

2772 than ``min_count`` non-NA values are present the result will be NA. 

2773 

2774 skipna : bool, default True 

2775 Exclude NA/null values. If the entire group is NA and ``skipna`` is 

2776 ``True``, the result will be NA. 

2777 

2778 .. versionchanged:: 3.0.0 

2779 

2780 engine : str, default None None 

2781 * ``'cython'`` : Runs rolling apply through C-extensions from cython. 

2782 * ``'numba'`` : Runs rolling apply through JIT compiled code from numba. 

2783 Only available when ``raw`` is set to ``True``. 

2784 * ``None`` : Defaults to ``'cython'`` or globally setting 

2785 ``compute.use_numba`` 

2786 

2787 engine_kwargs : dict, default None None 

2788 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

2789 * For ``'numba'`` engine, the engine can accept ``nogil`` 

2790 and ``parallel`` dictionary keys. The values must either be ``True`` or 

2791 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

2792 ``{'nogil': False, 'parallel': False}`` and will be 

2793 applied to both the ``func`` and the ``apply`` groupby aggregation. 

2794 

2795 Returns 

2796 ------- 

2797 Series or DataFrame 

2798 Computed sum of values within each group. 

2799 

2800 See Also 

2801 -------- 

2802 Series.sum : Return the sum of the values over the requested axis. 

2803 DataFrame.sum : Return the sum of the values over the requested axis. 

2804 

2805 Examples 

2806 -------- 

2807 For SeriesGroupBy: 

2808 

2809 >>> lst = ["a", "a", "b", "b"] 

2810 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

2811 >>> ser 

2812 a 1 

2813 a 2 

2814 b 3 

2815 b 4 

2816 dtype: int64 

2817 >>> ser.groupby(level=0).sum() 

2818 a 3 

2819 b 7 

2820 dtype: int64 

2821 

2822 For DataFrameGroupBy: 

2823 

2824 >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] 

2825 >>> df = pd.DataFrame( 

2826 ... data, 

2827 ... columns=["a", "b", "c"], 

2828 ... index=["tiger", "leopard", "cheetah", "lion"], 

2829 ... ) 

2830 >>> df 

2831 a b c 

2832 tiger 1 8 2 

2833 leopard 1 2 5 

2834 cheetah 2 5 8 

2835 lion 2 6 9 

2836 >>> df.groupby("a").sum() 

2837 b c 

2838 a 

2839 1 10 7 

2840 2 11 17 

2841 """ 

2842 if maybe_use_numba(engine): 

2843 from pandas.core._numba.kernels import grouped_sum 

2844 

2845 return self._numba_agg_general( 

2846 grouped_sum, 

2847 executor.default_dtype_mapping, 

2848 engine_kwargs, 

2849 min_periods=min_count, 

2850 skipna=skipna, 

2851 ) 

2852 else: 

2853 

2854 def sum_compat(obj: NDFrameT): 

2855 # GH#18588: see min_compat below 

2856 return obj.sum(skipna=skipna) 

2857 

2858 return self._cython_agg_general( 

2859 "sum", 

2860 alt=sum_compat, 

2861 numeric_only=numeric_only, 

2862 min_count=min_count, 

2863 skipna=skipna, 

2864 ) 

2865 

2866 @final 

2867 def prod( 

2868 self, numeric_only: bool = False, min_count: int = 0, skipna: bool = True 

2869 ) -> NDFrameT: 

2870 """ 

2871 Compute prod of group values. 

2872 

2873 This method computes the product of all values within each group, 

2874 returning a result for each group. 

2875 

2876 Parameters 

2877 ---------- 

2878 numeric_only : bool, default False 

2879 Include only float, int, boolean columns. 

2880 

2881 .. versionchanged:: 2.0.0 

2882 

2883 numeric_only no longer accepts ``None``. 

2884 

2885 min_count : int, default 0 

2886 The required number of valid values to perform the operation. If fewer 

2887 than ``min_count`` non-NA values are present the result will be NA. 

2888 

2889 skipna : bool, default True 

2890 Exclude NA/null values. If an entire group is NA, the result will be NA. 

2891 

2892 .. versionadded:: 3.0.0 

2893 

2894 Returns 

2895 ------- 

2896 Series or DataFrame 

2897 Computed prod of values within each group. 

2898 

2899 See Also 

2900 -------- 

2901 Series.prod : Return the product of the values over the requested axis. 

2902 DataFrame.prod : Return the product of the values over the requested axis. 

2903 

2904 Examples 

2905 -------- 

2906 For SeriesGroupBy: 

2907 

2908 >>> lst = ["a", "a", "b", "b"] 

2909 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

2910 >>> ser 

2911 a 1 

2912 a 2 

2913 b 3 

2914 b 4 

2915 dtype: int64 

2916 >>> ser.groupby(level=0).prod() 

2917 a 2 

2918 b 12 

2919 dtype: int64 

2920 

2921 For DataFrameGroupBy: 

2922 

2923 >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] 

2924 >>> df = pd.DataFrame( 

2925 ... data, 

2926 ... columns=["a", "b", "c"], 

2927 ... index=["tiger", "leopard", "cheetah", "lion"], 

2928 ... ) 

2929 >>> df 

2930 a b c 

2931 tiger 1 8 2 

2932 leopard 1 2 5 

2933 cheetah 2 5 8 

2934 lion 2 6 9 

2935 >>> df.groupby("a").prod() 

2936 b c 

2937 a 

2938 1 16 10 

2939 2 30 72 

2940 """ 

2941 

2942 def prod_compat(obj: NDFrameT): 

2943 # GH#18588: see min_compat below 

2944 return obj.prod(skipna=skipna) 

2945 

2946 return self._cython_agg_general( 

2947 "prod", 

2948 alt=prod_compat, 

2949 numeric_only=numeric_only, 

2950 min_count=min_count, 

2951 skipna=skipna, 

2952 ) 

2953 

2954 @final 

2955 def min( 

2956 self, 

2957 numeric_only: bool = False, 

2958 min_count: int = -1, 

2959 skipna: bool = True, 

2960 engine: Literal["cython", "numba"] | None = None, 

2961 engine_kwargs: dict[str, bool] | None = None, 

2962 ): 

2963 """ 

2964 Compute min of group values. 

2965 

2966 Returns the minimum value for each group. Missing values are 

2967 excluded by default but this behavior can be controlled with 

2968 the ``skipna`` parameter. 

2969 

2970 Parameters 

2971 ---------- 

2972 numeric_only : bool, default False 

2973 Include only float, int, boolean columns. 

2974 

2975 .. versionchanged:: 2.0.0 

2976 

2977 numeric_only no longer accepts ``None``. 

2978 

2979 min_count : int, default -1 

2980 The required number of valid values to perform the operation. If fewer 

2981 than ``min_count`` non-NA values are present the result will be NA. 

2982 

2983 skipna : bool, default True 

2984 Exclude NA/null values. If the entire group is NA and ``skipna`` is 

2985 ``True``, the result will be NA. 

2986 

2987 .. versionchanged:: 3.0.0 

2988 

2989 engine : str, default None None 

2990 * ``'cython'`` : Runs rolling apply through C-extensions from cython. 

2991 * ``'numba'`` : Runs rolling apply through JIT compiled code from numba. 

2992 Only available when ``raw`` is set to ``True``. 

2993 * ``None`` : Defaults to ``'cython'`` or globally setting 

2994 ``compute.use_numba`` 

2995 

2996 engine_kwargs : dict, default None None 

2997 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

2998 * For ``'numba'`` engine, the engine can accept ``nogil`` 

2999 and ``parallel`` dictionary keys. The values must either be ``True`` or 

3000 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

3001 ``{'nogil': False, 'parallel': False}`` and will be 

3002 applied to both the ``func`` and the ``apply`` groupby aggregation. 

3003 

3004 Returns 

3005 ------- 

3006 Series or DataFrame 

3007 Computed min of values within each group. 

3008 

3009 See Also 

3010 -------- 

3011 Series.min : Return the minimum of the values over the requested axis. 

3012 DataFrame.min : Return the minimum of the values over the requested axis. 

3013 

3014 Examples 

3015 -------- 

3016 For SeriesGroupBy: 

3017 

3018 >>> lst = ["a", "a", "b", "b"] 

3019 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

3020 >>> ser 

3021 a 1 

3022 a 2 

3023 b 3 

3024 b 4 

3025 dtype: int64 

3026 >>> ser.groupby(level=0).min() 

3027 a 1 

3028 b 3 

3029 dtype: int64 

3030 

3031 For DataFrameGroupBy: 

3032 

3033 >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] 

3034 >>> df = pd.DataFrame( 

3035 ... data, 

3036 ... columns=["a", "b", "c"], 

3037 ... index=["tiger", "leopard", "cheetah", "lion"], 

3038 ... ) 

3039 >>> df 

3040 a b c 

3041 tiger 1 8 2 

3042 leopard 1 2 5 

3043 cheetah 2 5 8 

3044 lion 2 6 9 

3045 >>> df.groupby("a").min() 

3046 b c 

3047 a 

3048 1 2 2 

3049 2 5 8 

3050 """ 

3051 if maybe_use_numba(engine): 

3052 from pandas.core._numba.kernels import grouped_min_max 

3053 

3054 return self._numba_agg_general( 

3055 grouped_min_max, 

3056 executor.identity_dtype_mapping, 

3057 engine_kwargs, 

3058 min_periods=min_count, 

3059 is_max=False, 

3060 skipna=skipna, 

3061 ) 

3062 else: 

3063 

3064 def min_compat(obj: NDFrameT): 

3065 # GH#18588: object/string dtypes have no cython group_min_max 

3066 # and reduce through this alt instead, so it has to apply 

3067 # skipna itself; np.min would always skip. 

3068 return obj.min(skipna=skipna) 

3069 

3070 return self._cython_agg_general( 

3071 "min", 

3072 alt=min_compat, 

3073 numeric_only=numeric_only, 

3074 min_count=min_count, 

3075 skipna=skipna, 

3076 ) 

3077 

3078 @final 

3079 def max( 

3080 self, 

3081 numeric_only: bool = False, 

3082 min_count: int = -1, 

3083 skipna: bool = True, 

3084 engine: Literal["cython", "numba"] | None = None, 

3085 engine_kwargs: dict[str, bool] | None = None, 

3086 ): 

3087 """ 

3088 Compute max of group values. 

3089 

3090 Returns the maximum value for each group. Missing values are 

3091 excluded by default but this behavior can be controlled with 

3092 the ``skipna`` parameter. 

3093 

3094 Parameters 

3095 ---------- 

3096 numeric_only : bool, default False 

3097 Include only float, int, boolean columns. 

3098 

3099 .. versionchanged:: 2.0.0 

3100 

3101 numeric_only no longer accepts ``None``. 

3102 

3103 min_count : int, default -1 

3104 The required number of valid values to perform the operation. If fewer 

3105 than ``min_count`` non-NA values are present the result will be NA. 

3106 

3107 skipna : bool, default True 

3108 Exclude NA/null values. If the entire group is NA and ``skipna`` is 

3109 ``True``, the result will be NA. 

3110 

3111 .. versionchanged:: 3.0.0 

3112 

3113 engine : str, default None None 

3114 * ``'cython'`` : Runs rolling apply through C-extensions from cython. 

3115 * ``'numba'`` : Runs rolling apply through JIT compiled code from numba. 

3116 Only available when ``raw`` is set to ``True``. 

3117 * ``None`` : Defaults to ``'cython'`` or globally setting 

3118 ``compute.use_numba`` 

3119 

3120 engine_kwargs : dict, default None None 

3121 * For ``'cython'`` engine, there are no accepted ``engine_kwargs`` 

3122 * For ``'numba'`` engine, the engine can accept ``nogil`` 

3123 and ``parallel`` dictionary keys. The values must either be ``True`` or 

3124 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is 

3125 ``{'nogil': False, 'parallel': False}`` and will be 

3126 applied to both the ``func`` and the ``apply`` groupby aggregation. 

3127 

3128 Returns 

3129 ------- 

3130 Series or DataFrame 

3131 Computed max of values within each group. 

3132 

3133 See Also 

3134 -------- 

3135 Series.max : Return the maximum of the values over the requested axis. 

3136 DataFrame.max : Return the maximum of the values over the requested axis. 

3137 

3138 Examples 

3139 -------- 

3140 For SeriesGroupBy: 

3141 

3142 >>> lst = ["a", "a", "b", "b"] 

3143 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

3144 >>> ser 

3145 a 1 

3146 a 2 

3147 b 3 

3148 b 4 

3149 dtype: int64 

3150 >>> ser.groupby(level=0).max() 

3151 a 2 

3152 b 4 

3153 dtype: int64 

3154 

3155 For DataFrameGroupBy: 

3156 

3157 >>> data = [[1, 8, 2], [1, 2, 5], [2, 5, 8], [2, 6, 9]] 

3158 >>> df = pd.DataFrame( 

3159 ... data, 

3160 ... columns=["a", "b", "c"], 

3161 ... index=["tiger", "leopard", "cheetah", "lion"], 

3162 ... ) 

3163 >>> df 

3164 a b c 

3165 tiger 1 8 2 

3166 leopard 1 2 5 

3167 cheetah 2 5 8 

3168 lion 2 6 9 

3169 >>> df.groupby("a").max() 

3170 b c 

3171 a 

3172 1 8 5 

3173 2 6 9 

3174 """ 

3175 if maybe_use_numba(engine): 

3176 from pandas.core._numba.kernels import grouped_min_max 

3177 

3178 return self._numba_agg_general( 

3179 grouped_min_max, 

3180 executor.identity_dtype_mapping, 

3181 engine_kwargs, 

3182 min_periods=min_count, 

3183 is_max=True, 

3184 skipna=skipna, 

3185 ) 

3186 else: 

3187 

3188 def max_compat(obj: NDFrameT): 

3189 # GH#18588: see min_compat above 

3190 return obj.max(skipna=skipna) 

3191 

3192 return self._cython_agg_general( 

3193 "max", 

3194 alt=max_compat, 

3195 numeric_only=numeric_only, 

3196 min_count=min_count, 

3197 skipna=skipna, 

3198 ) 

3199 

3200 @final 

3201 def first( 

3202 self, numeric_only: bool = False, min_count: int = -1, skipna: bool = True 

3203 ) -> NDFrameT: 

3204 """ 

3205 Compute the first entry of each column within each group. 

3206 

3207 NA values are skipped by default, so the result is the first non-NA 

3208 value per column — which may differ from the first row of the group. 

3209 Pass ``skipna=False`` to keep NAs. 

3210 

3211 Parameters 

3212 ---------- 

3213 numeric_only : bool, default False 

3214 Include only float, int, boolean columns. 

3215 min_count : int, default -1 

3216 The required number of valid values to perform the operation. If fewer 

3217 than ``min_count`` valid values are present the result will be NA. 

3218 skipna : bool, default True 

3219 Exclude NA values. If an entire group is NA, the result will be NA. 

3220 

3221 .. versionadded:: 2.2.1 

3222 

3223 Returns 

3224 ------- 

3225 Series or DataFrame 

3226 First values within each group. 

3227 

3228 See Also 

3229 -------- 

3230 DataFrame.groupby : Apply a function groupby to each row or column of a 

3231 DataFrame. 

3232 api.typing.DataFrameGroupBy.last : Compute the last entry of each 

3233 column within each group. 

3234 api.typing.DataFrameGroupBy.nth : Take the nth row from each group. 

3235 

3236 Examples 

3237 -------- 

3238 >>> df = pd.DataFrame( 

3239 ... dict( 

3240 ... A=[1, 1, 3], 

3241 ... B=[None, 5, 6], 

3242 ... C=[1, 2, 3], 

3243 ... D=["3/11/2000", "3/12/2000", "3/13/2000"], 

3244 ... ) 

3245 ... ) 

3246 >>> df["D"] = pd.to_datetime(df["D"]) 

3247 >>> df.groupby("A").first() 

3248 B C D 

3249 A 

3250 1 5.0 1 2000-03-11 

3251 3 6.0 3 2000-03-13 

3252 >>> df.groupby("A").first(min_count=2) 

3253 B C D 

3254 A 

3255 1 NaN 1.0 2000-03-11 

3256 3 NaN NaN NaT 

3257 >>> df.groupby("A").first(numeric_only=True) 

3258 B C 

3259 A 

3260 1 5.0 1 

3261 3 6.0 3 

3262 """ 

3263 

3264 def first_compat(obj: NDFrameT): 

3265 def first(x: Series): 

3266 """Helper function for first item that isn't NA.""" 

3267 arr = x.array[notna(x.array)] 

3268 if not len(arr): 

3269 return x.array.dtype.na_value 

3270 return arr[0] 

3271 

3272 if isinstance(obj, DataFrame): 

3273 return obj.apply(first) 

3274 elif isinstance(obj, Series): 

3275 return first(obj) 

3276 else: # pragma: no cover 

3277 raise TypeError(type(obj)) 

3278 

3279 return self._cython_agg_general( 

3280 "first", 

3281 alt=first_compat, 

3282 numeric_only=numeric_only, 

3283 min_count=min_count, 

3284 skipna=skipna, 

3285 ) 

3286 

3287 @final 

3288 def last( 

3289 self, numeric_only: bool = False, min_count: int = -1, skipna: bool = True 

3290 ) -> NDFrameT: 

3291 """ 

3292 Compute the last entry of each column within each group. 

3293 

3294 NA values are skipped by default, so the result is the last non-NA 

3295 value per column — which may differ from the last row of the group. 

3296 Pass ``skipna=False`` to keep NAs. 

3297 

3298 Parameters 

3299 ---------- 

3300 numeric_only : bool, default False 

3301 Include only float, int, boolean columns. If None, will attempt to use 

3302 everything, then use only numeric data. 

3303 min_count : int, default -1 

3304 The required number of valid values to perform the operation. If fewer 

3305 than ``min_count`` valid values are present the result will be NA. 

3306 skipna : bool, default True 

3307 Exclude NA values. If an entire group is NA, the result will be NA. 

3308 

3309 .. versionadded:: 2.2.1 

3310 

3311 Returns 

3312 ------- 

3313 Series or DataFrame 

3314 Last values within each group. 

3315 

3316 See Also 

3317 -------- 

3318 DataFrame.groupby : Apply a function groupby to each row or column of a 

3319 DataFrame. 

3320 api.typing.DataFrameGroupBy.first : Compute the first entry of each 

3321 column within each group. 

3322 api.typing.DataFrameGroupBy.nth : Take the nth row from each group. 

3323 

3324 Examples 

3325 -------- 

3326 >>> df = pd.DataFrame(dict(A=[1, 1, 3], B=[5, None, 6], C=[1, 2, 3])) 

3327 >>> df.groupby("A").last() 

3328 B C 

3329 A 

3330 1 5.0 2 

3331 3 6.0 3 

3332 """ 

3333 

3334 def last_compat(obj: NDFrameT): 

3335 def last(x: Series): 

3336 """Helper function for last item that isn't NA.""" 

3337 arr = x.array[notna(x.array)] 

3338 if not len(arr): 

3339 return x.array.dtype.na_value 

3340 return arr[-1] 

3341 

3342 if isinstance(obj, DataFrame): 

3343 return obj.apply(last) 

3344 elif isinstance(obj, Series): 

3345 return last(obj) 

3346 else: # pragma: no cover 

3347 raise TypeError(type(obj)) 

3348 

3349 return self._cython_agg_general( 

3350 "last", 

3351 alt=last_compat, 

3352 numeric_only=numeric_only, 

3353 min_count=min_count, 

3354 skipna=skipna, 

3355 ) 

3356 

3357 @final 

3358 def ohlc(self) -> DataFrame: 

3359 """ 

3360 Compute open, high, low and close values of a group, excluding missing values. 

3361 

3362 For multiple groupings, the result index will be a MultiIndex 

3363 

3364 Returns 

3365 ------- 

3366 DataFrame 

3367 Open, high, low and close values within each group. 

3368 

3369 See Also 

3370 -------- 

3371 DataFrame.agg : Aggregate using one or more operations over the specified axis. 

3372 DataFrame.resample : Resample time-series data. 

3373 DataFrame.groupby : Group DataFrame using a mapper or by a Series of columns. 

3374 

3375 Examples 

3376 -------- 

3377 

3378 For SeriesGroupBy: 

3379 

3380 >>> lst = [ 

3381 ... "SPX", 

3382 ... "CAC", 

3383 ... "SPX", 

3384 ... "CAC", 

3385 ... "SPX", 

3386 ... "CAC", 

3387 ... "SPX", 

3388 ... "CAC", 

3389 ... ] 

3390 >>> ser = pd.Series([3.4, 9.0, 7.2, 5.2, 8.8, 9.4, 0.1, 0.5], index=lst) 

3391 >>> ser 

3392 SPX 3.4 

3393 CAC 9.0 

3394 SPX 7.2 

3395 CAC 5.2 

3396 SPX 8.8 

3397 CAC 9.4 

3398 SPX 0.1 

3399 CAC 0.5 

3400 dtype: float64 

3401 >>> ser.groupby(level=0).ohlc() 

3402 open high low close 

3403 CAC 9.0 9.4 0.5 0.5 

3404 SPX 3.4 8.8 0.1 0.1 

3405 

3406 For DataFrameGroupBy: 

3407 

3408 >>> data = { 

3409 ... 2022: [1.2, 2.3, 8.9, 4.5, 4.4, 3, 2, 1], 

3410 ... 2023: [3.4, 9.0, 7.2, 5.2, 8.8, 9.4, 8.2, 1.0], 

3411 ... } 

3412 >>> df = pd.DataFrame( 

3413 ... data, index=["SPX", "CAC", "SPX", "CAC", "SPX", "CAC", "SPX", "CAC"] 

3414 ... ) 

3415 >>> df 

3416 2022 2023 

3417 SPX 1.2 3.4 

3418 CAC 2.3 9.0 

3419 SPX 8.9 7.2 

3420 CAC 4.5 5.2 

3421 SPX 4.4 8.8 

3422 CAC 3.0 9.4 

3423 SPX 2.0 8.2 

3424 CAC 1.0 1.0 

3425 >>> df.groupby(level=0).ohlc() 

3426 2022 2023 

3427 open high low close open high low close 

3428 CAC 2.3 4.5 1.0 1.0 9.0 9.4 1.0 1.0 

3429 SPX 1.2 8.9 1.2 2.0 3.4 8.8 3.4 8.2 

3430 

3431 For Resampler: 

3432 

3433 >>> ser = pd.Series( 

3434 ... [1, 3, 2, 4, 3, 5], 

3435 ... index=pd.DatetimeIndex( 

3436 ... [ 

3437 ... "2023-01-01", 

3438 ... "2023-01-10", 

3439 ... "2023-01-15", 

3440 ... "2023-02-01", 

3441 ... "2023-02-10", 

3442 ... "2023-02-15", 

3443 ... ] 

3444 ... ), 

3445 ... ) 

3446 >>> ser.resample("MS").ohlc() 

3447 open high low close 

3448 2023-01-01 1 3 1 2 

3449 2023-02-01 4 5 3 5 

3450 """ 

3451 if self.obj.ndim == 1: 

3452 obj = self._selected_obj 

3453 

3454 is_numeric = is_numeric_dtype(obj.dtype) 

3455 if not is_numeric: 

3456 raise DataError("No numeric types to aggregate") 

3457 

3458 res_values = self._grouper._cython_operation( 

3459 obj._values, "ohlc", axis=0, min_count=-1 

3460 ) 

3461 

3462 agg_names = ["open", "high", "low", "close"] 

3463 result = self.obj._constructor_expanddim( 

3464 res_values, index=self._grouper.result_index, columns=agg_names 

3465 ) 

3466 if not self.as_index: 

3467 result = result.reset_index() 

3468 return result 

3469 

3470 result = self._apply_to_column_groupbys(lambda sgb: sgb.ohlc()) 

3471 return result 

3472 

3473 def describe( 

3474 self, 

3475 percentiles=None, 

3476 include=None, 

3477 exclude=None, 

3478 ) -> NDFrameT: 

3479 obj = self._obj_with_exclusions 

3480 

3481 if len(obj) == 0: 

3482 described = obj.describe( 

3483 percentiles=percentiles, include=include, exclude=exclude 

3484 ) 

3485 if obj.ndim == 1: 

3486 result = described 

3487 else: 

3488 result = described.unstack() 

3489 return result.to_frame().T.iloc[:0] 

3490 

3491 with com.temp_setattr(self, "as_index", True): 

3492 result = self._python_apply_general( 

3493 lambda x: x.describe( 

3494 percentiles=percentiles, include=include, exclude=exclude 

3495 ), 

3496 obj, 

3497 not_indexed_same=True, 

3498 ) 

3499 

3500 # GH#49256 - properly handle the grouping column(s) 

3501 result = result.unstack() 

3502 if not self.as_index: 

3503 result = self._insert_inaxis_grouper(result) 

3504 result.index = default_index(len(result)) 

3505 

3506 return result 

3507 

3508 @final 

3509 def resample( 

3510 self, 

3511 rule, 

3512 closed: Literal["right", "left"] | None = None, 

3513 label: Literal["right", "left"] | None = None, 

3514 convention: Literal["start", "end", "s", "e"] | None = None, 

3515 on: Level | None = None, 

3516 origin: str | TimestampConvertibleTypes = "start_day", 

3517 offset: TimedeltaConvertibleTypes | None = None, 

3518 group_keys: bool = False, 

3519 *, 

3520 include_groups: bool = False, 

3521 **kwargs, 

3522 ) -> Resampler: 

3523 """ 

3524 Provide resampling within each group of a groupby. 

3525 

3526 Given a grouper, the function resamples it according to a string 

3527 "string" -> "frequency". 

3528 

3529 See the :ref:`frequency aliases <timeseries.offset_aliases>` 

3530 documentation for more details. 

3531 

3532 Parameters 

3533 ---------- 

3534 rule : str or DateOffset 

3535 The offset string or object representing target grouper conversion. 

3536 closed : {'right', 'left'}, optional 

3537 Which side of bin interval is closed. See :meth:`Series.resample`. 

3538 label : {'right', 'left'}, optional 

3539 Which bin edge label to label bucket with. See :meth:`Series.resample`. 

3540 convention : {'start', 'end', 's', 'e'}, optional 

3541 For ``PeriodIndex`` only. See :meth:`Series.resample`. 

3542 on : str, optional 

3543 For a DataFrame, column to use instead of index for resampling. 

3544 Column must be datetime-like. 

3545 origin : Timestamp or str, default 'start_day' 

3546 The timestamp on which to adjust the grouping. See 

3547 :meth:`Series.resample` for accepted string values. 

3548 offset : Timedelta or str, optional 

3549 An offset timedelta added to the origin. 

3550 group_keys : bool, default False 

3551 Whether to include the group keys in the result index when using 

3552 ``.apply()`` on the resampled object. 

3553 include_groups : bool, default False 

3554 When True, will attempt to include the groupings in the operation in 

3555 the case that they are columns of the DataFrame. If this raises a 

3556 TypeError, the result will be computed with the groupings excluded. 

3557 When False, the groupings will be excluded when applying ``func``. 

3558 

3559 .. versionadded:: 2.2.0 

3560 

3561 .. versionchanged:: 3.0 

3562 

3563 The default was changed to False, and True is no longer allowed. 

3564 

3565 **kwargs 

3566 Additional keyword arguments forwarded to the underlying 

3567 :class:`Grouper`. 

3568 

3569 Returns 

3570 ------- 

3571 DatetimeIndexResampler, PeriodIndexResampler or TimedeltaResampler 

3572 Resampler object for the type of the index. 

3573 

3574 See Also 

3575 -------- 

3576 Series.resample : Resample a Series. 

3577 DataFrame.resample : Resample a DataFrame. 

3578 Grouper : Specify a frequency to resample with when 

3579 grouping by a key. 

3580 DatetimeIndex.resample : Frequency conversion and resampling of 

3581 time series. 

3582 

3583 Examples 

3584 -------- 

3585 >>> idx = pd.date_range("1/1/2000", periods=4, freq="min") 

3586 >>> df = pd.DataFrame(data=4 * [range(2)], index=idx, columns=["a", "b"]) 

3587 >>> df.iloc[2, 0] = 5 

3588 >>> df 

3589 a b 

3590 2000-01-01 00:00:00 0 1 

3591 2000-01-01 00:01:00 0 1 

3592 2000-01-01 00:02:00 5 1 

3593 2000-01-01 00:03:00 0 1 

3594 

3595 Downsample the DataFrame into 3 minute bins and sum the values of 

3596 the timestamps falling into a bin. 

3597 

3598 >>> df.groupby("a").resample("3min").sum() 

3599 b 

3600 a 

3601 0 2000-01-01 00:00:00 2 

3602 2000-01-01 00:03:00 1 

3603 5 2000-01-01 00:00:00 1 

3604 

3605 Upsample the series into 30 second bins. 

3606 

3607 >>> df.groupby("a").resample("30s").sum() 

3608 b 

3609 a 

3610 0 2000-01-01 00:00:00 1 

3611 2000-01-01 00:00:30 0 

3612 2000-01-01 00:01:00 1 

3613 2000-01-01 00:01:30 0 

3614 2000-01-01 00:02:00 0 

3615 2000-01-01 00:02:30 0 

3616 2000-01-01 00:03:00 1 

3617 5 2000-01-01 00:02:00 1 

3618 

3619 Resample by month. Values are assigned to the month of the period. 

3620 

3621 >>> df.groupby("a").resample("ME").sum() 

3622 b 

3623 a 

3624 0 2000-01-31 3 

3625 5 2000-01-31 1 

3626 

3627 Downsample the series into 3 minute bins as above, but close the right 

3628 side of the bin interval. 

3629 

3630 >>> (df.groupby("a").resample("3min", closed="right").sum()) 

3631 b 

3632 a 

3633 0 1999-12-31 23:57:00 1 

3634 2000-01-01 00:00:00 2 

3635 5 2000-01-01 00:00:00 1 

3636 

3637 Downsample the series into 3 minute bins and close the right side of 

3638 the bin interval, but label each bin using the right edge instead of 

3639 the left. 

3640 

3641 >>> (df.groupby("a").resample("3min", closed="right", label="right").sum()) 

3642 b 

3643 a 

3644 0 2000-01-01 00:00:00 1 

3645 2000-01-01 00:03:00 2 

3646 5 2000-01-01 00:03:00 1 

3647 """ 

3648 from pandas.core.resample import get_resampler_for_grouping 

3649 

3650 if include_groups: 

3651 raise ValueError("include_groups=True is no longer allowed.") 

3652 

3653 return get_resampler_for_grouping( 

3654 self, 

3655 rule, 

3656 on=on, 

3657 closed=closed, 

3658 label=label, 

3659 convention=convention, 

3660 origin=origin, 

3661 offset=offset, 

3662 group_keys=group_keys, 

3663 **kwargs, 

3664 ) 

3665 

3666 @final 

3667 def rolling( 

3668 self, 

3669 window: int | datetime.timedelta | str | BaseOffset | BaseIndexer, 

3670 min_periods: int | None = None, 

3671 center: bool = False, 

3672 win_type: str | None = None, 

3673 on: str | None = None, 

3674 closed: IntervalClosedType | None = None, 

3675 method: str = "single", 

3676 ) -> RollingGroupBy: 

3677 """ 

3678 Return a rolling grouper, providing rolling functionality per group. 

3679 

3680 Allows the application of rolling window operations 

3681 (e.g., moving averages) independently within each group defined 

3682 by the groupby keys. 

3683 

3684 Parameters 

3685 ---------- 

3686 window : int, timedelta, str, offset, or BaseIndexer subclass 

3687 Interval of the moving window. 

3688 

3689 If an integer, the delta between the start and end of each window. 

3690 The number of points in the window depends on the ``closed`` argument. 

3691 

3692 If a timedelta, str, or offset, the time period of each window. Each 

3693 window will be a variable sized based on the observations included in 

3694 the time-period. This is only valid for datetimelike indexes. 

3695 The offset must correspond to a fixed frequency (for example, ``'2D'`` 

3696 or ``'1h'``); non-fixed frequencies such as ``'B'`` (business day) or 

3697 ``'ME'`` (month end) are not supported and raise ``ValueError``. 

3698 To learn more about the offsets & frequency strings, please see 

3699 :ref:`this link<timeseries.offset_aliases>`. 

3700 

3701 If a BaseIndexer subclass, the window boundaries 

3702 based on the defined ``get_window_bounds`` method. Additional rolling 

3703 keyword arguments, namely ``min_periods``, ``center``, ``closed`` and 

3704 ``step`` will be passed to ``get_window_bounds``. 

3705 

3706 min_periods : int, default None 

3707 Minimum number of observations in window required to have a value; 

3708 otherwise, result is ``np.nan``. 

3709 

3710 For a window that is specified by an offset, 

3711 ``min_periods`` will default to 1. 

3712 

3713 For a window that is specified by an integer, ``min_periods`` will default 

3714 to the size of the window. 

3715 

3716 center : bool, default False 

3717 If False, set the window labels as the right edge of the window index. 

3718 

3719 If True, set the window labels as the center of the window index. 

3720 

3721 win_type : str, default None 

3722 If ``None``, all points are evenly weighted. 

3723 

3724 If a string, it must be a valid `scipy.signal window function 

3725 <https://docs.scipy.org/doc/scipy/reference/signal.windows.html#module-scipy.signal.windows>`__. 

3726 

3727 Certain Scipy window types require additional parameters to be passed 

3728 in the aggregation function. The additional parameters must match 

3729 the keywords specified in the Scipy window type method signature. 

3730 

3731 on : str, optional 

3732 For a DataFrame, a column label or Index level on which 

3733 to calculate the rolling window, rather than the DataFrame's index. 

3734 

3735 For integer ``window`` values, the window bounds are based on the number 

3736 of observations and are not calculated using the values of the 

3737 ``on`` column. The ``on`` column is excluded from the aggregation, 

3738 but is included in the result when its values differ from the 

3739 object's index. 

3740 

3741 closed : str, default None 

3742 Determines the inclusivity of points in the window 

3743 

3744 If ``'right'``, uses the window (first, last] meaning the last point 

3745 is included in the calculations. 

3746 

3747 If ``'left'``, uses the window [first, last) meaning the first point 

3748 is included in the calculations. 

3749 

3750 If ``'both'``, uses the window [first, last] meaning all points in 

3751 the window are included in the calculations. 

3752 

3753 If ``'neither'``, uses the window (first, last) meaning the first 

3754 and last points in the window are excluded from calculations. 

3755 

3756 () and [] are referencing open and closed set 

3757 notation respetively. 

3758 

3759 Default ``None`` (``'right'``). 

3760 

3761 method : str {'single', 'table'}, default 'single' 

3762 Execute the rolling operation per single column or row (``'single'``) 

3763 or over the entire object (``'table'``). 

3764 

3765 This argument is only implemented when specifying ``engine='numba'`` 

3766 in the method call. 

3767 

3768 Returns 

3769 ------- 

3770 pandas.api.typing.RollingGroupBy 

3771 Return a new grouper with our rolling appended. 

3772 

3773 See Also 

3774 -------- 

3775 Series.rolling : Calling object with Series data. 

3776 DataFrame.rolling : Calling object with DataFrames. 

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

3778 DataFrame.groupby : Apply a function groupby. 

3779 

3780 Examples 

3781 -------- 

3782 >>> df = pd.DataFrame( 

3783 ... { 

3784 ... "A": [1, 1, 2, 2], 

3785 ... "B": [1, 2, 3, 4], 

3786 ... "C": [0.362, 0.227, 1.267, -0.562], 

3787 ... } 

3788 ... ) 

3789 >>> df 

3790 A B C 

3791 0 1 1 0.362 

3792 1 1 2 0.227 

3793 2 2 3 1.267 

3794 3 2 4 -0.562 

3795 

3796 >>> df.groupby("A").rolling(2).sum() 

3797 B C 

3798 A 

3799 1 0 NaN NaN 

3800 1 3.0 0.589 

3801 2 2 NaN NaN 

3802 3 7.0 0.705 

3803 

3804 >>> df.groupby("A").rolling(2, min_periods=1).sum() 

3805 B C 

3806 A 

3807 1 0 1.0 0.362 

3808 1 3.0 0.589 

3809 2 2 3.0 1.267 

3810 3 7.0 0.705 

3811 

3812 >>> df.groupby("A").rolling(2, on="B").sum() 

3813 B C 

3814 A 

3815 1 0 1 NaN 

3816 1 2 0.589 

3817 2 2 3 NaN 

3818 3 4 0.705 

3819 """ 

3820 from pandas.core.window import RollingGroupBy 

3821 

3822 return RollingGroupBy( 

3823 self._selected_obj, 

3824 window=window, 

3825 min_periods=min_periods, 

3826 center=center, 

3827 win_type=win_type, 

3828 on=on, 

3829 closed=closed, 

3830 method=method, 

3831 _grouper=self._grouper, 

3832 _as_index=self.as_index, 

3833 ) 

3834 

3835 @final 

3836 def expanding( 

3837 self, 

3838 min_periods: int = 1, 

3839 method: str = "single", 

3840 ) -> ExpandingGroupBy: 

3841 """ 

3842 Return an expanding grouper, providing expanding functionality per group. 

3843 

3844 Each group's expanding window includes all prior rows within that 

3845 group up to and including the current row. 

3846 

3847 Parameters 

3848 ---------- 

3849 min_periods : int, default 1 

3850 Minimum number of observations in window required to have a value; 

3851 otherwise, result is ``np.nan``. 

3852 

3853 method : str {'single', 'table'}, default 'single' 

3854 Execute the expanding operation per single column or row (``'single'``) 

3855 or over the entire object (``'table'``). 

3856 

3857 This argument is only implemented when specifying ``engine='numba'`` 

3858 in the method call. 

3859 

3860 Returns 

3861 ------- 

3862 pandas.api.typing.ExpandingGroupBy 

3863 An object that supports expanding transformations over each group. 

3864 

3865 See Also 

3866 -------- 

3867 Series.expanding : Expanding transformations for Series. 

3868 DataFrame.expanding : Expanding transformations for DataFrames. 

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

3870 DataFrame.groupby : Apply a function groupby. 

3871 

3872 Examples 

3873 -------- 

3874 >>> df = pd.DataFrame( 

3875 ... { 

3876 ... "Class": ["A", "A", "A", "B", "B", "B"], 

3877 ... "Value": [10, 20, 30, 40, 50, 60], 

3878 ... } 

3879 ... ) 

3880 >>> df 

3881 Class Value 

3882 0 A 10 

3883 1 A 20 

3884 2 A 30 

3885 3 B 40 

3886 4 B 50 

3887 5 B 60 

3888 

3889 >>> df.groupby("Class").expanding().mean() 

3890 Value 

3891 Class 

3892 A 0 10.0 

3893 1 15.0 

3894 2 20.0 

3895 B 3 40.0 

3896 4 45.0 

3897 5 50.0 

3898 """ 

3899 from pandas.core.window import ExpandingGroupBy 

3900 

3901 return ExpandingGroupBy( 

3902 self._selected_obj, 

3903 min_periods=min_periods, 

3904 method=method, 

3905 _grouper=self._grouper, 

3906 ) 

3907 

3908 @final 

3909 def ewm( 

3910 self, 

3911 com: float | None = None, 

3912 span: float | None = None, 

3913 halflife: float | str | Timedelta | None = None, 

3914 alpha: float | None = None, 

3915 min_periods: int | None = 0, 

3916 adjust: bool = True, 

3917 ignore_na: bool = False, 

3918 times: np.ndarray | Series | None = None, 

3919 method: str = "single", 

3920 ) -> ExponentialMovingWindowGroupBy: 

3921 """ 

3922 Return an ewm grouper, providing ewm functionality per group. 

3923 

3924 The decay can be specified in terms of center of mass, span, 

3925 half-life, or smoothing factor alpha. 

3926 

3927 Parameters 

3928 ---------- 

3929 com : float, optional 

3930 Specify decay in terms of center of mass. 

3931 Alternative to ``span``, ``halflife``, and ``alpha``. 

3932 

3933 span : float, optional 

3934 Specify decay in terms of span. 

3935 

3936 halflife : float, str, or Timedelta, optional 

3937 Specify decay in terms of half-life. 

3938 

3939 alpha : float, optional 

3940 Specify smoothing factor directly. 

3941 

3942 min_periods : int, default 0 

3943 Minimum number of observations in the window required to have a value; 

3944 otherwise, result is ``np.nan``. 

3945 

3946 adjust : bool, default True 

3947 Divide by decaying adjustment factor to account for imbalance in 

3948 relative weights. 

3949 

3950 ignore_na : bool, default False 

3951 Ignore missing values when calculating weights. 

3952 

3953 times : str or array-like of datetime64, optional 

3954 Times corresponding to the observations. 

3955 

3956 method : {'single', 'table'}, default 'single' 

3957 Execute the operation per group independently (``'single'``) or over the 

3958 entire object before regrouping (``'table'``). Only applicable to 

3959 ``mean()``, and only when using ``engine='numba'``. 

3960 

3961 Returns 

3962 ------- 

3963 pandas.api.typing.ExponentialMovingWindowGroupBy 

3964 An object that supports exponentially weighted moving transformations over 

3965 each group. 

3966 

3967 See Also 

3968 -------- 

3969 Series.ewm : EWM transformations for Series. 

3970 DataFrame.ewm : EWM transformations for DataFrames. 

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

3972 DataFrame.groupby : Apply a function groupby. 

3973 

3974 Examples 

3975 -------- 

3976 >>> df = pd.DataFrame( 

3977 ... { 

3978 ... "Class": ["A", "A", "A", "B", "B", "B"], 

3979 ... "Value": [10, 20, 30, 40, 50, 60], 

3980 ... } 

3981 ... ) 

3982 >>> df 

3983 Class Value 

3984 0 A 10 

3985 1 A 20 

3986 2 A 30 

3987 3 B 40 

3988 4 B 50 

3989 5 B 60 

3990 

3991 >>> df.groupby("Class").ewm(com=0.5).mean() 

3992 Value 

3993 Class 

3994 A 0 10.000000 

3995 1 17.500000 

3996 2 26.153846 

3997 B 3 40.000000 

3998 4 47.500000 

3999 5 56.153846 

4000 """ 

4001 from pandas.core.window import ExponentialMovingWindowGroupBy 

4002 

4003 return ExponentialMovingWindowGroupBy( 

4004 self._selected_obj, 

4005 com=com, 

4006 span=span, 

4007 halflife=halflife, 

4008 alpha=alpha, 

4009 min_periods=min_periods, 

4010 adjust=adjust, 

4011 ignore_na=ignore_na, 

4012 times=times, 

4013 method=method, 

4014 _grouper=self._grouper, 

4015 ) 

4016 

4017 @final 

4018 def _fill(self, direction: Literal["ffill", "bfill"], limit: int | None = None): 

4019 """ 

4020 Shared function for `pad` and `backfill` to call Cython method. 

4021 

4022 Parameters 

4023 ---------- 

4024 direction : {'ffill', 'bfill'} 

4025 Direction passed to underlying Cython function. `bfill` will cause 

4026 values to be filled backwards. `ffill` and any other values will 

4027 default to a forward fill 

4028 limit : int, default None 

4029 Maximum number of consecutive values to fill. If `None`, this 

4030 method will convert to -1 prior to passing to Cython 

4031 

4032 Returns 

4033 ------- 

4034 `Series` or `DataFrame` with filled values 

4035 

4036 See Also 

4037 -------- 

4038 pad : Forward fill values within each group. 

4039 backfill : Backward fill values within each group. 

4040 """ 

4041 # Need int value for Cython 

4042 if limit is None: 

4043 limit = -1 

4044 

4045 ids = self._grouper.ids 

4046 ngroups = self._grouper.ngroups 

4047 

4048 col_func = partial( 

4049 libgroupby.group_fillna_indexer, 

4050 labels=ids, 

4051 limit=limit, 

4052 compute_ffill=(direction == "ffill"), 

4053 ngroups=ngroups, 

4054 ) 

4055 

4056 def blk_func(values: ArrayLike) -> ArrayLike: 

4057 mask = isna(values) 

4058 if values.ndim == 1: 

4059 indexer = np.empty(values.shape, dtype=np.intp) 

4060 col_func(out=indexer, mask=mask) # type: ignore[arg-type] 

4061 return algorithms.take_nd(values, indexer) 

4062 

4063 else: 

4064 # We broadcast algorithms.take_nd analogous to 

4065 # np.take_along_axis 

4066 if isinstance(values, np.ndarray): 

4067 dtype = values.dtype 

4068 if self._grouper.has_dropped_na: 

4069 # dropped null groups give rise to nan in the result 

4070 dtype = ensure_dtype_can_hold_na(values.dtype) 

4071 out = np.empty(values.shape, dtype=dtype) 

4072 else: 

4073 # Note: we only get here with backfill/pad, 

4074 # so if we have a dtype that cannot hold NAs, 

4075 # then there will be no -1s in indexer, so we can use 

4076 # the original dtype (no need to ensure_dtype_can_hold_na) 

4077 out = type(values)._empty(values.shape, dtype=values.dtype) 

4078 

4079 for i, value_element in enumerate(values): 

4080 # call group_fillna_indexer column-wise 

4081 indexer = np.empty(values.shape[1], dtype=np.intp) 

4082 col_func(out=indexer, mask=mask[i]) 

4083 out[i, :] = algorithms.take_nd(value_element, indexer) 

4084 return out 

4085 

4086 mgr = self._get_data_to_aggregate() 

4087 res_mgr = mgr.apply(blk_func) 

4088 

4089 new_obj = self._wrap_agged_manager(res_mgr) 

4090 new_obj.index = self.obj.index 

4091 return new_obj 

4092 

4093 @final 

4094 def ffill(self, limit: int | None = None): 

4095 """ 

4096 Forward fill the values. 

4097 

4098 Propagates the last valid observation forward within each group 

4099 to fill missing values. 

4100 

4101 Parameters 

4102 ---------- 

4103 limit : int, optional 

4104 Limit of how many values to fill. 

4105 

4106 Returns 

4107 ------- 

4108 Series or DataFrame 

4109 Object with missing values filled. 

4110 

4111 See Also 

4112 -------- 

4113 Series.ffill : Forward fill missing values in a Series. 

4114 DataFrame.ffill : Forward fill missing values in a DataFrame. 

4115 Series.fillna: Fill NaN values of a Series. 

4116 DataFrame.fillna: Fill NaN values of a DataFrame. 

4117 

4118 Examples 

4119 -------- 

4120 

4121 For SeriesGroupBy: 

4122 

4123 >>> key = [0, 0, 1, 1] 

4124 >>> ser = pd.Series([np.nan, 2, 3, np.nan], index=key) 

4125 >>> ser 

4126 0 NaN 

4127 0 2.0 

4128 1 3.0 

4129 1 NaN 

4130 dtype: float64 

4131 >>> ser.groupby(level=0).ffill() 

4132 0 NaN 

4133 0 2.0 

4134 1 3.0 

4135 1 3.0 

4136 dtype: float64 

4137 

4138 For DataFrameGroupBy: 

4139 

4140 >>> df = pd.DataFrame( 

4141 ... { 

4142 ... "key": [0, 0, 1, 1, 1], 

4143 ... "A": [np.nan, 2, np.nan, 3, np.nan], 

4144 ... "B": [2, 3, np.nan, np.nan, np.nan], 

4145 ... "C": [np.nan, np.nan, 2, np.nan, np.nan], 

4146 ... } 

4147 ... ) 

4148 >>> df 

4149 key A B C 

4150 0 0 NaN 2.0 NaN 

4151 1 0 2.0 3.0 NaN 

4152 2 1 NaN NaN 2.0 

4153 3 1 3.0 NaN NaN 

4154 4 1 NaN NaN NaN 

4155 

4156 Propagate non-null values forward or backward within each group along columns. 

4157 

4158 >>> df.groupby("key").ffill() 

4159 A B C 

4160 0 NaN 2.0 NaN 

4161 1 2.0 3.0 NaN 

4162 2 NaN NaN 2.0 

4163 3 3.0 NaN 2.0 

4164 4 3.0 NaN 2.0 

4165 

4166 Propagate non-null values forward or backward within each group along rows. 

4167 

4168 >>> df.T.groupby(np.array([0, 0, 1, 1])).ffill().T 

4169 key A B C 

4170 0 0.0 0.0 2.0 2.0 

4171 1 0.0 2.0 3.0 3.0 

4172 2 1.0 1.0 NaN 2.0 

4173 3 1.0 3.0 NaN NaN 

4174 4 1.0 1.0 NaN NaN 

4175 

4176 Only replace the first NaN element within a group along columns. 

4177 

4178 >>> df.groupby("key").ffill(limit=1) 

4179 A B C 

4180 0 NaN 2.0 NaN 

4181 1 2.0 3.0 NaN 

4182 2 NaN NaN 2.0 

4183 3 3.0 NaN 2.0 

4184 4 3.0 NaN NaN 

4185 """ 

4186 return self._fill("ffill", limit=limit) 

4187 

4188 @final 

4189 def bfill(self, limit: int | None = None): 

4190 """ 

4191 Backward fill the values. 

4192 

4193 Fill missing values within each group by propagating the next 

4194 valid observation backward. 

4195 

4196 Parameters 

4197 ---------- 

4198 limit : int, optional 

4199 Limit of how many values to fill. 

4200 

4201 Returns 

4202 ------- 

4203 Series or DataFrame 

4204 Object with missing values filled. 

4205 

4206 See Also 

4207 -------- 

4208 Series.bfill : Backward fill the missing values in the dataset. 

4209 DataFrame.bfill: Backward fill the missing values in the dataset. 

4210 Series.fillna: Fill NaN values of a Series. 

4211 DataFrame.fillna: Fill NaN values of a DataFrame. 

4212 

4213 Examples 

4214 -------- 

4215 

4216 With Series: 

4217 

4218 >>> index = ["Falcon", "Falcon", "Parrot", "Parrot", "Parrot"] 

4219 >>> s = pd.Series([None, 1, None, None, 3], index=index) 

4220 >>> s 

4221 Falcon NaN 

4222 Falcon 1.0 

4223 Parrot NaN 

4224 Parrot NaN 

4225 Parrot 3.0 

4226 dtype: float64 

4227 >>> s.groupby(level=0).bfill() 

4228 Falcon 1.0 

4229 Falcon 1.0 

4230 Parrot 3.0 

4231 Parrot 3.0 

4232 Parrot 3.0 

4233 dtype: float64 

4234 >>> s.groupby(level=0).bfill(limit=1) 

4235 Falcon 1.0 

4236 Falcon 1.0 

4237 Parrot NaN 

4238 Parrot 3.0 

4239 Parrot 3.0 

4240 dtype: float64 

4241 

4242 With DataFrame: 

4243 

4244 >>> df = pd.DataFrame( 

4245 ... {"A": [1, None, None, None, 4], "B": [None, None, 5, None, 7]}, 

4246 ... index=index, 

4247 ... ) 

4248 >>> df 

4249 A B 

4250 Falcon 1.0 NaN 

4251 Falcon NaN NaN 

4252 Parrot NaN 5.0 

4253 Parrot NaN NaN 

4254 Parrot 4.0 7.0 

4255 >>> df.groupby(level=0).bfill() 

4256 A B 

4257 Falcon 1.0 NaN 

4258 Falcon NaN NaN 

4259 Parrot 4.0 5.0 

4260 Parrot 4.0 7.0 

4261 Parrot 4.0 7.0 

4262 >>> df.groupby(level=0).bfill(limit=1) 

4263 A B 

4264 Falcon 1.0 NaN 

4265 Falcon NaN NaN 

4266 Parrot NaN 5.0 

4267 Parrot 4.0 7.0 

4268 Parrot 4.0 7.0 

4269 """ 

4270 return self._fill("bfill", limit=limit) 

4271 

4272 @final 

4273 @property 

4274 def nth(self) -> GroupByNthSelector: 

4275 """ 

4276 Take the nth row from each group if n is an int, otherwise a subset of rows. 

4277 

4278 .. deprecated:: 3.1.0 

4279 

4280 Index notation (``g.nth[n]``) is deprecated in favor of calling 

4281 ``g.nth(n)`` and will be removed in a future version of pandas. 

4282 

4283 If dropna, will take the nth non-null row, dropna is either 

4284 'all' or 'any'; this is equivalent to calling dropna(how=dropna) 

4285 before the groupby. 

4286 

4287 Returns 

4288 ------- 

4289 Series or DataFrame 

4290 N-th value within each group. 

4291 

4292 See Also 

4293 -------- 

4294 Series.nth : Apply function nth to a Series. 

4295 DataFrame.nth : Apply function nth to each row or column of a DataFrame. 

4296 

4297 Examples 

4298 -------- 

4299 

4300 >>> df = pd.DataFrame( 

4301 ... {"A": [1, 1, 2, 1, 2], "B": [np.nan, 2, 3, 4, 5]}, columns=["A", "B"] 

4302 ... ) 

4303 >>> g = df.groupby("A") 

4304 >>> g.nth(0) 

4305 A B 

4306 0 1 NaN 

4307 2 2 3.0 

4308 >>> g.nth(1) 

4309 A B 

4310 1 1 2.0 

4311 4 2 5.0 

4312 >>> g.nth(-1) 

4313 A B 

4314 3 1 4.0 

4315 4 2 5.0 

4316 >>> g.nth([0, 1]) 

4317 A B 

4318 0 1 NaN 

4319 1 1 2.0 

4320 2 2 3.0 

4321 4 2 5.0 

4322 >>> g.nth(slice(None, -1)) 

4323 A B 

4324 0 1 NaN 

4325 1 1 2.0 

4326 2 2 3.0 

4327 

4328 Specifying `dropna` allows ignoring ``NaN`` values 

4329 

4330 >>> g.nth(0, dropna="any") 

4331 A B 

4332 1 1 2.0 

4333 2 2 3.0 

4334 

4335 When the specified ``n`` is larger than any of the groups, an 

4336 empty DataFrame is returned 

4337 

4338 >>> g.nth(3, dropna="any") 

4339 Empty DataFrame 

4340 Columns: [A, B] 

4341 Index: [] 

4342 """ 

4343 return GroupByNthSelector(self) 

4344 

4345 def _nth( 

4346 self, 

4347 n: PositionalIndexer | tuple, 

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

4349 ) -> NDFrameT: 

4350 if not dropna: 

4351 mask = self._make_mask_from_positional_indexer(n) 

4352 

4353 ids = self._grouper.ids 

4354 

4355 # Drop NA values in grouping 

4356 mask = mask & (ids != -1) 

4357 

4358 out = self._mask_selected_obj(mask) 

4359 return out 

4360 

4361 # dropna is truthy 

4362 if not is_integer(n): 

4363 raise ValueError("dropna option only supported for an integer argument") 

4364 

4365 if dropna not in ["any", "all"]: 

4366 # Note: when agg-ing picker doesn't raise this, just returns NaN 

4367 raise ValueError( 

4368 "For a DataFrame or Series groupby.nth, dropna must be " 

4369 "either None, 'any' or 'all', " 

4370 f"(was passed {dropna})." 

4371 ) 

4372 

4373 # old behaviour, but with all and any support for DataFrames. 

4374 # modified in GH 7559 to have better perf 

4375 n = cast("int", n) 

4376 dropped = self._selected_obj.dropna(how=dropna, axis=0) 

4377 

4378 # get a new grouper for our dropped obj 

4379 grouper: np.ndarray | Index | ops.BaseGrouper 

4380 if len(dropped) == len(self._selected_obj): 

4381 # Nothing was dropped, can use the same grouper 

4382 grouper = self._grouper 

4383 else: 

4384 # we don't have the grouper info available 

4385 # (e.g. we have selected out 

4386 # a column that is not in the current object) 

4387 axis = self._grouper.axis 

4388 grouper = self._grouper.codes_info[axis.isin(dropped.index)] 

4389 if self._grouper.has_dropped_na: 

4390 # Null groups need to still be encoded as -1 when passed to groupby 

4391 nulls = grouper == -1 

4392 # error: No overload variant of "where" matches argument types 

4393 # "Any", "NAType", "Any" 

4394 values = np.where(nulls, NA, grouper) # type: ignore[call-overload] 

4395 grouper = Index(values, dtype="Int64", copy=False) 

4396 

4397 grb = dropped.groupby(grouper, as_index=self.as_index, sort=self.sort) 

4398 return grb.nth(n) 

4399 

4400 @final 

4401 def quantile( 

4402 self, 

4403 q: float | AnyArrayLike = 0.5, 

4404 interpolation: Literal[ 

4405 "linear", "lower", "higher", "nearest", "midpoint" 

4406 ] = "linear", 

4407 numeric_only: bool = False, 

4408 ): 

4409 """ 

4410 Return group values at the given quantile, a la numpy.percentile. 

4411 

4412 This method returns the value at the given quantile for each group, 

4413 using the specified interpolation method when the desired quantile 

4414 falls between two data points. 

4415 

4416 Parameters 

4417 ---------- 

4418 q : float or array-like, default 0.5 (50% quantile) 

4419 Value(s) between 0 and 1 providing the quantile(s) to compute. 

4420 interpolation : {'linear', 'lower', 'higher', 'midpoint', 'nearest'} 

4421 Method to use when the desired quantile falls between two points. 

4422 numeric_only : bool, default False 

4423 Include only `float`, `int` or `boolean` data. 

4424 

4425 .. versionchanged:: 2.0.0 

4426 

4427 numeric_only now defaults to ``False``. 

4428 

4429 Returns 

4430 ------- 

4431 Series or DataFrame 

4432 Return type determined by caller of GroupBy object. 

4433 

4434 See Also 

4435 -------- 

4436 Series.quantile : Similar method for Series. 

4437 DataFrame.quantile : Similar method for DataFrame. 

4438 numpy.percentile : NumPy method to compute qth percentile. 

4439 

4440 Examples 

4441 -------- 

4442 >>> df = pd.DataFrame( 

4443 ... [["a", 1], ["a", 2], ["a", 3], ["b", 1], ["b", 3], ["b", 5]], 

4444 ... columns=["key", "val"], 

4445 ... ) 

4446 >>> df.groupby("key").quantile() 

4447 val 

4448 key 

4449 a 2.0 

4450 b 3.0 

4451 """ 

4452 mgr = self._get_data_to_aggregate(numeric_only=numeric_only, name="quantile") 

4453 obj = self._wrap_agged_manager(mgr) 

4454 splitter = self._grouper._get_splitter(obj) 

4455 sdata = splitter._sorted_data 

4456 

4457 starts, ends = lib.generate_slices(splitter._slabels, splitter.ngroups) 

4458 

4459 if is_scalar(q): 

4460 qs = np.array([q], dtype=np.float64) 

4461 pass_qs: np.ndarray | None = None 

4462 else: 

4463 qs = np.asarray(q, dtype=np.float64) 

4464 pass_qs = qs 

4465 

4466 ids = self._grouper.ids 

4467 ngroups = self._grouper.ngroups 

4468 if self.dropna: 

4469 # splitter drops NA groups, we need to do the same 

4470 ids = ids[ids >= 0] 

4471 nqs = len(qs) 

4472 

4473 func = partial( 

4474 libgroupby.group_quantile, 

4475 labels=ids, 

4476 qs=qs, 

4477 interpolation=interpolation, 

4478 starts=starts, 

4479 ends=ends, 

4480 ) 

4481 

4482 def blk_func(values: ArrayLike) -> ArrayLike: 

4483 if isinstance(values, ExtensionArray): 

4484 return values._groupby_quantile( 

4485 qs=qs, 

4486 interpolation=interpolation, 

4487 ids=ids, 

4488 ngroups=ngroups, 

4489 starts=starts, 

4490 ends=ends, 

4491 ) 

4492 

4493 # ndarray path 

4494 if is_object_dtype(values.dtype): 

4495 raise TypeError( 

4496 f"dtype '{values.dtype}' does not support operation 'quantile'" 

4497 ) 

4498 if is_bool_dtype(values.dtype): 

4499 # GH#51424 remove to match Series/DataFrame behavior 

4500 raise TypeError("Cannot use quantile with bool dtype") 

4501 

4502 mask = isna(values) 

4503 inference: np.dtype | None = ( 

4504 np.dtype(np.int64) if is_integer_dtype(values.dtype) else None 

4505 ) 

4506 vals = np.asarray(values) 

4507 

4508 ncols = 1 

4509 if vals.ndim == 2: 

4510 ncols = vals.shape[0] 

4511 

4512 out = np.empty((ncols, ngroups, nqs), dtype=np.float64) 

4513 

4514 if vals.ndim == 1: 

4515 func( 

4516 out[0], 

4517 values=vals, 

4518 mask=mask, # type: ignore[arg-type] 

4519 result_mask=None, 

4520 is_datetimelike=False, 

4521 ) 

4522 else: 

4523 for i in range(ncols): 

4524 func( 

4525 out[i], 

4526 values=vals[i], 

4527 mask=mask[i], 

4528 result_mask=None, 

4529 is_datetimelike=False, 

4530 ) 

4531 

4532 if vals.ndim == 1: 

4533 out = out.ravel("K") # type: ignore[assignment] 

4534 else: 

4535 out = out.reshape(ncols, ngroups * nqs) # type: ignore[assignment] 

4536 

4537 if inference is not None and interpolation not in { 

4538 "linear", 

4539 "midpoint", 

4540 }: 

4541 out = out.astype(inference) 

4542 

4543 return out 

4544 

4545 res_mgr = sdata._mgr.grouped_reduce(blk_func) 

4546 

4547 res = self._wrap_agged_manager(res_mgr) 

4548 return self._wrap_aggregated_output(res, qs=pass_qs) 

4549 

4550 @final 

4551 def ngroup(self, ascending: bool = True): 

4552 """ 

4553 Number each group from 0 to the number of groups - 1. 

4554 

4555 This is the enumerative complement of cumcount. Note that the 

4556 numbers given to the groups match the order in which the groups 

4557 would be seen when iterating over the groupby object, not the 

4558 order they are first observed. 

4559 

4560 Groups with missing keys (where `pd.isna()` is True) will be labeled with `NaN` 

4561 and will be skipped from the count. 

4562 

4563 Parameters 

4564 ---------- 

4565 ascending : bool, default True 

4566 If False, number in reverse, from number of group - 1 to 0. 

4567 

4568 Returns 

4569 ------- 

4570 Series 

4571 Unique numbers for each group. 

4572 

4573 See Also 

4574 -------- 

4575 .cumcount : Number the rows in each group. 

4576 

4577 Examples 

4578 -------- 

4579 >>> df = pd.DataFrame({"color": ["red", None, "red", "blue", "blue", "red"]}) 

4580 >>> df 

4581 color 

4582 0 red 

4583 1 NaN 

4584 2 red 

4585 3 blue 

4586 4 blue 

4587 5 red 

4588 >>> df.groupby("color").ngroup() 

4589 0 1.0 

4590 1 NaN 

4591 2 1.0 

4592 3 0.0 

4593 4 0.0 

4594 5 1.0 

4595 dtype: float64 

4596 >>> df.groupby("color", dropna=False).ngroup() 

4597 0 1 

4598 1 2 

4599 2 1 

4600 3 0 

4601 4 0 

4602 5 1 

4603 dtype: int64 

4604 >>> df.groupby("color", dropna=False).ngroup(ascending=False) 

4605 0 1 

4606 1 0 

4607 2 1 

4608 3 2 

4609 4 2 

4610 5 1 

4611 dtype: int64 

4612 """ 

4613 obj = self._obj_with_exclusions 

4614 index = obj.index 

4615 comp_ids = self._grouper.ids 

4616 

4617 dtype: type 

4618 if self._grouper.has_dropped_na: 

4619 comp_ids = np.where(comp_ids == -1, np.nan, comp_ids) 

4620 dtype = np.float64 

4621 else: 

4622 dtype = np.int64 

4623 

4624 if any(ping._passed_categorical for ping in self._grouper.groupings): 

4625 # comp_ids reflect non-observed groups, we need only observed 

4626 comp_ids = rank_1d(comp_ids, ties_method="dense") - 1 

4627 

4628 result = self._obj_1d_constructor(comp_ids, index, dtype=dtype) 

4629 if not ascending: 

4630 result = self.ngroups - 1 - result 

4631 return result 

4632 

4633 @final 

4634 def cumcount(self, ascending: bool = True): 

4635 """ 

4636 Number each item in each group from 0 to the length of that group - 1. 

4637 

4638 Essentially this is equivalent to 

4639 

4640 .. code-block:: python 

4641 

4642 self.apply(lambda x: pd.Series(np.arange(len(x)), x.index)) 

4643 

4644 Parameters 

4645 ---------- 

4646 ascending : bool, default True 

4647 If False, number in reverse, from length of group - 1 to 0. 

4648 

4649 Returns 

4650 ------- 

4651 Series 

4652 Sequence number of each element within each group. 

4653 

4654 See Also 

4655 -------- 

4656 .ngroup : Number the groups themselves. 

4657 

4658 Examples 

4659 -------- 

4660 >>> df = pd.DataFrame([["a"], ["a"], ["a"], ["b"], ["b"], ["a"]], columns=["A"]) 

4661 >>> df 

4662 A 

4663 0 a 

4664 1 a 

4665 2 a 

4666 3 b 

4667 4 b 

4668 5 a 

4669 >>> df.groupby("A").cumcount() 

4670 0 0 

4671 1 1 

4672 2 2 

4673 3 0 

4674 4 1 

4675 5 3 

4676 dtype: int64 

4677 >>> df.groupby("A").cumcount(ascending=False) 

4678 0 3 

4679 1 2 

4680 2 1 

4681 3 1 

4682 4 0 

4683 5 0 

4684 dtype: int64 

4685 """ 

4686 index = self._obj_with_exclusions.index 

4687 cumcounts = self._cumcount_array(ascending=ascending) 

4688 return self._obj_1d_constructor(cumcounts, index) 

4689 

4690 @final 

4691 def rank( 

4692 self, 

4693 method: RankMethod = "average", 

4694 ascending: bool = True, 

4695 na_option: RankNaOption = "keep", 

4696 pct: bool = False, 

4697 ) -> NDFrameT: 

4698 """ 

4699 Provide the rank of values within each group. 

4700 

4701 This method assigns ranks to values within each group, with options 

4702 for handling duplicate values, NaN values, and computing percentage ranks. 

4703 

4704 Parameters 

4705 ---------- 

4706 method : {'average', 'min', 'max', 'first', 'dense'}, default 'average' 

4707 * average: average rank of group. 

4708 * min: lowest rank in group. 

4709 * max: highest rank in group. 

4710 * first: ranks assigned in order they appear in the array. 

4711 * dense: like 'min', but rank always increases by 1 between groups. 

4712 ascending : bool, default True 

4713 False for ranks by high (1) to low (N). 

4714 na_option : {'keep', 'top', 'bottom'}, default 'keep' 

4715 * keep: leave NA values where they are. 

4716 * top: smallest rank if ascending. 

4717 * bottom: smallest rank if descending. 

4718 pct : bool, default False 

4719 Compute percentage rank of data within each group. 

4720 

4721 Returns 

4722 ------- 

4723 DataFrame 

4724 The ranking of values within each group. 

4725 

4726 See Also 

4727 -------- 

4728 Series.rank : Apply function rank to a Series. 

4729 DataFrame.rank : Apply function rank to each row or column of a DataFrame. 

4730 

4731 Examples 

4732 -------- 

4733 >>> df = pd.DataFrame( 

4734 ... { 

4735 ... "group": ["a", "a", "a", "a", "a", "b", "b", "b", "b", "b"], 

4736 ... "value": [2, 4, 2, 3, 5, 1, 2, 4, 1, 5], 

4737 ... } 

4738 ... ) 

4739 >>> df 

4740 group value 

4741 0 a 2 

4742 1 a 4 

4743 2 a 2 

4744 3 a 3 

4745 4 a 5 

4746 5 b 1 

4747 6 b 2 

4748 7 b 4 

4749 8 b 1 

4750 9 b 5 

4751 >>> for method in ["average", "min", "max", "dense", "first"]: 

4752 ... df[f"{method}_rank"] = df.groupby("group")["value"].rank(method) 

4753 >>> df 

4754 group value average_rank min_rank max_rank dense_rank first_rank 

4755 0 a 2 1.5 1.0 2.0 1.0 1.0 

4756 1 a 4 4.0 4.0 4.0 3.0 4.0 

4757 2 a 2 1.5 1.0 2.0 1.0 2.0 

4758 3 a 3 3.0 3.0 3.0 2.0 3.0 

4759 4 a 5 5.0 5.0 5.0 4.0 5.0 

4760 5 b 1 1.5 1.0 2.0 1.0 1.0 

4761 6 b 2 3.0 3.0 3.0 2.0 3.0 

4762 7 b 4 4.0 4.0 4.0 3.0 4.0 

4763 8 b 1 1.5 1.0 2.0 1.0 2.0 

4764 9 b 5 5.0 5.0 5.0 4.0 5.0 

4765 """ 

4766 if na_option not in {"keep", "top", "bottom"}: 

4767 msg = "na_option must be one of 'keep', 'top', or 'bottom'" 

4768 raise ValueError(msg) 

4769 

4770 kwargs = { 

4771 "ties_method": method, 

4772 "ascending": ascending, 

4773 "na_option": na_option, 

4774 "pct": pct, 

4775 } 

4776 

4777 return self._cython_transform( 

4778 "rank", 

4779 numeric_only=False, 

4780 **kwargs, 

4781 ) 

4782 

4783 @final 

4784 def cumprod( 

4785 self, 

4786 numeric_only: bool = False, 

4787 skipna: bool = True, 

4788 *args, 

4789 **kwargs, 

4790 ) -> NDFrameT: 

4791 """ 

4792 Cumulative product for each group. 

4793 

4794 Returns a Series or DataFrame of the same size with the cumulative 

4795 product computed within each group. 

4796 

4797 Parameters 

4798 ---------- 

4799 numeric_only : bool, default False 

4800 Include only float, int, boolean columns. 

4801 skipna : bool, default True 

4802 Exclude NA/null values. If an entire row/column is NA, the result 

4803 will be NA. 

4804 *args : tuple 

4805 Positional arguments to be passed to `func`. 

4806 

4807 .. deprecated:: 3.1.0 

4808 Passing ``*args`` to GroupBy.cumprod is deprecated 

4809 and will be removed in a future version of pandas. 

4810 

4811 **kwargs : dict 

4812 Additional keyword arguments to be passed to the function. 

4813 

4814 .. deprecated:: 3.1.0 

4815 Passing ``**kwargs`` to GroupBy.cumprod is deprecated 

4816 and will be removed in a future version of pandas. 

4817 

4818 Returns 

4819 ------- 

4820 Series or DataFrame 

4821 Cumulative product for each group. Same object type as the caller. 

4822 

4823 See Also 

4824 -------- 

4825 Series.cumprod : Apply function cumprod to a Series. 

4826 DataFrame.cumprod : Apply function cumprod to each row or column of a DataFrame. 

4827 

4828 Examples 

4829 -------- 

4830 For SeriesGroupBy: 

4831 

4832 >>> lst = ["a", "a", "b"] 

4833 >>> ser = pd.Series([6, 2, 0], index=lst) 

4834 >>> ser 

4835 a 6 

4836 a 2 

4837 b 0 

4838 dtype: int64 

4839 >>> ser.groupby(level=0).cumprod() 

4840 a 6 

4841 a 12 

4842 b 0 

4843 dtype: int64 

4844 

4845 For DataFrameGroupBy: 

4846 

4847 >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] 

4848 >>> df = pd.DataFrame( 

4849 ... data, columns=["a", "b", "c"], index=["cow", "horse", "bull"] 

4850 ... ) 

4851 >>> df 

4852 a b c 

4853 cow 1 8 2 

4854 horse 1 2 5 

4855 bull 2 6 9 

4856 >>> df.groupby("a").groups 

4857 {1: ['cow', 'horse'], 2: ['bull']} 

4858 >>> df.groupby("a").cumprod() 

4859 b c 

4860 cow 8 2 

4861 horse 16 10 

4862 bull 6 9 

4863 """ 

4864 nv.validate_groupby_func("cumprod", args, kwargs, ["skipna"]) 

4865 if args or kwargs: 

4866 warnings.warn( 

4867 "Passing additional arguments to GroupBy.cumprod is deprecated " 

4868 "and will be removed in a future version of pandas.", 

4869 Pandas4Warning, 

4870 stacklevel=find_stack_level(), 

4871 ) 

4872 return self._cython_transform("cumprod", numeric_only, skipna=skipna) 

4873 

4874 @final 

4875 def cumsum( 

4876 self, 

4877 numeric_only: bool = False, 

4878 skipna: bool = True, 

4879 *args, 

4880 **kwargs, 

4881 ) -> NDFrameT: 

4882 """ 

4883 Cumulative sum for each group. 

4884 

4885 Returns a Series or DataFrame of the same size with the cumulative 

4886 sum computed within each group. 

4887 

4888 Parameters 

4889 ---------- 

4890 numeric_only : bool, default False 

4891 Include only float, int, boolean columns. 

4892 skipna : bool, default True 

4893 Exclude NA/null values. If an entire row/column is NA, the result 

4894 will be NA. 

4895 *args : tuple 

4896 Positional arguments to be passed to `func`. 

4897 

4898 .. deprecated:: 3.1.0 

4899 Passing ``*args`` to GroupBy.cumsum is deprecated 

4900 and will be removed in a future version of pandas. 

4901 

4902 **kwargs : dict 

4903 Additional keyword arguments to be passed to the function. 

4904 

4905 .. deprecated:: 3.1.0 

4906 Passing ``**kwargs`` to GroupBy.cumsum is deprecated 

4907 and will be removed in a future version of pandas. 

4908 

4909 Returns 

4910 ------- 

4911 Series or DataFrame 

4912 Cumulative sum for each group. Same object type as the caller. 

4913 

4914 See Also 

4915 -------- 

4916 Series.cumsum : Apply function cumsum to a Series. 

4917 DataFrame.cumsum : Apply function cumsum to each row or column of a DataFrame. 

4918 

4919 Examples 

4920 -------- 

4921 For SeriesGroupBy: 

4922 

4923 >>> lst = ["a", "a", "b"] 

4924 >>> ser = pd.Series([6, 2, 0], index=lst) 

4925 >>> ser 

4926 a 6 

4927 a 2 

4928 b 0 

4929 dtype: int64 

4930 >>> ser.groupby(level=0).cumsum() 

4931 a 6 

4932 a 8 

4933 b 0 

4934 dtype: int64 

4935 

4936 For DataFrameGroupBy: 

4937 

4938 >>> data = [[1, 8, 2], [1, 2, 5], [2, 6, 9]] 

4939 >>> df = pd.DataFrame( 

4940 ... data, columns=["a", "b", "c"], index=["fox", "gorilla", "lion"] 

4941 ... ) 

4942 >>> df 

4943 a b c 

4944 fox 1 8 2 

4945 gorilla 1 2 5 

4946 lion 2 6 9 

4947 >>> df.groupby("a").groups 

4948 {1: ['fox', 'gorilla'], 2: ['lion']} 

4949 >>> df.groupby("a").cumsum() 

4950 b c 

4951 fox 8 2 

4952 gorilla 10 7 

4953 lion 6 9 

4954 """ 

4955 nv.validate_groupby_func("cumsum", args, kwargs, ["skipna"]) 

4956 if args or kwargs: 

4957 warnings.warn( 

4958 "Passing additional arguments to GroupBy.cumsum is deprecated " 

4959 "and will be removed in a future version of pandas.", 

4960 Pandas4Warning, 

4961 stacklevel=find_stack_level(), 

4962 ) 

4963 return self._cython_transform("cumsum", numeric_only, skipna=skipna) 

4964 

4965 @final 

4966 def cummin( 

4967 self, 

4968 numeric_only: bool = False, 

4969 skipna: bool = True, 

4970 **kwargs, 

4971 ) -> NDFrameT: 

4972 """ 

4973 Cumulative min for each group. 

4974 

4975 Returns a same-sized object where each value is replaced by the 

4976 minimum of all preceding values in its group. 

4977 

4978 Parameters 

4979 ---------- 

4980 numeric_only : bool, default False 

4981 Include only `float`, `int` or `boolean` data. 

4982 skipna : bool, default True 

4983 Exclude NA/null values. If an entire row/column is NA, the result 

4984 will be NA. 

4985 **kwargs : dict, optional 

4986 Additional keyword arguments to be passed to the function. 

4987 

4988 .. deprecated:: 3.1.0 

4989 Passing ``**kwargs`` to GroupBy.cummin is deprecated 

4990 and will be removed in a future version of pandas. 

4991 

4992 Returns 

4993 ------- 

4994 Series or DataFrame 

4995 Cumulative min for each group. Same object type as the caller. 

4996 

4997 See Also 

4998 -------- 

4999 Series.cummin : Apply function cummin to a Series. 

5000 DataFrame.cummin : Apply function cummin to each row or column of a DataFrame. 

5001 

5002 Examples 

5003 -------- 

5004 For SeriesGroupBy: 

5005 

5006 >>> lst = ["a", "a", "a", "b", "b", "b"] 

5007 >>> ser = pd.Series([1, 6, 2, 3, 0, 4], index=lst) 

5008 >>> ser 

5009 a 1 

5010 a 6 

5011 a 2 

5012 b 3 

5013 b 0 

5014 b 4 

5015 dtype: int64 

5016 >>> ser.groupby(level=0).cummin() 

5017 a 1 

5018 a 1 

5019 a 1 

5020 b 3 

5021 b 0 

5022 b 0 

5023 dtype: int64 

5024 

5025 For DataFrameGroupBy: 

5026 

5027 >>> data = [[1, 0, 2], [1, 1, 5], [6, 6, 9]] 

5028 >>> df = pd.DataFrame( 

5029 ... data, columns=["a", "b", "c"], index=["snake", "rabbit", "turtle"] 

5030 ... ) 

5031 >>> df 

5032 a b c 

5033 snake 1 0 2 

5034 rabbit 1 1 5 

5035 turtle 6 6 9 

5036 >>> df.groupby("a").groups 

5037 {1: ['snake', 'rabbit'], 6: ['turtle']} 

5038 >>> df.groupby("a").cummin() 

5039 b c 

5040 snake 0 2 

5041 rabbit 0 2 

5042 turtle 6 9 

5043 """ 

5044 if kwargs: 

5045 warnings.warn( 

5046 "Passing additional arguments to GroupBy.cummin is deprecated " 

5047 "and will be removed in a future version of pandas.", 

5048 Pandas4Warning, 

5049 stacklevel=find_stack_level(), 

5050 ) 

5051 return self._cython_transform( 

5052 "cummin", numeric_only=numeric_only, skipna=skipna 

5053 ) 

5054 

5055 @final 

5056 def cummax( 

5057 self, 

5058 numeric_only: bool = False, 

5059 skipna: bool = True, 

5060 **kwargs, 

5061 ) -> NDFrameT: 

5062 """ 

5063 Cumulative max for each group. 

5064 

5065 Returns the cumulative maximum of values within each group. The result 

5066 has the same size as the input, with each element representing the 

5067 maximum of all preceding elements (including itself) within its group. 

5068 

5069 Parameters 

5070 ---------- 

5071 numeric_only : bool, default False 

5072 Include only `float`, `int` or `boolean` data. 

5073 skipna : bool, default True 

5074 Exclude NA/null values. If an entire row/column is NA, the result 

5075 will be NA. 

5076 **kwargs : dict, optional 

5077 Additional keyword arguments to be passed to the function. 

5078 

5079 .. deprecated:: 3.1.0 

5080 Passing ``**kwargs`` to GroupBy.cummax is deprecated 

5081 and will be removed in a future version of pandas. 

5082 

5083 Returns 

5084 ------- 

5085 Series or DataFrame 

5086 Cumulative max for each group. Same object type as the caller. 

5087 

5088 See Also 

5089 -------- 

5090 Series.cummax : Apply function cummax to a Series. 

5091 DataFrame.cummax : Apply function cummax to each row or column of a DataFrame. 

5092 

5093 Examples 

5094 -------- 

5095 For SeriesGroupBy: 

5096 

5097 >>> lst = ["a", "a", "a", "b", "b", "b"] 

5098 >>> ser = pd.Series([1, 6, 2, 3, 1, 4], index=lst) 

5099 >>> ser 

5100 a 1 

5101 a 6 

5102 a 2 

5103 b 3 

5104 b 1 

5105 b 4 

5106 dtype: int64 

5107 >>> ser.groupby(level=0).cummax() 

5108 a 1 

5109 a 6 

5110 a 6 

5111 b 3 

5112 b 3 

5113 b 4 

5114 dtype: int64 

5115 

5116 For DataFrameGroupBy: 

5117 

5118 >>> data = [[1, 8, 2], [1, 1, 0], [2, 6, 9]] 

5119 >>> df = pd.DataFrame( 

5120 ... data, columns=["a", "b", "c"], index=["cow", "horse", "bull"] 

5121 ... ) 

5122 >>> df 

5123 a b c 

5124 cow 1 8 2 

5125 horse 1 1 0 

5126 bull 2 6 9 

5127 >>> df.groupby("a").groups 

5128 {1: ['cow', 'horse'], 2: ['bull']} 

5129 >>> df.groupby("a").cummax() 

5130 b c 

5131 cow 8 2 

5132 horse 8 2 

5133 bull 6 9 

5134 """ 

5135 if kwargs: 

5136 warnings.warn( 

5137 "Passing additional arguments to GroupBy.cummax is deprecated " 

5138 "and will be removed in a future version of pandas.", 

5139 Pandas4Warning, 

5140 stacklevel=find_stack_level(), 

5141 ) 

5142 return self._cython_transform( 

5143 "cummax", numeric_only=numeric_only, skipna=skipna 

5144 ) 

5145 

5146 @final 

5147 def shift( 

5148 self, 

5149 periods: int | Sequence[int] = 1, 

5150 freq=None, 

5151 fill_value=lib.no_default, 

5152 suffix: str | None = None, 

5153 ): 

5154 """ 

5155 Shift each group by periods observations. 

5156 

5157 If freq is passed, the index will be increased using the periods and the freq. 

5158 

5159 Parameters 

5160 ---------- 

5161 periods : int | Sequence[int], default 1 

5162 Number of periods to shift. If a list of values, shift each group by 

5163 each period. 

5164 freq : str, optional 

5165 Frequency string. 

5166 fill_value : optional 

5167 The scalar value to use for newly introduced missing values. 

5168 

5169 .. versionchanged:: 2.1.0 

5170 Will raise a ``ValueError`` if ``freq`` is provided too. 

5171 

5172 suffix : str, optional 

5173 A string to add to each shifted column if there are multiple periods. 

5174 Ignored otherwise. 

5175 

5176 Returns 

5177 ------- 

5178 Series or DataFrame 

5179 Object shifted within each group. 

5180 

5181 See Also 

5182 -------- 

5183 Index.shift : Shift values of Index. 

5184 

5185 Examples 

5186 -------- 

5187 

5188 For SeriesGroupBy: 

5189 

5190 >>> lst = ["a", "a", "b", "b"] 

5191 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

5192 >>> ser 

5193 a 1 

5194 a 2 

5195 b 3 

5196 b 4 

5197 dtype: int64 

5198 >>> ser.groupby(level=0).shift(1) 

5199 a NaN 

5200 a 1.0 

5201 b NaN 

5202 b 3.0 

5203 dtype: float64 

5204 

5205 For DataFrameGroupBy: 

5206 

5207 >>> data = [[1, 2, 3], [1, 5, 6], [2, 5, 8], [2, 6, 9]] 

5208 >>> df = pd.DataFrame( 

5209 ... data, 

5210 ... columns=["a", "b", "c"], 

5211 ... index=["tuna", "salmon", "catfish", "goldfish"], 

5212 ... ) 

5213 >>> df 

5214 a b c 

5215 tuna 1 2 3 

5216 salmon 1 5 6 

5217 catfish 2 5 8 

5218 goldfish 2 6 9 

5219 >>> df.groupby("a").shift(1) 

5220 b c 

5221 tuna NaN NaN 

5222 salmon 2.0 3.0 

5223 catfish NaN NaN 

5224 goldfish 5.0 8.0 

5225 """ 

5226 if is_list_like(periods): 

5227 periods = cast("Sequence", periods) 

5228 if len(periods) == 0: 

5229 raise ValueError("If `periods` is an iterable, it cannot be empty.") 

5230 from pandas.core.reshape.concat import concat 

5231 

5232 add_suffix = True 

5233 else: 

5234 if not is_integer(periods): 

5235 raise TypeError( 

5236 f"Periods must be integer, but {periods} is {type(periods)}." 

5237 ) 

5238 if suffix: 

5239 raise ValueError("Cannot specify `suffix` if `periods` is an int.") 

5240 periods = [cast("int", periods)] 

5241 add_suffix = False 

5242 

5243 shifted_dataframes = [] 

5244 for period in periods: 

5245 if not is_integer(period): 

5246 raise TypeError( 

5247 f"Periods must be integer, but {period} is {type(period)}." 

5248 ) 

5249 if freq is not None: 

5250 f = lambda x: x.shift( 

5251 period, 

5252 freq, 

5253 0, # axis 

5254 fill_value, 

5255 ) 

5256 shifted = self._python_apply_general( 

5257 f, self._selected_obj, is_transform=True 

5258 ) 

5259 else: 

5260 if fill_value is lib.no_default: 

5261 fill_value = None 

5262 ids = self._grouper.ids 

5263 ngroups = self._grouper.ngroups 

5264 res_indexer = np.zeros(len(ids), dtype=np.int64) 

5265 

5266 libgroupby.group_shift_indexer(res_indexer, ids, ngroups, period) 

5267 

5268 obj = self._obj_with_exclusions 

5269 

5270 shifted = obj._reindex_with_indexers( 

5271 {0: (obj.index, res_indexer)}, 

5272 fill_value=fill_value, 

5273 allow_dups=True, 

5274 ) 

5275 

5276 if add_suffix: 

5277 if isinstance(shifted, Series): 

5278 shifted = cast("NDFrameT", shifted.to_frame()) 

5279 shifted = shifted.add_suffix( 

5280 f"{suffix}_{period}" if suffix else f"_{period}" 

5281 ) 

5282 shifted_dataframes.append(cast("Series | DataFrame", shifted)) 

5283 

5284 return ( 

5285 shifted_dataframes[0] 

5286 if len(shifted_dataframes) == 1 

5287 else concat(shifted_dataframes, axis=1, sort=False) 

5288 ) 

5289 

5290 @final 

5291 def diff( 

5292 self, 

5293 periods: int = 1, 

5294 ) -> NDFrameT: 

5295 """ 

5296 First discrete difference of element. 

5297 

5298 Calculates the difference of each element compared with another 

5299 element in the group (default is element in previous row). 

5300 

5301 Parameters 

5302 ---------- 

5303 periods : int, default 1 

5304 Periods to shift for calculating difference, accepts negative values. 

5305 

5306 Returns 

5307 ------- 

5308 Series or DataFrame 

5309 First differences. 

5310 

5311 See Also 

5312 -------- 

5313 Series.diff : Apply function diff to a Series. 

5314 DataFrame.diff : Apply function diff to each row or column of a DataFrame. 

5315 

5316 Examples 

5317 -------- 

5318 For SeriesGroupBy: 

5319 

5320 >>> lst = ["a", "a", "a", "b", "b", "b"] 

5321 >>> ser = pd.Series([7, 2, 8, 4, 3, 3], index=lst) 

5322 >>> ser 

5323 a 7 

5324 a 2 

5325 a 8 

5326 b 4 

5327 b 3 

5328 b 3 

5329 dtype: int64 

5330 >>> ser.groupby(level=0).diff() 

5331 a NaN 

5332 a -5.0 

5333 a 6.0 

5334 b NaN 

5335 b -1.0 

5336 b 0.0 

5337 dtype: float64 

5338 

5339 For DataFrameGroupBy: 

5340 

5341 >>> data = {"a": [1, 3, 5, 7, 7, 8, 3], "b": [1, 4, 8, 4, 4, 2, 1]} 

5342 >>> df = pd.DataFrame( 

5343 ... data, index=["dog", "dog", "dog", "mouse", "mouse", "mouse", "mouse"] 

5344 ... ) 

5345 >>> df 

5346 a b 

5347 dog 1 1 

5348 dog 3 4 

5349 dog 5 8 

5350 mouse 7 4 

5351 mouse 7 4 

5352 mouse 8 2 

5353 mouse 3 1 

5354 >>> df.groupby(level=0).diff() 

5355 a b 

5356 dog NaN NaN 

5357 dog 2.0 3.0 

5358 dog 2.0 4.0 

5359 mouse NaN NaN 

5360 mouse 0.0 0.0 

5361 mouse 1.0 -2.0 

5362 mouse -5.0 -1.0 

5363 """ 

5364 obj = self._obj_with_exclusions 

5365 shifted = self.shift(periods=periods) 

5366 

5367 # GH45562 - to retain existing behavior and match behavior of Series.diff(), 

5368 # int8 and int16 are coerced to float32 rather than float64. 

5369 dtypes_to_f32 = ["int8", "int16"] 

5370 if obj.ndim == 1: 

5371 if obj.dtype in dtypes_to_f32: 

5372 shifted = shifted.astype("float32") 

5373 else: 

5374 to_coerce = [c for c, dtype in obj.dtypes.items() if dtype in dtypes_to_f32] 

5375 if to_coerce: 

5376 shifted = shifted.astype(dict.fromkeys(to_coerce, "float32")) 

5377 

5378 return obj - shifted 

5379 

5380 @final 

5381 def pct_change( 

5382 self, 

5383 periods: int = 1, 

5384 fill_method: None = None, 

5385 freq=None, 

5386 ): 

5387 """ 

5388 Calculate pct_change of each value to previous entry in group. 

5389 

5390 This method calculates the percentage change between the current and 

5391 a prior element within each group, useful for computing growth rates. 

5392 

5393 Parameters 

5394 ---------- 

5395 periods : int, default 1 

5396 Periods to shift for calculating percentage change. Comparing with 

5397 a period of 1 means adjacent elements are compared, whereas a period 

5398 of 2 compares every other element. 

5399 

5400 fill_method : None 

5401 Must be None. This argument will be removed in a future version of pandas. 

5402 

5403 freq : str, pandas offset object, or None, default None 

5404 The frequency increment for time series data (e.g., 'M' for month-end). 

5405 If None, the frequency is inferred from the index. Relevant for time 

5406 series data only. 

5407 

5408 Returns 

5409 ------- 

5410 Series or DataFrame 

5411 Percentage changes within each group. 

5412 

5413 See Also 

5414 -------- 

5415 Series.pct_change : Apply function pct_change to a Series. 

5416 DataFrame.pct_change : Apply function pct_change to each row or column of 

5417 a DataFrame. 

5418 

5419 Examples 

5420 -------- 

5421 

5422 For SeriesGroupBy: 

5423 

5424 >>> lst = ["a", "a", "b", "b"] 

5425 >>> ser = pd.Series([1, 2, 3, 4], index=lst) 

5426 >>> ser 

5427 a 1 

5428 a 2 

5429 b 3 

5430 b 4 

5431 dtype: int64 

5432 >>> ser.groupby(level=0).pct_change() 

5433 a NaN 

5434 a 1.000000 

5435 b NaN 

5436 b 0.333333 

5437 dtype: float64 

5438 

5439 For DataFrameGroupBy: 

5440 

5441 >>> data = [[1, 2, 3], [1, 5, 6], [2, 5, 8], [2, 6, 9]] 

5442 >>> df = pd.DataFrame( 

5443 ... data, 

5444 ... columns=["a", "b", "c"], 

5445 ... index=["tuna", "salmon", "catfish", "goldfish"], 

5446 ... ) 

5447 >>> df 

5448 a b c 

5449 tuna 1 2 3 

5450 salmon 1 5 6 

5451 catfish 2 5 8 

5452 goldfish 2 6 9 

5453 >>> df.groupby("a").pct_change() 

5454 b c 

5455 tuna NaN NaN 

5456 salmon 1.5 1.000 

5457 catfish NaN NaN 

5458 goldfish 0.2 0.125 

5459 """ 

5460 # GH#53491 

5461 if fill_method is not None: 

5462 raise ValueError(f"fill_method must be None; got {fill_method=}.") 

5463 

5464 # TODO(GH#23918): Remove this conditional for SeriesGroupBy when 

5465 # GH#23918 is fixed 

5466 if freq is not None: 

5467 f = lambda x: x.pct_change( 

5468 periods=periods, 

5469 freq=freq, 

5470 axis=0, 

5471 ) 

5472 return self._python_apply_general(f, self._selected_obj, is_transform=True) 

5473 

5474 # fill_method is None (validated above); GH30463 

5475 filled = self.ffill(limit=0) 

5476 fill_grp = filled.groupby(self._grouper.codes, group_keys=self.group_keys) 

5477 shifted = fill_grp.shift(periods=periods, freq=freq) 

5478 return (filled / shifted) - 1 

5479 

5480 @final 

5481 def head(self, n: int = 5) -> NDFrameT: 

5482 """ 

5483 Return first n rows of each group. 

5484 

5485 Similar to ``.apply(lambda x: x.head(n))``, but it returns a subset of rows 

5486 from the original DataFrame with original index and order preserved 

5487 (``as_index`` flag is ignored). 

5488 

5489 Parameters 

5490 ---------- 

5491 n : int 

5492 If positive: number of entries to include from start of each group. 

5493 If negative: number of entries to exclude from end of each group. 

5494 

5495 Returns 

5496 ------- 

5497 Series or DataFrame 

5498 Subset of original Series or DataFrame as determined by n. 

5499 

5500 See Also 

5501 -------- 

5502 Series.head : Apply function head to a Series. 

5503 DataFrame.head : Apply function head to each row or column of a DataFrame. 

5504 

5505 Examples 

5506 -------- 

5507 

5508 >>> df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"]) 

5509 >>> df.groupby("A").head(1) 

5510 A B 

5511 0 1 2 

5512 2 5 6 

5513 >>> df.groupby("A").head(-1) 

5514 A B 

5515 0 1 2 

5516 """ 

5517 mask = self._make_mask_from_positional_indexer(slice(None, n)) 

5518 return self._mask_selected_obj(mask) 

5519 

5520 @final 

5521 def tail(self, n: int = 5) -> NDFrameT: 

5522 """ 

5523 Return last n rows of each group. 

5524 

5525 Similar to ``.apply(lambda x: x.tail(n))``, but it returns a subset of rows 

5526 from the original DataFrame with original index and order preserved 

5527 (``as_index`` flag is ignored). 

5528 

5529 Parameters 

5530 ---------- 

5531 n : int 

5532 If positive: number of entries to include from end of each group. 

5533 If negative: number of entries to exclude from start of each group. 

5534 

5535 Returns 

5536 ------- 

5537 Series or DataFrame 

5538 Subset of original Series or DataFrame as determined by n. 

5539 

5540 See Also 

5541 -------- 

5542 Series.tail : Apply function tail to a Series. 

5543 DataFrame.tail : Apply function tail to each row or column of a DataFrame. 

5544 

5545 Examples 

5546 -------- 

5547 

5548 >>> df = pd.DataFrame( 

5549 ... [["a", 1], ["a", 2], ["b", 1], ["b", 2]], columns=["A", "B"] 

5550 ... ) 

5551 >>> df.groupby("A").tail(1) 

5552 A B 

5553 1 a 2 

5554 3 b 2 

5555 >>> df.groupby("A").tail(-1) 

5556 A B 

5557 1 a 2 

5558 3 b 2 

5559 """ 

5560 if n: 

5561 mask = self._make_mask_from_positional_indexer(slice(-n, None)) 

5562 else: 

5563 mask = self._make_mask_from_positional_indexer([]) 

5564 

5565 return self._mask_selected_obj(mask) 

5566 

5567 @final 

5568 def _mask_selected_obj(self, mask: npt.NDArray[np.bool_]) -> NDFrameT: 

5569 """ 

5570 Return _selected_obj with mask applied. 

5571 

5572 Parameters 

5573 ---------- 

5574 mask : np.ndarray[bool] 

5575 Boolean mask to apply. 

5576 

5577 Returns 

5578 ------- 

5579 Series or DataFrame 

5580 Filtered _selected_obj. 

5581 """ 

5582 ids = self._grouper.ids 

5583 mask = mask & (ids != -1) 

5584 return self._selected_obj[mask] 

5585 

5586 @final 

5587 def sample( 

5588 self, 

5589 n: int | None = None, 

5590 frac: float | None = None, 

5591 replace: bool = False, 

5592 weights: ListLike | None = None, 

5593 random_state: RandomState | None = None, 

5594 ): 

5595 """ 

5596 Return a random sample of items from each group. 

5597 

5598 You can use `random_state` for reproducibility. 

5599 

5600 Parameters 

5601 ---------- 

5602 n : int, optional 

5603 Number of items to return for each group. Cannot be used with 

5604 `frac` and must be no larger than the smallest group unless 

5605 `replace` is True. Default is one if `frac` is None. 

5606 frac : float, optional 

5607 Fraction of items to return. Cannot be used with `n`. 

5608 replace : bool, default False 

5609 Allow or disallow sampling of the same row more than once. 

5610 weights : list-like, optional 

5611 Default None results in equal probability weighting. 

5612 If passed a list-like then values must have the same length as 

5613 the underlying DataFrame or Series object and will be used as 

5614 sampling probabilities after normalization within each group. 

5615 Values must be non-negative with at least one positive element 

5616 within each group. 

5617 random_state : int, array-like, BitGenerator, np.random.RandomState, np.random.Generator, optional 

5618 If int, array-like, or BitGenerator, seed for random number generator. 

5619 If np.random.RandomState or np.random.Generator, use as given. 

5620 Default ``None`` results in sampling with the current state of np.random. 

5621 

5622 Returns 

5623 ------- 

5624 Series or DataFrame 

5625 A new object of same type as caller containing items randomly 

5626 sampled within each group from the caller object. 

5627 

5628 See Also 

5629 -------- 

5630 DataFrame.sample: Generate random samples from a DataFrame object. 

5631 Series.sample: Generate random samples from a Series object. 

5632 numpy.random.choice: Generate a random sample from a given 1-D numpy 

5633 array. 

5634 

5635 Examples 

5636 -------- 

5637 >>> df = pd.DataFrame( 

5638 ... {"a": ["red"] * 2 + ["blue"] * 2 + ["black"] * 2, "b": range(6)} 

5639 ... ) 

5640 >>> df 

5641 a b 

5642 0 red 0 

5643 1 red 1 

5644 2 blue 2 

5645 3 blue 3 

5646 4 black 4 

5647 5 black 5 

5648 

5649 Select one row at random for each distinct value in column a. The 

5650 `random_state` argument can be used to guarantee reproducibility: 

5651 

5652 >>> df.groupby("a").sample(n=1, random_state=1) 

5653 a b 

5654 4 black 4 

5655 2 blue 2 

5656 1 red 1 

5657 

5658 Set `frac` to sample fixed proportions rather than counts: 

5659 

5660 >>> df.groupby("a")["b"].sample(frac=0.5, random_state=2) 

5661 5 5 

5662 2 2 

5663 0 0 

5664 Name: b, dtype: int64 

5665 

5666 Control sample probabilities within groups by setting weights: 

5667 

5668 >>> df.groupby("a").sample( 

5669 ... n=1, 

5670 ... weights=[1, 1, 1, 0, 0, 1], 

5671 ... random_state=1, 

5672 ... ) 

5673 a b 

5674 5 black 5 

5675 2 blue 2 

5676 0 red 0 

5677 """ # noqa: E501 

5678 if self._selected_obj.empty: 

5679 # GH48459 prevent ValueError when object is empty 

5680 return self._selected_obj 

5681 size = sample.process_sampling_size(n, frac, replace) 

5682 if weights is not None: 

5683 weights_arr = sample.preprocess_weights(self._selected_obj, weights, axis=0) 

5684 

5685 random_state = com.random_state(random_state) 

5686 

5687 group_iterator = self._grouper.get_iterator(self._selected_obj) 

5688 

5689 sampled_indices = [] 

5690 for labels, obj in group_iterator: 

5691 grp_indices = self.indices[labels] 

5692 group_size = len(grp_indices) 

5693 if size is not None: 

5694 sample_size = size 

5695 else: 

5696 assert frac is not None 

5697 sample_size = round(frac * group_size) 

5698 

5699 grp_sample = sample.sample( 

5700 group_size, 

5701 size=sample_size, 

5702 replace=replace, 

5703 weights=None if weights is None else weights_arr[grp_indices], 

5704 random_state=random_state, 

5705 ) 

5706 sampled_indices.append(grp_indices[grp_sample]) 

5707 

5708 concatenated_sampled_indices = np.concatenate(sampled_indices) 

5709 return self._selected_obj.take(concatenated_sampled_indices, axis=0) 

5710 

5711 def _idxmax_idxmin( 

5712 self, 

5713 how: Literal["idxmax", "idxmin"], 

5714 ignore_unobserved: bool = False, 

5715 skipna: bool = True, 

5716 numeric_only: bool = False, 

5717 ) -> NDFrameT: 

5718 """Compute idxmax/idxmin. 

5719 

5720 Parameters 

5721 ---------- 

5722 how : {'idxmin', 'idxmax'} 

5723 Whether to compute idxmin or idxmax. 

5724 numeric_only : bool, default False 

5725 Include only float, int, boolean columns. 

5726 skipna : bool, default True 

5727 Exclude NA/null values. If an entire group is NA, the result will be NA. 

5728 ignore_unobserved : bool, default False 

5729 When True and an unobserved group is encountered, do not raise. This used 

5730 for transform where unobserved groups do not play an impact on the result. 

5731 

5732 Returns 

5733 ------- 

5734 Series or DataFrame 

5735 idxmax or idxmin for the groupby operation. 

5736 """ 

5737 if not self.observed and any( 

5738 ping._passed_categorical for ping in self._grouper.groupings 

5739 ): 

5740 expected_len = len(self._grouper.result_index) 

5741 # TODO: Better way to find # of observed groups? 

5742 group_sizes = self._grouper.size() 

5743 result_len = group_sizes[group_sizes > 0].shape[0] 

5744 assert result_len <= expected_len 

5745 has_unobserved = result_len < expected_len 

5746 

5747 raise_err: bool | np.bool_ = not ignore_unobserved and has_unobserved 

5748 # Only raise an error if there are columns to compute; otherwise we return 

5749 # an empty DataFrame with an index (possibly including unobserved) but no 

5750 # columns 

5751 data = self._obj_with_exclusions 

5752 if raise_err and isinstance(data, DataFrame): 

5753 if numeric_only: 

5754 data = data._get_numeric_data() 

5755 raise_err = len(data.columns) > 0 

5756 

5757 if raise_err: 

5758 raise ValueError( 

5759 f"Can't get {how} of an empty group due to unobserved categories. " 

5760 "Specify observed=True in groupby instead." 

5761 ) 

5762 elif not skipna and self._obj_with_exclusions.isna().any(axis=None): 

5763 raise ValueError(f"{how} with skipna=False encountered an NA value.") 

5764 

5765 result = self._cython_agg_general( 

5766 how, 

5767 numeric_only=numeric_only, 

5768 min_count=1, 

5769 skipna=skipna, 

5770 ) 

5771 return result 

5772 

5773 def _wrap_idxmax_idxmin( 

5774 self, res: NDFrameT, how: Literal["idxmax", "idxmin"], skipna: bool 

5775 ) -> NDFrameT: 

5776 index = self.obj.index 

5777 if res.size == 0: 

5778 result = res.astype(index.dtype) 

5779 elif skipna and res.lt(0).any(axis=None): 

5780 raise ValueError( 

5781 f"{how} with skipna=True encountered all NA values in a group." 

5782 ) 

5783 else: 

5784 if isinstance(index, MultiIndex): 

5785 index = index.to_flat_index() 

5786 values = res._values 

5787 assert isinstance(values, np.ndarray) 

5788 na_value = na_value_for_dtype(index.dtype, compat=False) 

5789 if isinstance(res, Series): 

5790 # mypy: expression has type "Series", variable has type "NDFrameT" 

5791 result = res._constructor( # type: ignore[assignment] 

5792 index.array.take(values, allow_fill=True, fill_value=na_value), 

5793 index=res.index, 

5794 name=res.name, 

5795 ) 

5796 else: 

5797 data = {} 

5798 for k, column_values in enumerate(values.T): 

5799 data[k] = index.array.take( 

5800 column_values, allow_fill=True, fill_value=na_value 

5801 ) 

5802 result = self.obj._constructor(data, index=res.index) 

5803 result.columns = res.columns 

5804 return result 

5805 

5806 

5807def get_groupby( 

5808 obj: NDFrame, 

5809 by: _KeysArgType | None = None, 

5810 grouper: ops.BaseGrouper | None = None, 

5811 group_keys: bool = True, 

5812) -> GroupBy: 

5813 """ 

5814 Class for grouping and aggregating relational data. 

5815 

5816 See aggregate, transform, and apply functions on this object. 

5817 

5818 It's easiest to use obj.groupby(...) to use GroupBy, but you can also do: 

5819 

5820 :: 

5821 

5822 grouped = groupby(obj, ...) 

5823 

5824 Parameters 

5825 ---------- 

5826 obj : pandas object 

5827 

5828 Returns 

5829 ------- 

5830 **Attributes** 

5831 groups : dict 

5832 {group name -> group labels} 

5833 len(grouped) : int 

5834 Number of groups 

5835 

5836 Notes 

5837 ----- 

5838 After grouping, see aggregate, apply, and transform functions. Here are 

5839 some other brief notes about usage. When grouping by multiple groups, the 

5840 result index will be a MultiIndex (hierarchical) by default. 

5841 

5842 Iteration produces (key, group) tuples, i.e. chunking the data by group. So 

5843 you can write code like: 

5844 

5845 :: 

5846 

5847 grouped = obj.groupby(keys) 

5848 for key, group in grouped: 

5849 # do something with the data 

5850 

5851 Function calls on GroupBy, if not specially implemented, "dispatch" to the 

5852 grouped data. So if you group a DataFrame and wish to invoke the std() 

5853 method on each group, you can simply do: 

5854 

5855 :: 

5856 

5857 df.groupby(mapper).std() 

5858 

5859 rather than 

5860 

5861 :: 

5862 

5863 df.groupby(mapper).aggregate(np.std) 

5864 

5865 You can pass arguments to these "wrapped" functions, too. 

5866 

5867 See the online documentation for full exposition on these topics and much 

5868 more 

5869 """ 

5870 if isinstance(obj, Series): 

5871 from pandas.core.groupby.generic import SeriesGroupBy 

5872 

5873 return SeriesGroupBy( 

5874 obj=obj, 

5875 keys=by, 

5876 grouper=grouper, 

5877 group_keys=group_keys, 

5878 ) 

5879 elif isinstance(obj, DataFrame): 

5880 from pandas.core.groupby.generic import DataFrameGroupBy 

5881 

5882 return DataFrameGroupBy( 

5883 obj=obj, 

5884 keys=by, 

5885 grouper=grouper, 

5886 group_keys=group_keys, 

5887 ) 

5888 else: # pragma: no cover 

5889 raise TypeError(f"invalid type: {obj}") 

5890 

5891 

5892def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiIndex: 

5893 """ 

5894 Insert the sequence 'qs' of quantiles as the inner-most level of a MultiIndex. 

5895 

5896 The quantile level in the MultiIndex is a repeated copy of 'qs'. 

5897 

5898 Parameters 

5899 ---------- 

5900 idx : Index 

5901 qs : np.ndarray[float64] 

5902 

5903 Returns 

5904 ------- 

5905 MultiIndex 

5906 """ 

5907 nqs = len(qs) 

5908 lev_codes, lev = Index(qs, copy=False).factorize() 

5909 lev_codes = coerce_indexer_dtype(lev_codes, lev) 

5910 

5911 if idx._is_multi: 

5912 idx = cast("MultiIndex", idx) 

5913 levels = [*idx.levels, lev] 

5914 codes = [np.repeat(x, nqs) for x in idx.codes] + [np.tile(lev_codes, len(idx))] 

5915 mi = MultiIndex(levels=levels, codes=codes, names=[*idx.names, None]) 

5916 else: 

5917 nidx = len(idx) 

5918 idx_codes = coerce_indexer_dtype(np.arange(nidx), idx) 

5919 levels = [idx, lev] 

5920 codes = [np.repeat(idx_codes, nqs), np.tile(lev_codes, nidx)] 

5921 mi = MultiIndex(levels=levels, codes=codes, names=[idx.name, None]) 

5922 

5923 return mi