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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

648 statements  

1""" 

2Define the SeriesGroupBy and DataFrameGroupBy 

3classes that hold the groupby interfaces (and some implementations). 

4 

5These are user facing as the result of the ``df.groupby(...)`` operations, 

6which here returns a DataFrameGroupBy object. 

7""" 

8 

9from __future__ import annotations 

10 

11from collections import abc 

12from collections.abc import Callable 

13import dataclasses 

14from functools import partial 

15from textwrap import dedent 

16from typing import ( 

17 TYPE_CHECKING, 

18 Any, 

19 Literal, 

20 TypeAlias, 

21 TypeVar, 

22 cast, 

23) 

24import warnings 

25 

26import numpy as np 

27 

28from pandas._libs import Interval 

29from pandas._libs.hashtable import duplicated 

30from pandas.errors import ( 

31 Pandas4Warning, 

32 SpecificationError, 

33) 

34from pandas.util._decorators import set_module 

35from pandas.util._exceptions import find_stack_level 

36 

37from pandas.core.dtypes.common import ( 

38 ensure_int64, 

39 is_bool, 

40 is_dict_like, 

41 is_integer_dtype, 

42 is_list_like, 

43 is_numeric_dtype, 

44 is_scalar, 

45) 

46from pandas.core.dtypes.dtypes import ( 

47 CategoricalDtype, 

48 IntervalDtype, 

49) 

50from pandas.core.dtypes.inference import is_hashable 

51from pandas.core.dtypes.missing import ( 

52 isna, 

53 notna, 

54) 

55 

56from pandas.core import algorithms 

57from pandas.core.apply import ( 

58 GroupByApply, 

59 maybe_mangle_lambdas, 

60 reconstruct_func, 

61 validate_func_kwargs, 

62) 

63import pandas.core.common as com 

64from pandas.core.frame import DataFrame 

65from pandas.core.groupby import base 

66from pandas.core.groupby.groupby import ( 

67 GroupBy, 

68 GroupByPlot, 

69) 

70from pandas.core.indexes.api import ( 

71 Index, 

72 MultiIndex, 

73 all_indexes_same, 

74 default_index, 

75) 

76from pandas.core.series import Series 

77from pandas.core.sorting import get_group_index 

78from pandas.core.util.numba_ import maybe_use_numba 

79 

80from pandas.plotting import boxplot_frame_groupby 

81 

82if TYPE_CHECKING: 

83 from collections.abc import ( 

84 Hashable, 

85 Sequence, 

86 ) 

87 

88 from pandas._typing import ( 

89 ArrayLike, 

90 BlockManager, 

91 CorrelationMethod, 

92 IndexLabel, 

93 Manager, 

94 SingleBlockManager, 

95 TakeIndexer, 

96 ) 

97 

98 from pandas import Categorical 

99 from pandas.core.generic import NDFrame 

100 

101# TODO(typing) the return value on this callable should be any *scalar*. 

102AggScalar: TypeAlias = str | Callable[..., Any] 

103# TODO: validate types on ScalarResult and move to _typing 

104# Blocked from using by https://github.com/python/mypy/issues/1484 

105# See note at _mangle_lambda_list 

106ScalarResult = TypeVar("ScalarResult") 

107 

108 

109@set_module("pandas") 

110@dataclasses.dataclass 

111class NamedAgg: 

112 """ 

113 Helper for column specific aggregation with control over output column names. 

114 

115 Parameters 

116 ---------- 

117 column : Hashable 

118 Column label in the DataFrame to apply aggfunc. 

119 aggfunc : function or str 

120 Function to apply to the provided column. If string, the name of a built-in 

121 pandas function. 

122 *args, **kwargs : Any 

123 Optional positional and keyword arguments passed to ``aggfunc``. 

124 

125 See Also 

126 -------- 

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

128 

129 Examples 

130 -------- 

131 >>> df = pd.DataFrame({"key": [1, 1, 2], "a": [-1, 0, 1], 1: [10, 11, 12]}) 

132 >>> agg_a = pd.NamedAgg(column="a", aggfunc="min") 

133 >>> agg_1 = pd.NamedAgg(column=1, aggfunc=lambda x: np.mean(x)) 

134 >>> df.groupby("key").agg(result_a=agg_a, result_1=agg_1) 

135 result_a result_1 

136 key 

137 1 -1 10.5 

138 2 1 12.0 

139 

140 >>> def n_between(ser, low, high, **kwargs): 

141 ... return ser.between(low, high, **kwargs).sum() 

142 

143 >>> agg_between = pd.NamedAgg("a", n_between, 0, 1) 

144 >>> df.groupby("key").agg(count_between=agg_between) 

145 count_between 

146 key 

147 1 1 

148 2 1 

149 

150 >>> agg_between_kw = pd.NamedAgg("a", n_between, 0, 1, inclusive="both") 

151 >>> df.groupby("key").agg(count_between_kw=agg_between_kw) 

152 count_between_kw 

153 key 

154 1 1 

155 2 1 

156 """ 

157 

158 column: Hashable 

159 aggfunc: AggScalar 

160 args: tuple[Any, ...] = () 

161 kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) 

162 

163 def __init__( 

164 self, 

165 column: Hashable, 

166 aggfunc: Callable[..., Any] | str, 

167 *args: Any, 

168 **kwargs: Any, 

169 ) -> None: 

170 self.column = column 

171 self.aggfunc = aggfunc 

172 self.args = args 

173 self.kwargs = kwargs 

174 

175 def __getitem__(self, key: int) -> Any: 

176 """Provide backward-compatible tuple-style access.""" 

177 if key == 0: 

178 return self.column 

179 elif key == 1: 

180 return self.aggfunc 

181 elif key == 2: 

182 return self.args 

183 elif key == 3: 

184 return self.kwargs 

185 raise IndexError("index out of range") 

186 

187 

188@set_module("pandas.api.typing") 

189class SeriesGroupBy(GroupBy[Series]): 

190 def _wrap_agged_manager(self, mgr: Manager) -> Series: 

191 out = self.obj._constructor_from_mgr(mgr, axes=mgr.axes) 

192 out._name = self.obj.name 

193 return out 

194 

195 def _get_data_to_aggregate( 

196 self, *, numeric_only: bool = False, name: str | None = None 

197 ) -> SingleBlockManager: 

198 ser = self._obj_with_exclusions 

199 single = ser._mgr 

200 if numeric_only and not is_numeric_dtype(ser.dtype): 

201 # GH#41291 match Series behavior 

202 kwd_name = "numeric_only" 

203 raise TypeError( 

204 f"Cannot use {kwd_name}=True with " 

205 f"{type(self).__name__}.{name} and non-numeric dtypes." 

206 ) 

207 return single 

208 

209 def apply(self, func, *args, **kwargs) -> Series: 

210 """ 

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

212 

213 The function passed to ``apply`` must take a series as its first 

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

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

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

217 grouping method. 

218 

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

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

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

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

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

224 

225 Parameters 

226 ---------- 

227 func : callable 

228 A callable that takes a series as its first argument, and 

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

230 callable may take positional and keyword arguments. 

231 

232 *args : tuple 

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

234 

235 **kwargs : dict 

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

237 

238 Returns 

239 ------- 

240 Series or DataFrame 

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

242 

243 See Also 

244 -------- 

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

246 group. 

247 aggregate : Apply aggregate function to the GroupBy object. 

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

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

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

251 

252 Notes 

253 ----- 

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

255 see the examples below. 

256 

257 Functions that mutate the passed object can produce unexpected 

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

259 for more details. 

260 

261 Examples 

262 -------- 

263 >>> s = pd.Series([0, 1, 2], index="a a b".split()) 

264 >>> g1 = s.groupby(s.index, group_keys=False) 

265 >>> g2 = s.groupby(s.index, group_keys=True) 

266 

267 From ``s`` above we can see that ``g`` has two groups, ``a`` and ``b``. 

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

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

270 we can get different grouping results: 

271 

272 Example 1: The function passed to `apply` takes a Series as 

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

274 each group together into a new Series. 

275 

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

277 

278 >>> g1.apply(lambda x: x * 2 if x.name == "a" else x / 2) 

279 a 0.0 

280 a 2.0 

281 b 1.0 

282 dtype: float64 

283 

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

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

286 

287 >>> g2.apply(lambda x: x * 2 if x.name == "a" else x / 2) 

288 a a 0.0 

289 a 2.0 

290 b b 1.0 

291 dtype: float64 

292 

293 Example 2: The function passed to `apply` takes a Series as 

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

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

296 appropriate: 

297 

298 >>> g1.apply(lambda x: x.max() - x.min()) 

299 a 1 

300 b 0 

301 dtype: int64 

302 

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

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

305 to the input. 

306 

307 >>> g2.apply(lambda x: x.max() - x.min()) 

308 a 1 

309 b 0 

310 dtype: int64 

311 """ 

312 return super().apply(func, *args, **kwargs) 

313 

314 def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs): 

315 """ 

316 Aggregate using one or more operations. 

317 

318 The ``aggregate`` method enables flexible and efficient aggregation of grouped 

319 data using a variety of functions, including built-in, user-defined, and 

320 optimized JIT-compiled functions. 

321 

322 Parameters 

323 ---------- 

324 func : function, str, list or None 

325 Function to use for aggregating the data. If a function, must either 

326 work when passed a Series or when passed to Series.apply. 

327 

328 Accepted combinations are: 

329 

330 - function 

331 - string function name 

332 - list of functions and/or function names, e.g. ``[np.sum, 'mean']`` 

333 - None, in which case ``**kwargs`` are used with Named Aggregation. Here 

334 the output has one column for each element in ``**kwargs``. The name of 

335 the column is keyword, whereas the value determines the aggregation 

336 used to compute the values in the column. 

337 

338 Can also accept a Numba JIT function with 

339 ``engine='numba'`` specified. Only passing a single function is supported 

340 with this engine. 

341 

342 If the ``'numba'`` engine is chosen, the function must be 

343 a user defined function with ``values`` and ``index`` as the 

344 first and second arguments respectively in the function signature. 

345 Each group's index will be passed to the user defined function 

346 and optionally available for use. 

347 

348 *args 

349 Positional arguments to pass to func. 

350 engine : str, default None 

351 * ``'cython'`` : Runs the function through C-extensions from cython. 

352 * ``'numba'`` : Runs the function through JIT compiled code from numba. 

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

354 ``compute.use_numba`` 

355 

356 engine_kwargs : dict, default None 

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

358 * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil`` 

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

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

361 ``{'nopython': True, 'nogil': False, 'parallel': False}`` and will be 

362 applied to the function 

363 

364 **kwargs 

365 * If ``func`` is None, ``**kwargs`` are used to define the output names and 

366 aggregations via Named Aggregation. See ``func`` entry. 

367 * Otherwise, keyword arguments to be passed into func. 

368 

369 Returns 

370 ------- 

371 Series 

372 Aggregated Series based on the grouping and the applied aggregation 

373 functions. 

374 

375 See Also 

376 -------- 

377 SeriesGroupBy.apply : Apply function func group-wise 

378 and combine the results together. 

379 SeriesGroupBy.transform : Transforms the Series on each group 

380 based on the given function. 

381 Series.aggregate : Aggregate using one or more operations. 

382 

383 Notes 

384 ----- 

385 When using ``engine='numba'``, there will be no "fall back" behavior internally. 

386 The group data and group index will be passed as numpy arrays to the JITed 

387 user defined function, and no alternative execution attempts will be tried. 

388 

389 Functions that mutate the passed object can produce unexpected 

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

391 for more details. 

392 

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

394 see the examples below. 

395 

396 Examples 

397 -------- 

398 >>> s = pd.Series([1, 2, 3, 4]) 

399 

400 >>> s 

401 0 1 

402 1 2 

403 2 3 

404 3 4 

405 dtype: int64 

406 

407 >>> s.groupby([1, 1, 2, 2]).min() 

408 1 1 

409 2 3 

410 dtype: int64 

411 

412 >>> s.groupby([1, 1, 2, 2]).agg("min") 

413 1 1 

414 2 3 

415 dtype: int64 

416 

417 >>> s.groupby([1, 1, 2, 2]).agg(["min", "max"]) 

418 min max 

419 1 1 2 

420 2 3 4 

421 

422 The output column names can be controlled by passing 

423 the desired column names and aggregations as keyword arguments. 

424 

425 >>> s.groupby([1, 1, 2, 2]).agg( 

426 ... minimum="min", 

427 ... maximum="max", 

428 ... ) 

429 minimum maximum 

430 1 1 2 

431 2 3 4 

432 

433 The resulting dtype will reflect the return value of the aggregating 

434 function. 

435 

436 >>> s.groupby([1, 1, 2, 2]).agg(lambda x: x.astype(float).min()) 

437 1 1.0 

438 2 3.0 

439 dtype: float64 

440 """ 

441 relabeling = func is None 

442 columns = None 

443 if relabeling: 

444 columns, func = validate_func_kwargs(kwargs) 

445 kwargs = {} 

446 

447 if isinstance(func, str): 

448 if maybe_use_numba(engine) and engine is not None: 

449 # Not all agg functions support numba, only propagate numba kwargs 

450 # if user asks for numba, and engine is not None 

451 # (if engine is None, the called function will handle the case where 

452 # numba is requested via the global option) 

453 kwargs["engine"] = engine 

454 if engine_kwargs is not None: 

455 kwargs["engine_kwargs"] = engine_kwargs 

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

457 

458 elif isinstance(func, abc.Iterable): 

459 # Catch instances of lists / tuples 

460 # but not the class list / tuple itself. 

461 func = maybe_mangle_lambdas(func) 

462 kwargs["engine"] = engine 

463 kwargs["engine_kwargs"] = engine_kwargs 

464 ret = self._aggregate_multiple_funcs(func, *args, **kwargs) 

465 if relabeling: 

466 # columns is not narrowed by mypy from relabeling flag 

467 assert columns is not None # for mypy 

468 ret.columns = columns 

469 if not self.as_index: 

470 ret = ret.reset_index() 

471 return ret 

472 

473 else: 

474 if maybe_use_numba(engine): 

475 return self._aggregate_with_numba( 

476 func, *args, engine_kwargs=engine_kwargs, **kwargs 

477 ) 

478 

479 if self.ngroups == 0: 

480 # e.g. test_evaluate_with_empty_groups without any groups to 

481 # iterate over, we have no output on which to do dtype 

482 # inference. We default to using the existing dtype. 

483 # xref GH#51445 

484 obj = self._obj_with_exclusions 

485 return self._wrap_aggregated_output( 

486 self.obj._constructor( 

487 [], 

488 name=self.obj.name, 

489 index=self._grouper.result_index, 

490 dtype=obj.dtype, 

491 ) 

492 ) 

493 return self._python_agg_general(func, *args, **kwargs) 

494 

495 agg = aggregate 

496 

497 def _python_agg_general(self, func, *args, **kwargs): 

498 f = lambda x: func(x, *args, **kwargs) 

499 

500 obj = self._obj_with_exclusions 

501 result = self._grouper.agg_series(obj, f) 

502 res = obj._constructor(result, name=obj.name) 

503 return self._wrap_aggregated_output(res) 

504 

505 def _aggregate_multiple_funcs(self, arg, *args, **kwargs) -> DataFrame: 

506 if isinstance(arg, dict): 

507 raise SpecificationError("nested renamer is not supported") 

508 

509 if any(isinstance(x, (tuple, list)) for x in arg): 

510 arg = ((x, x) if not isinstance(x, (tuple, list)) else x for x in arg) 

511 else: 

512 # list of functions / function names 

513 columns = (com.get_callable_name(f) or f for f in arg) 

514 arg = zip(columns, arg, strict=True) 

515 

516 results: dict[base.OutputKey, DataFrame | Series] = {} 

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

518 # Combine results using the index, need to adjust index after 

519 # if as_index=False (GH#50724) 

520 for idx, (name, func) in enumerate(arg): 

521 key = base.OutputKey(label=name, position=idx) 

522 results[key] = self.aggregate(func, *args, **kwargs) 

523 

524 if any(isinstance(x, DataFrame) for x in results.values()): 

525 from pandas import concat 

526 

527 res_df = concat( 

528 results.values(), axis=1, keys=[key.label for key in results] 

529 ) 

530 return res_df 

531 

532 indexed_output = {key.position: val for key, val in results.items()} 

533 output = self.obj._constructor_expanddim(indexed_output, index=None) 

534 output.columns = Index(key.label for key in results) 

535 

536 return output 

537 

538 def _wrap_applied_output( 

539 self, 

540 data: Series, 

541 values: list[Any], 

542 not_indexed_same: bool = False, 

543 is_transform: bool = False, 

544 ) -> DataFrame | Series: 

545 """ 

546 Wrap the output of SeriesGroupBy.apply into the expected result. 

547 

548 Parameters 

549 ---------- 

550 data : Series 

551 Input data for groupby operation. 

552 values : List[Any] 

553 Applied output for each group. 

554 not_indexed_same : bool, default False 

555 Whether the applied outputs are not indexed the same as the group axes. 

556 

557 Returns 

558 ------- 

559 DataFrame or Series 

560 """ 

561 if len(values) == 0: 

562 # GH #6265 

563 if is_transform: 

564 # GH#47787 see test_group_on_empty_multiindex 

565 res_index = data.index 

566 elif not self.group_keys: 

567 res_index = None 

568 else: 

569 res_index = self._grouper.result_index 

570 

571 return self.obj._constructor( 

572 [], 

573 name=self.obj.name, 

574 index=res_index, 

575 dtype=data.dtype, 

576 ) 

577 assert values is not None 

578 

579 if isinstance(values[0], dict): 

580 # GH #823 #24880 

581 index = self._grouper.result_index 

582 res_df = self.obj._constructor_expanddim(values, index=index) 

583 # if self.observed is False, 

584 # keep all-NaN rows created while re-indexing 

585 res_ser = res_df.stack() 

586 res_ser.name = self.obj.name 

587 return res_ser 

588 elif isinstance(values[0], (Series, DataFrame)): 

589 result = self._concat_objects( 

590 values, 

591 not_indexed_same=not_indexed_same, 

592 is_transform=is_transform, 

593 ) 

594 if isinstance(result, Series): 

595 result.name = self.obj.name 

596 if not self.as_index and not_indexed_same: 

597 result = self._insert_inaxis_grouper(result) 

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

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

600 else: 

601 # GH #6265 #24880 

602 result = self.obj._constructor( 

603 data=values, index=self._grouper.result_index, name=self.obj.name 

604 ) 

605 if not self.as_index: 

606 result = self._insert_inaxis_grouper(result) 

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

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

609 

610 __examples_series_doc = dedent( 

611 """ 

612 >>> ser = pd.Series([390.0, 350.0, 30.0, 20.0], 

613 ... index=["Falcon", "Falcon", "Parrot", "Parrot"], 

614 ... name="Max Speed") 

615 >>> grouped = ser.groupby([1, 1, 2, 2]) 

616 >>> grouped.transform(lambda x: (x - x.mean()) / x.std()) 

617 Falcon 0.707107 

618 Falcon -0.707107 

619 Parrot 0.707107 

620 Parrot -0.707107 

621 Name: Max Speed, dtype: float64 

622 

623 Broadcast result of the transformation 

624 

625 >>> grouped.transform(lambda x: x.max() - x.min()) 

626 Falcon 40.0 

627 Falcon 40.0 

628 Parrot 10.0 

629 Parrot 10.0 

630 Name: Max Speed, dtype: float64 

631 

632 >>> grouped.transform("mean") 

633 Falcon 370.0 

634 Falcon 370.0 

635 Parrot 25.0 

636 Parrot 25.0 

637 Name: Max Speed, dtype: float64 

638 

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

640 for example: 

641 

642 >>> grouped.transform(lambda x: x.astype(int).max()) 

643 Falcon 390 

644 Falcon 390 

645 Parrot 30 

646 Parrot 30 

647 Name: Max Speed, dtype: int64 

648 """ 

649 ) 

650 

651 def transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs): 

652 """ 

653 Call function producing a same-indexed Series on each group. 

654 

655 Returns a Series having the same indexes as the original object 

656 filled with the transformed values. 

657 

658 Parameters 

659 ---------- 

660 func : function, str 

661 Function to apply to each group. 

662 See the Notes section below for requirements. 

663 

664 Accepted inputs are: 

665 

666 - String 

667 - Python function 

668 - Numba JIT function with ``engine='numba'`` specified. 

669 

670 Only passing a single function is supported with this engine. 

671 If the ``'numba'`` engine is chosen, the function must be 

672 a user defined function with ``values`` and ``index`` as the 

673 first and second arguments respectively in the function signature. 

674 Each group's index will be passed to the user defined function 

675 and optionally available for use. 

676 

677 If a string is chosen, then it needs to be the name 

678 of the groupby method you want to use. 

679 *args 

680 Positional arguments to pass to func. 

681 engine : str, default None 

682 * ``'cython'`` : Runs the function through C-extensions from cython. 

683 * ``'numba'`` : Runs the function through JIT compiled code from numba. 

684 * ``None`` : Defaults to ``'cython'`` 

685 or the global setting ``compute.use_numba`` 

686 

687 engine_kwargs : dict, default None 

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

689 * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil`` 

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

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

692 ``{'nopython': True, 'nogil': False, 'parallel': False}`` and will be 

693 applied to the function 

694 

695 **kwargs 

696 Keyword arguments to be passed into func. 

697 

698 Returns 

699 ------- 

700 Series 

701 Series with the same indexes as the original object filled 

702 with transformed values. 

703 

704 See Also 

705 -------- 

706 Series.groupby.apply : Apply function ``func`` group-wise and combine 

707 the results together. 

708 Series.groupby.aggregate : Aggregate using one or more operations. 

709 Series.transform : Call ``func`` on self producing a Series with the 

710 same axis shape as self. 

711 

712 Notes 

713 ----- 

714 Each group is endowed the attribute 'name' in case you need to know 

715 which group you are working on. 

716 

717 The current implementation imposes three requirements on f: 

718 

719 * f must return a value that either has the same shape as the input 

720 subframe or can be broadcast to the shape of the input subframe. 

721 For example, if `f` returns a scalar it will be broadcast to have the 

722 same shape as the input subframe. 

723 * if this is a DataFrame, f must support application column-by-column 

724 in the subframe. If f also supports application to the entire subframe, 

725 then a fast path is used starting from the second chunk. 

726 * f must not mutate groups. Mutation is not supported and may 

727 produce unexpected results. See :ref:`gotchas.udf-mutation` for more details. 

728 

729 When using ``engine='numba'``, there will be no "fall back" behavior internally. 

730 The group data and group index will be passed as numpy arrays to the JITed 

731 user defined function, and no alternative execution attempts will be tried. 

732 

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

734 see the examples below. 

735 

736 .. versionchanged:: 2.0.0 

737 

738 When using ``.transform`` on a grouped DataFrame and 

739 the transformation function returns a DataFrame, 

740 pandas now aligns the result's index with the input's index. 

741 You can call ``.to_numpy()`` on the result of 

742 the transformation function to avoid alignment. 

743 

744 Examples 

745 -------- 

746 

747 >>> ser = pd.Series( 

748 ... [390.0, 350.0, 30.0, 20.0], 

749 ... index=["Falcon", "Falcon", "Parrot", "Parrot"], 

750 ... name="Max Speed", 

751 ... ) 

752 >>> grouped = ser.groupby([1, 1, 2, 2]) 

753 >>> grouped.transform(lambda x: (x - x.mean()) / x.std()) 

754 Falcon 0.707107 

755 Falcon -0.707107 

756 Parrot 0.707107 

757 Parrot -0.707107 

758 Name: Max Speed, dtype: float64 

759 

760 Broadcast result of the transformation 

761 

762 >>> grouped.transform(lambda x: x.max() - x.min()) 

763 Falcon 40.0 

764 Falcon 40.0 

765 Parrot 10.0 

766 Parrot 10.0 

767 Name: Max Speed, dtype: float64 

768 

769 >>> grouped.transform("mean") 

770 Falcon 370.0 

771 Falcon 370.0 

772 Parrot 25.0 

773 Parrot 25.0 

774 Name: Max Speed, dtype: float64 

775 

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

777 for example: 

778 

779 >>> grouped.transform(lambda x: x.astype(int).max()) 

780 Falcon 390 

781 Falcon 390 

782 Parrot 30 

783 Parrot 30 

784 Name: Max Speed, dtype: int64 

785 """ 

786 return self._transform( 

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

788 ) 

789 

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

791 obj = self._obj_with_exclusions 

792 

793 try: 

794 result = self._grouper._cython_operation( 

795 "transform", obj._values, how, 0, **kwargs 

796 ) 

797 except NotImplementedError as err: 

798 # e.g. test_groupby_raises_string 

799 raise TypeError(f"{how} is not supported for {obj.dtype} dtype") from err 

800 

801 return obj._constructor(result, index=self.obj.index, name=obj.name) 

802 

803 def _transform_general( 

804 self, func: Callable, engine, engine_kwargs, *args, **kwargs 

805 ) -> Series: 

806 """ 

807 Transform with a callable `func`. 

808 """ 

809 if maybe_use_numba(engine): 

810 return self._transform_with_numba( 

811 func, *args, engine_kwargs=engine_kwargs, **kwargs 

812 ) 

813 assert callable(func) 

814 klass = type(self.obj) 

815 

816 results = [] 

817 for name, group in self._grouper.get_iterator( 

818 self._obj_with_exclusions, 

819 ): 

820 # this setattr is needed for test_transform_lambda_with_datetimetz 

821 object.__setattr__(group, "name", name) 

822 res = func(group, *args, **kwargs) 

823 

824 results.append(klass(res, index=group.index)) 

825 

826 # check for empty "results" to avoid concat ValueError 

827 if results: 

828 from pandas.core.reshape.concat import concat 

829 

830 concatenated = concat(results, ignore_index=True) 

831 result = self._set_result_index_ordered(concatenated) 

832 else: 

833 result = self.obj._constructor(dtype=np.float64) 

834 

835 result.name = self.obj.name 

836 return result 

837 

838 def filter(self, func, dropna: bool = True, *args, **kwargs): 

839 """ 

840 Filter elements from groups that don't satisfy a criterion. 

841 

842 Elements from groups are filtered if they do not satisfy the 

843 boolean criterion specified by func. 

844 

845 Parameters 

846 ---------- 

847 func : function 

848 Criterion to apply to each group. Should return True or False. 

849 dropna : bool, optional 

850 Drop groups that do not pass the filter. True by default; if False, 

851 groups that evaluate False are filled with NaNs. 

852 *args : tuple 

853 Optional positional arguments to pass to `func`. 

854 **kwargs : dict 

855 Optional keyword arguments to pass to `func`. 

856 

857 Returns 

858 ------- 

859 Series 

860 The filtered subset of the original Series. 

861 

862 See Also 

863 -------- 

864 Series.filter: Filter elements of ungrouped Series. 

865 DataFrameGroupBy.filter : Filter elements from groups base on criterion. 

866 

867 Notes 

868 ----- 

869 Functions that mutate the passed object can produce unexpected 

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

871 for more details. 

872 

873 Examples 

874 -------- 

875 >>> df = pd.DataFrame( 

876 ... { 

877 ... "A": ["foo", "bar", "foo", "bar", "foo", "bar"], 

878 ... "B": [1, 2, 3, 4, 5, 6], 

879 ... "C": [2.0, 5.0, 8.0, 1.0, 2.0, 9.0], 

880 ... } 

881 ... ) 

882 >>> grouped = df.groupby("A") 

883 >>> df.groupby("A").B.filter(lambda x: x.mean() > 3.0) 

884 1 2 

885 3 4 

886 5 6 

887 Name: B, dtype: int64 

888 """ 

889 if isinstance(func, str): 

890 wrapper = lambda x: getattr(x, func)(*args, **kwargs) 

891 else: 

892 wrapper = lambda x: func(x, *args, **kwargs) 

893 

894 # Interpret np.nan as False. 

895 def true_and_notna(x) -> bool: 

896 b = wrapper(x) 

897 return notna(b) and b 

898 

899 try: 

900 indices = [ 

901 self._get_index(name) 

902 for name, group in self._grouper.get_iterator(self._obj_with_exclusions) 

903 if true_and_notna(group) 

904 ] 

905 except (ValueError, TypeError) as err: 

906 raise TypeError("the filter must return a boolean result") from err 

907 

908 filtered = self._apply_filter(indices, dropna) 

909 return filtered 

910 

911 def nunique(self, dropna: bool = True) -> Series | DataFrame: 

912 """ 

913 Return number of unique elements in the group. 

914 

915 Parameters 

916 ---------- 

917 dropna : bool, default True 

918 Don't include NaN in the counts. 

919 

920 Returns 

921 ------- 

922 Series 

923 Number of unique values within each group. 

924 

925 See Also 

926 -------- 

927 core.resample.Resampler.nunique : Method nunique for Resampler. 

928 

929 Examples 

930 -------- 

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

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

933 >>> ser 

934 a 1 

935 a 2 

936 b 3 

937 b 3 

938 dtype: int64 

939 >>> ser.groupby(level=0).nunique() 

940 a 2 

941 b 1 

942 dtype: int64 

943 """ 

944 ids = self._grouper.ids 

945 ngroups = self._grouper.ngroups 

946 val = self.obj._values 

947 codes, uniques = algorithms.factorize(val, use_na_sentinel=dropna, sort=False) 

948 

949 if self._grouper.has_dropped_na: 

950 mask = ids >= 0 

951 ids = ids[mask] 

952 codes = codes[mask] 

953 

954 group_index = get_group_index( 

955 labels=[ids, codes], 

956 shape=(ngroups, len(uniques)), 

957 sort=False, 

958 xnull=dropna, 

959 ) 

960 

961 if dropna: 

962 mask = group_index >= 0 

963 if (~mask).any(): 

964 ids = ids[mask] 

965 group_index = group_index[mask] 

966 

967 mask = duplicated(group_index, "first") 

968 res = np.bincount(ids[~mask], minlength=ngroups) 

969 res = ensure_int64(res) 

970 

971 ri = self._grouper.result_index 

972 result: Series | DataFrame = self.obj._constructor( 

973 res, index=ri, name=self.obj.name 

974 ) 

975 if not self.as_index: 

976 result = self._insert_inaxis_grouper(result) 

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

978 return result 

979 

980 def describe(self, percentiles=None, include=None, exclude=None) -> Series: 

981 """ 

982 Generate descriptive statistics. 

983 

984 Descriptive statistics include those that summarize the central 

985 tendency, dispersion and shape of a 

986 dataset's distribution, excluding ``NaN`` values. 

987 

988 Analyzes both numeric and object series, as well 

989 as ``DataFrame`` column sets of mixed data types. The output 

990 will vary depending on what is provided. Refer to the notes 

991 below for more detail. 

992 

993 Parameters 

994 ---------- 

995 percentiles : list-like of numbers, optional 

996 The percentiles to include in the output. All should 

997 fall between 0 and 1. The default, ``None``, will automatically 

998 return the 25th, 50th, and 75th percentiles. 

999 include : 'all', list-like of dtypes or None (default), optional 

1000 A white list of data types to include in the result. Ignored 

1001 for ``Series``. Here are the options: 

1002 

1003 - 'all' : All columns of the input will be included in the output. 

1004 - A list-like of dtypes : Limits the results to the 

1005 provided data types. 

1006 To limit the result to numeric types submit 

1007 ``numpy.number``. To limit it instead to object columns submit 

1008 the ``numpy.object`` data type. Strings 

1009 can also be used in the style of 

1010 ``select_dtypes`` (e.g. ``df.describe(include=['O'])``). To 

1011 select pandas categorical columns, use ``'category'`` 

1012 - None (default) : The result will include all numeric columns. 

1013 exclude : list-like of dtypes or None (default), optional, 

1014 A black list of data types to omit from the result. Ignored 

1015 for ``Series``. Here are the options: 

1016 

1017 - A list-like of dtypes : Excludes the provided data types 

1018 from the result. To exclude numeric types submit 

1019 ``numpy.number``. To exclude object columns submit the data 

1020 type ``numpy.object``. Strings can also be used in the style of 

1021 ``select_dtypes`` (e.g. ``df.describe(exclude=['O'])``). To 

1022 exclude pandas categorical columns, use ``'category'`` 

1023 - None (default) : The result will exclude nothing. 

1024 

1025 Returns 

1026 ------- 

1027 Series or DataFrame 

1028 Summary statistics of the Series or Dataframe provided. 

1029 

1030 See Also 

1031 -------- 

1032 DataFrame.count: Count number of non-NA/null observations. 

1033 DataFrame.max: Maximum of the values in the object. 

1034 DataFrame.min: Minimum of the values in the object. 

1035 DataFrame.mean: Mean of the values. 

1036 DataFrame.std: Standard deviation of the observations. 

1037 DataFrame.select_dtypes: Subset of a DataFrame including/excluding 

1038 columns based on their dtype. 

1039 

1040 Notes 

1041 ----- 

1042 For numeric data, the result's index will include ``count``, 

1043 ``mean``, ``std``, ``min``, ``max`` as well as lower, ``50`` and 

1044 upper percentiles. By default the lower percentile is ``25`` and the 

1045 upper percentile is ``75``. The ``50`` percentile is the 

1046 same as the median. 

1047 

1048 For object data (e.g. strings), the result's index 

1049 will include ``count``, ``unique``, ``top``, and ``freq``. The ``top`` 

1050 is the most common value. The ``freq`` is the most common value's 

1051 frequency. 

1052 

1053 If multiple object values have the highest count, then the 

1054 ``count`` and ``top`` results will be arbitrarily chosen from 

1055 among those with the highest count. 

1056 

1057 For mixed data types provided via a ``DataFrame``, the default is to 

1058 return only an analysis of numeric columns. If the DataFrame consists 

1059 only of object and categorical data without any numeric columns, the 

1060 default is to return an analysis of both the object and categorical 

1061 columns. If ``include='all'`` is provided as an option, the result 

1062 will include a union of attributes of each type. 

1063 

1064 The `include` and `exclude` parameters can be used to limit 

1065 which columns in a ``DataFrame`` are analyzed for the output. 

1066 The parameters are ignored when analyzing a ``Series``. 

1067 

1068 Examples 

1069 -------- 

1070 Describing a numeric ``Series``. 

1071 

1072 >>> s = pd.Series([1, 2, 3, 4]) 

1073 

1074 >>> s 

1075 0 1 

1076 1 2 

1077 2 3 

1078 3 4 

1079 dtype: int64 

1080 

1081 >>> s.groupby([1, 1, 2, 2]).describe() 

1082 count mean std min 25% 50% 75% max 

1083 1 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 

1084 2 2.0 3.5 0.707107 3.0 3.25 3.5 3.75 4.0 

1085 """ 

1086 return super().describe( 

1087 percentiles=percentiles, include=include, exclude=exclude 

1088 ) 

1089 

1090 def value_counts( 

1091 self, 

1092 normalize: bool = False, 

1093 sort: bool = True, 

1094 ascending: bool = False, 

1095 bins=None, 

1096 dropna: bool = True, 

1097 ) -> Series | DataFrame: 

1098 """ 

1099 Return a Series or DataFrame containing counts of unique rows. 

1100 

1101 Parameters 

1102 ---------- 

1103 normalize : bool, default False 

1104 Return proportions rather than frequencies. 

1105 sort : bool, default True 

1106 Sort by frequencies. 

1107 ascending : bool, default False 

1108 Sort in ascending order. 

1109 bins : int or list of ints, optional 

1110 Rather than count values, group them into half-open bins, 

1111 a convenience for pd.cut, only works with numeric data. 

1112 dropna : bool, default True 

1113 Don't include counts of rows that contain NA values. 

1114 

1115 Returns 

1116 ------- 

1117 Series or DataFrame 

1118 Series if the groupby ``as_index`` is True, otherwise DataFrame. 

1119 

1120 See Also 

1121 -------- 

1122 Series.value_counts: Equivalent method on Series. 

1123 DataFrame.value_counts: Equivalent method on DataFrame. 

1124 DataFrameGroupBy.value_counts: Equivalent method on DataFrameGroupBy. 

1125 

1126 Notes 

1127 ----- 

1128 - If the groupby ``as_index`` is True then the returned Series will have a 

1129 MultiIndex with one level per input column. 

1130 - If the groupby ``as_index`` is False then the returned DataFrame will have an 

1131 additional column with the value_counts. The column is labelled 'count' or 

1132 'proportion', depending on the ``normalize`` parameter. 

1133 

1134 By default, rows that contain any NA values are omitted from 

1135 the result. 

1136 

1137 By default, the result will be in descending order so that the 

1138 first element of each group is the most frequently-occurring row. 

1139 

1140 Examples 

1141 -------- 

1142 >>> s = pd.Series( 

1143 ... [1, 1, 2, 3, 2, 3, 3, 1, 1, 3, 3, 3], 

1144 ... index=["A", "A", "A", "A", "A", "A", "B", "B", "B", "B", "B", "B"], 

1145 ... ) 

1146 >>> s 

1147 A 1 

1148 A 1 

1149 A 2 

1150 A 3 

1151 A 2 

1152 A 3 

1153 B 3 

1154 B 1 

1155 B 1 

1156 B 3 

1157 B 3 

1158 B 3 

1159 dtype: int64 

1160 >>> g1 = s.groupby(s.index) 

1161 >>> g1.value_counts(bins=2) 

1162 A (0.997, 2.0] 4 

1163 (2.0, 3.0] 2 

1164 B (2.0, 3.0] 4 

1165 (0.997, 2.0] 2 

1166 Name: count, dtype: int64 

1167 >>> g1.value_counts(normalize=True) 

1168 A 1 0.333333 

1169 2 0.333333 

1170 3 0.333333 

1171 B 3 0.666667 

1172 1 0.333333 

1173 Name: proportion, dtype: float64 

1174 """ 

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

1176 

1177 if bins is None: 

1178 result = self._value_counts( 

1179 normalize=normalize, sort=sort, ascending=ascending, dropna=dropna 

1180 ) 

1181 result.name = name 

1182 return result 

1183 

1184 from pandas.core.reshape.merge import get_join_indexers 

1185 from pandas.core.reshape.tile import cut 

1186 

1187 ids = self._grouper.ids 

1188 val = self.obj._values 

1189 

1190 index_names = [*self._grouper.names, self.obj.name] 

1191 

1192 if isinstance(val.dtype, CategoricalDtype) or ( 

1193 bins is not None and not np.iterable(bins) 

1194 ): 

1195 # scalar bins cannot be done at top level 

1196 # in a backward compatible way 

1197 # GH38672 relates to categorical dtype 

1198 ser = self.apply( 

1199 Series.value_counts, 

1200 normalize=normalize, 

1201 sort=sort, 

1202 ascending=ascending, 

1203 bins=bins, 

1204 ) 

1205 ser.name = name 

1206 ser.index.names = index_names 

1207 return ser 

1208 

1209 # groupby removes null keys from groupings 

1210 mask = ids != -1 

1211 ids, val = ids[mask], val[mask] 

1212 

1213 lab: Index | np.ndarray 

1214 if bins is None: 

1215 lab, lev = algorithms.factorize(val, sort=True) 

1216 llab = lambda lab, inc: lab[inc] 

1217 else: 

1218 # lab is a Categorical with categories an IntervalIndex 

1219 cat_ser = cut(Series(val, copy=False), bins, include_lowest=True) 

1220 cat_obj = cast("Categorical", cat_ser._values) 

1221 lev = cat_obj.categories 

1222 lab = lev.take( 

1223 cat_obj.codes, 

1224 allow_fill=True, 

1225 fill_value=lev._na_value, 

1226 ) 

1227 llab = lambda lab, inc: lab[inc]._multiindex.codes[-1] 

1228 

1229 if isinstance(lab.dtype, IntervalDtype): 

1230 # TODO: should we do this inside II? 

1231 lab_interval = cast(Interval, lab) 

1232 

1233 sorter = np.lexsort((lab_interval.left, lab_interval.right, ids)) 

1234 else: 

1235 sorter = np.lexsort((lab, ids)) 

1236 

1237 ids, lab = ids[sorter], lab[sorter] 

1238 

1239 # group boundaries are where group ids change 

1240 idchanges = 1 + np.nonzero(ids[1:] != ids[:-1])[0] 

1241 idx = np.r_[0, idchanges] 

1242 if not len(ids): 

1243 idx = idchanges 

1244 

1245 # new values are where sorted labels change 

1246 lchanges = llab(lab, slice(1, None)) != llab(lab, slice(None, -1)) 

1247 inc = np.r_[True, lchanges] 

1248 if not len(val): 

1249 inc = lchanges 

1250 inc[idx] = True # group boundaries are also new values 

1251 out = np.diff(np.nonzero(np.r_[inc, True])[0]) # value counts 

1252 

1253 # num. of times each group should be repeated 

1254 rep = partial(np.repeat, repeats=np.add.reduceat(inc, idx)) 

1255 

1256 # multi-index components 

1257 if isinstance(self._grouper.result_index, MultiIndex): 

1258 codes = list(self._grouper.result_index.codes) 

1259 else: 

1260 codes = [ 

1261 algorithms.factorize( 

1262 self._grouper.result_index, 

1263 sort=self._grouper._sort, 

1264 use_na_sentinel=self._grouper.dropna, 

1265 )[0] 

1266 ] 

1267 codes = [rep(level_codes) for level_codes in codes] + [llab(lab, inc)] 

1268 levels = [*self._grouper.levels, lev] 

1269 

1270 if dropna: 

1271 mask = codes[-1] != -1 

1272 if mask.all(): 

1273 dropna = False 

1274 else: 

1275 out, codes = out[mask], [level_codes[mask] for level_codes in codes] 

1276 

1277 if normalize: 

1278 out = out.astype("float") 

1279 d = np.diff(np.r_[idx, len(ids)]) 

1280 if dropna: 

1281 m = ids[lab == -1] 

1282 np.add.at(d, m, -1) 

1283 acc = rep(d)[mask] 

1284 else: 

1285 acc = rep(d) 

1286 out /= acc 

1287 

1288 if sort and bins is None: 

1289 cat = ids[inc][mask] if dropna else ids[inc] 

1290 sorter = np.lexsort((out if ascending else -out, cat)) 

1291 out, codes[-1] = out[sorter], codes[-1][sorter] 

1292 

1293 if bins is not None: 

1294 # for compat. with libgroupby.value_counts need to ensure every 

1295 # bin is present at every index level, null filled with zeros 

1296 diff = np.zeros(len(out), dtype="bool") 

1297 for level_codes in codes[:-1]: 

1298 diff |= np.r_[True, level_codes[1:] != level_codes[:-1]] 

1299 

1300 ncat, nbin = diff.sum(), len(levels[-1]) 

1301 

1302 left = [np.repeat(np.arange(ncat), nbin), np.tile(np.arange(nbin), ncat)] 

1303 

1304 right = [diff.cumsum() - 1, codes[-1]] 

1305 

1306 # error: Argument 1 to "get_join_indexers" has incompatible type 

1307 # "List[ndarray[Any, Any]]"; expected "List[Union[Union[ExtensionArray, 

1308 # ndarray[Any, Any]], Index, Series]] 

1309 _, idx = get_join_indexers( 

1310 left, # type: ignore[arg-type] 

1311 right, 

1312 sort=False, 

1313 how="left", 

1314 ) 

1315 if idx is not None: 

1316 out = np.where(idx != -1, out[idx], 0) 

1317 

1318 if sort: 

1319 sorter = np.lexsort((out if ascending else -out, left[0])) 

1320 out, left[-1] = out[sorter], left[-1][sorter] 

1321 

1322 # build the multi-index w/ full levels 

1323 def build_codes(lev_codes: np.ndarray) -> np.ndarray: 

1324 return np.repeat(lev_codes[diff], nbin) 

1325 

1326 codes = [build_codes(lev_codes) for lev_codes in codes[:-1]] 

1327 codes.append(left[-1]) 

1328 

1329 mi = MultiIndex( 

1330 levels=levels, codes=codes, names=index_names, verify_integrity=False 

1331 ) 

1332 

1333 if is_integer_dtype(out.dtype): 

1334 out = ensure_int64(out) 

1335 result = self.obj._constructor(out, index=mi, name=name) 

1336 if not self.as_index: 

1337 result = result.reset_index() 

1338 return result 

1339 

1340 def take( 

1341 self, 

1342 indices: TakeIndexer, 

1343 **kwargs, 

1344 ) -> Series: 

1345 """ 

1346 Return the elements in the given *positional* indices in each group. 

1347 

1348 This means that we are not indexing according to actual values in 

1349 the index attribute of the object. We are indexing according to the 

1350 actual position of the element in the object. 

1351 

1352 If a requested index does not exist for some group, this method will raise. 

1353 To get similar behavior that ignores indices that don't exist, see 

1354 :meth:`.SeriesGroupBy.nth`. 

1355 

1356 Parameters 

1357 ---------- 

1358 indices : array-like 

1359 An array of ints indicating which positions to take in each group. 

1360 

1361 **kwargs 

1362 For compatibility with :meth:`numpy.take`. Has no effect on the 

1363 output. 

1364 

1365 Returns 

1366 ------- 

1367 Series 

1368 A Series containing the elements taken from each group. 

1369 

1370 See Also 

1371 -------- 

1372 Series.take : Take elements from a Series along an axis. 

1373 Series.loc : Select a subset of a DataFrame by labels. 

1374 Series.iloc : Select a subset of a DataFrame by positions. 

1375 numpy.take : Take elements from an array along an axis. 

1376 SeriesGroupBy.nth : Similar to take, won't raise if indices don't exist. 

1377 

1378 Examples 

1379 -------- 

1380 >>> df = pd.DataFrame( 

1381 ... [ 

1382 ... ("falcon", "bird", 389.0), 

1383 ... ("parrot", "bird", 24.0), 

1384 ... ("lion", "mammal", 80.5), 

1385 ... ("monkey", "mammal", np.nan), 

1386 ... ("rabbit", "mammal", 15.0), 

1387 ... ], 

1388 ... columns=["name", "class", "max_speed"], 

1389 ... index=[4, 3, 2, 1, 0], 

1390 ... ) 

1391 >>> df 

1392 name class max_speed 

1393 4 falcon bird 389.0 

1394 3 parrot bird 24.0 

1395 2 lion mammal 80.5 

1396 1 monkey mammal NaN 

1397 0 rabbit mammal 15.0 

1398 >>> gb = df["name"].groupby([1, 1, 2, 2, 2]) 

1399 

1400 Take elements at rows 0 and 1 in each group. 

1401 

1402 >>> gb.take([0, 1]) 

1403 1 4 falcon 

1404 3 parrot 

1405 2 2 lion 

1406 1 monkey 

1407 Name: name, dtype: str 

1408 

1409 We may take elements using negative integers for positive indices, 

1410 starting from the end of the object, just like with Python lists. 

1411 

1412 >>> gb.take([-1, -2]) 

1413 1 3 parrot 

1414 4 falcon 

1415 2 0 rabbit 

1416 1 monkey 

1417 Name: name, dtype: str 

1418 """ 

1419 result = self._op_via_apply("take", indices=indices, **kwargs) 

1420 return result 

1421 

1422 def skew( 

1423 self, 

1424 skipna: bool = True, 

1425 numeric_only: bool = False, 

1426 **kwargs, 

1427 ) -> Series: 

1428 """ 

1429 Return unbiased skew within groups. 

1430 

1431 Normalized by N-1. 

1432 

1433 Parameters 

1434 ---------- 

1435 skipna : bool, default True 

1436 Exclude NA/null values when computing the result. 

1437 

1438 numeric_only : bool, default False 

1439 Include only float, int, boolean columns. Not implemented for Series. 

1440 

1441 **kwargs 

1442 Additional keyword arguments to be passed to the function. 

1443 

1444 Returns 

1445 ------- 

1446 Series 

1447 Unbiased skew within groups. 

1448 

1449 See Also 

1450 -------- 

1451 Series.skew : Return unbiased skew over requested axis. 

1452 

1453 Examples 

1454 -------- 

1455 >>> ser = pd.Series( 

1456 ... [390.0, 350.0, 357.0, np.nan, 22.0, 20.0, 30.0], 

1457 ... index=[ 

1458 ... "Falcon", 

1459 ... "Falcon", 

1460 ... "Falcon", 

1461 ... "Falcon", 

1462 ... "Parrot", 

1463 ... "Parrot", 

1464 ... "Parrot", 

1465 ... ], 

1466 ... name="Max Speed", 

1467 ... ) 

1468 >>> ser 

1469 Falcon 390.0 

1470 Falcon 350.0 

1471 Falcon 357.0 

1472 Falcon NaN 

1473 Parrot 22.0 

1474 Parrot 20.0 

1475 Parrot 30.0 

1476 Name: Max Speed, dtype: float64 

1477 >>> ser.groupby(level=0).skew() 

1478 Falcon 1.525174 

1479 Parrot 1.457863 

1480 Name: Max Speed, dtype: float64 

1481 >>> ser.groupby(level=0).skew(skipna=False) 

1482 Falcon NaN 

1483 Parrot 1.457863 

1484 Name: Max Speed, dtype: float64 

1485 """ 

1486 

1487 return self._cython_agg_general( 

1488 "skew", alt=None, skipna=skipna, numeric_only=numeric_only, **kwargs 

1489 ) 

1490 

1491 def kurt( 

1492 self, 

1493 skipna: bool = True, 

1494 numeric_only: bool = False, 

1495 **kwargs, 

1496 ) -> Series: 

1497 """ 

1498 Return unbiased kurtosis within groups. 

1499 

1500 Parameters 

1501 ---------- 

1502 skipna : bool, default True 

1503 Exclude NA/null values when computing the result. 

1504 

1505 numeric_only : bool, default False 

1506 Include only float, int, boolean columns. Not implemented for Series. 

1507 

1508 **kwargs 

1509 Additional keyword arguments to be passed to the function. 

1510 

1511 Returns 

1512 ------- 

1513 Series 

1514 Unbiased kurtosis within groups. 

1515 

1516 See Also 

1517 -------- 

1518 Series.kurt : Return unbiased kurtosis over requested axis. 

1519 

1520 Examples 

1521 -------- 

1522 >>> ser = pd.Series( 

1523 ... [390.0, 350.0, 357.0, 333.0, np.nan, 22.0, 20.0, 30.0, 40.0, 41.0], 

1524 ... index=[ 

1525 ... "Falcon", 

1526 ... "Falcon", 

1527 ... "Falcon", 

1528 ... "Falcon", 

1529 ... "Falcon", 

1530 ... "Parrot", 

1531 ... "Parrot", 

1532 ... "Parrot", 

1533 ... "Parrot", 

1534 ... "Parrot", 

1535 ... ], 

1536 ... name="Max Speed", 

1537 ... ) 

1538 >>> ser 

1539 Falcon 390.0 

1540 Falcon 350.0 

1541 Falcon 357.0 

1542 Falcon 333.0 

1543 Falcon NaN 

1544 Parrot 22.0 

1545 Parrot 20.0 

1546 Parrot 30.0 

1547 Parrot 40.0 

1548 Parrot 41.0 

1549 Name: Max Speed, dtype: float64 

1550 >>> ser.groupby(level=0).kurt() 

1551 Falcon 1.622109 

1552 Parrot -2.878714 

1553 Name: Max Speed, dtype: float64 

1554 >>> ser.groupby(level=0).kurt(skipna=False) 

1555 Falcon NaN 

1556 Parrot -2.878714 

1557 Name: Max Speed, dtype: float64 

1558 """ 

1559 

1560 def alt(obj): 

1561 # This should not be reached since the cython path should raise 

1562 # TypeError and not NotImplementedError. 

1563 raise TypeError(f"'kurt' is not supported for dtype={obj.dtype}") 

1564 

1565 return self._cython_agg_general( 

1566 "kurt", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs 

1567 ) 

1568 

1569 @property 

1570 def plot(self) -> GroupByPlot: 

1571 """ 

1572 Make plots of groups from a Series. 

1573 

1574 Uses the backend specified by the option ``plotting.backend``. 

1575 By default, matplotlib is used. 

1576 

1577 Returns 

1578 ------- 

1579 GroupByPlot 

1580 A plotting object that can be used to create plots for each group. 

1581 

1582 See Also 

1583 -------- 

1584 Series.plot : Make plots of Series. 

1585 

1586 Examples 

1587 -------- 

1588 >>> ser = pd.Series([1, 2, 3, 4, 5], index=["a", "a", "b", "b", "c"]) 

1589 >>> g = ser.groupby(level=0) 

1590 >>> g.plot() # doctest: +SKIP 

1591 """ 

1592 result = GroupByPlot(self) 

1593 return result 

1594 

1595 def nlargest( 

1596 self, n: int = 5, keep: Literal["first", "last", "all"] = "first" 

1597 ) -> Series: 

1598 """ 

1599 Return the largest `n` elements. 

1600 

1601 Parameters 

1602 ---------- 

1603 n : int, default 5 

1604 Return this many descending sorted values. 

1605 keep : {'first', 'last', 'all'}, default 'first' 

1606 When there are duplicate values that cannot all fit in a 

1607 Series of `n` elements: 

1608 

1609 - ``first`` : return the first `n` occurrences in order 

1610 of appearance. 

1611 - ``last`` : return the last `n` occurrences in reverse 

1612 order of appearance. 

1613 - ``all`` : keep all occurrences. This can result in a Series of 

1614 size larger than `n`. 

1615 

1616 Returns 

1617 ------- 

1618 Series 

1619 The `n` largest values in the Series, sorted in decreasing order. 

1620 

1621 See Also 

1622 -------- 

1623 Series.nsmallest: Get the `n` smallest elements. 

1624 Series.sort_values: Sort Series by values. 

1625 Series.head: Return the first `n` rows. 

1626 

1627 Notes 

1628 ----- 

1629 Faster than ``.sort_values(ascending=False).head(n)`` for small `n` 

1630 relative to the size of the ``Series`` object. 

1631 

1632 Examples 

1633 -------- 

1634 >>> s = pd.Series([1, 2, 3, 4, 5, 6]) 

1635 

1636 >>> s 

1637 0 1 

1638 1 2 

1639 2 3 

1640 3 4 

1641 4 5 

1642 5 6 

1643 dtype: int64 

1644 

1645 >>> s.groupby([1, 1, 1, 2, 2, 2]).nlargest(n=2) 

1646 1 2 3 

1647 1 2 

1648 2 5 6 

1649 4 5 

1650 dtype: int64 

1651 """ 

1652 f = partial(Series.nlargest, n=n, keep=keep) 

1653 data = self._obj_with_exclusions 

1654 # Don't change behavior if result index happens to be the same, i.e. 

1655 # already ordered and n >= all group sizes. 

1656 result = self._python_apply_general(f, data, not_indexed_same=True) 

1657 return result 

1658 

1659 def nsmallest( 

1660 self, n: int = 5, keep: Literal["first", "last", "all"] = "first" 

1661 ) -> Series: 

1662 """ 

1663 Return the smallest `n` elements. 

1664 

1665 Parameters 

1666 ---------- 

1667 n : int, default 5 

1668 Return this many ascending sorted values. 

1669 keep : {'first', 'last', 'all'}, default 'first' 

1670 When there are duplicate values that cannot all fit in a 

1671 Series of `n` elements: 

1672 

1673 - ``first`` : return the first `n` occurrences in order 

1674 of appearance. 

1675 - ``last`` : return the last `n` occurrences in reverse 

1676 order of appearance. 

1677 - ``all`` : keep all occurrences. This can result in a Series of 

1678 size larger than `n`. 

1679 

1680 Returns 

1681 ------- 

1682 Series 

1683 The `n` smallest values in the Series, sorted in increasing order. 

1684 

1685 See Also 

1686 -------- 

1687 Series.nlargest: Get the `n` largest elements. 

1688 Series.sort_values: Sort Series by values. 

1689 Series.head: Return the first `n` rows. 

1690 

1691 Notes 

1692 ----- 

1693 Faster than ``.sort_values().head(n)`` for small `n` relative to 

1694 the size of the ``Series`` object. 

1695 

1696 Examples 

1697 -------- 

1698 >>> s = pd.Series([1, 2, 3, 4, 5, 6]) 

1699 

1700 >>> s 

1701 0 1 

1702 1 2 

1703 2 3 

1704 3 4 

1705 4 5 

1706 5 6 

1707 dtype: int64 

1708 

1709 >>> s.groupby([1, 1, 1, 2, 2, 2]).nsmallest(n=2) 

1710 1 0 1 

1711 1 2 

1712 2 3 4 

1713 4 5 

1714 dtype: int64 

1715 """ 

1716 f = partial(Series.nsmallest, n=n, keep=keep) 

1717 data = self._obj_with_exclusions 

1718 # Don't change behavior if result index happens to be the same, i.e. 

1719 # already ordered and n >= all group sizes. 

1720 result = self._python_apply_general(f, data, not_indexed_same=True) 

1721 return result 

1722 

1723 def idxmin(self, skipna: bool = True) -> Series: 

1724 """ 

1725 Return the row label of the minimum value. 

1726 

1727 If multiple values equal the minimum, the first row label with that 

1728 value is returned. 

1729 

1730 Parameters 

1731 ---------- 

1732 skipna : bool, default True 

1733 Exclude NA values. 

1734 

1735 Returns 

1736 ------- 

1737 Series 

1738 Indexes of minima in each group. 

1739 

1740 Raises 

1741 ------ 

1742 ValueError 

1743 When there are no valid values for a group. Then can happen if: 

1744 

1745 * There is an unobserved group and ``observed=False``. 

1746 * All values for a group are NA. 

1747 * Some values for a group are NA and ``skipna=False``. 

1748 

1749 .. versionchanged:: 3.0.0 

1750 Previously if all values for a group are NA or some values for a group are 

1751 NA and ``skipna=False``, this method would return NA. Now it raises instead. 

1752 

1753 See Also 

1754 -------- 

1755 numpy.argmin : Return indices of the minimum values 

1756 along the given axis. 

1757 DataFrame.idxmin : Return index of first occurrence of minimum 

1758 over requested axis. 

1759 Series.idxmax : Return index *label* of the first occurrence 

1760 of maximum of values. 

1761 

1762 Examples 

1763 -------- 

1764 >>> ser = pd.Series( 

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

1766 ... index=pd.DatetimeIndex( 

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

1768 ... ), 

1769 ... ) 

1770 >>> ser 

1771 2023-01-01 1 

1772 2023-01-15 2 

1773 2023-02-01 3 

1774 2023-02-15 4 

1775 dtype: int64 

1776 

1777 >>> ser.groupby(["a", "a", "b", "b"]).idxmin() 

1778 a 2023-01-01 

1779 b 2023-02-01 

1780 dtype: datetime64[us] 

1781 """ 

1782 return self._idxmax_idxmin("idxmin", skipna=skipna) 

1783 

1784 def idxmax(self, skipna: bool = True) -> Series: 

1785 """ 

1786 Return the row label of the maximum value. 

1787 

1788 If multiple values equal the maximum, the first row label with that 

1789 value is returned. 

1790 

1791 Parameters 

1792 ---------- 

1793 skipna : bool, default True 

1794 Exclude NA values. 

1795 

1796 Returns 

1797 ------- 

1798 Series 

1799 Indexes of maxima in each group. 

1800 

1801 Raises 

1802 ------ 

1803 ValueError 

1804 When there are no valid values for a group. Then can happen if: 

1805 

1806 * There is an unobserved group and ``observed=False``. 

1807 * All values for a group are NA. 

1808 * Some values for a group are NA and ``skipna=False``. 

1809 

1810 .. versionchanged:: 3.0.0 

1811 Previously if all values for a group are NA or some values for a group are 

1812 NA and ``skipna=False``, this method would return NA. Now it raises instead. 

1813 

1814 See Also 

1815 -------- 

1816 numpy.argmax : Return indices of the maximum values 

1817 along the given axis. 

1818 DataFrame.idxmax : Return index of first occurrence of maximum 

1819 over requested axis. 

1820 Series.idxmin : Return index *label* of the first occurrence 

1821 of minimum of values. 

1822 

1823 Examples 

1824 -------- 

1825 >>> ser = pd.Series( 

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

1827 ... index=pd.DatetimeIndex( 

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

1829 ... ), 

1830 ... ) 

1831 >>> ser 

1832 2023-01-01 1 

1833 2023-01-15 2 

1834 2023-02-01 3 

1835 2023-02-15 4 

1836 dtype: int64 

1837 

1838 >>> ser.groupby(["a", "a", "b", "b"]).idxmax() 

1839 a 2023-01-15 

1840 b 2023-02-15 

1841 dtype: datetime64[us] 

1842 """ 

1843 return self._idxmax_idxmin("idxmax", skipna=skipna) 

1844 

1845 def corr( 

1846 self, 

1847 other: Series, 

1848 method: CorrelationMethod = "pearson", 

1849 min_periods: int | None = None, 

1850 ) -> Series: 

1851 """ 

1852 Compute correlation between each group and another Series. 

1853 

1854 Parameters 

1855 ---------- 

1856 other : Series 

1857 Series to compute correlation with. 

1858 method : {'pearson', 'kendall', 'spearman'}, default 'pearson' 

1859 Method of correlation to use. 

1860 min_periods : int, optional 

1861 Minimum number of observations required per pair of columns to 

1862 have a valid result. 

1863 

1864 Returns 

1865 ------- 

1866 Series 

1867 Correlation value for each group. 

1868 

1869 See Also 

1870 -------- 

1871 Series.corr : Equivalent method on ``Series``. 

1872 

1873 Examples 

1874 -------- 

1875 >>> s = pd.Series([1, 2, 3, 4], index=[0, 0, 1, 1]) 

1876 >>> g = s.groupby([0, 0, 1, 1]) 

1877 >>> g.corr() # doctest: +SKIP 

1878 """ 

1879 result = self._op_via_apply( 

1880 "corr", other=other, method=method, min_periods=min_periods 

1881 ) 

1882 return result 

1883 

1884 def cov( 

1885 self, other: Series, min_periods: int | None = None, ddof: int | None = 1 

1886 ) -> Series: 

1887 """ 

1888 Compute covariance between each group and another Series. 

1889 

1890 Parameters 

1891 ---------- 

1892 other : Series 

1893 Series to compute covariance with. 

1894 min_periods : int, optional 

1895 Minimum number of observations required per pair of columns to 

1896 have a valid result. 

1897 ddof : int, optional 

1898 Delta degrees of freedom for variance calculation. 

1899 

1900 Returns 

1901 ------- 

1902 Series 

1903 Covariance value for each group. 

1904 

1905 See Also 

1906 -------- 

1907 Series.cov : Equivalent method on ``Series``. 

1908 

1909 Examples 

1910 -------- 

1911 >>> s = pd.Series([1, 2, 3, 4], index=[0, 0, 1, 1]) 

1912 >>> g = s.groupby([0, 0, 1, 1]) 

1913 >>> g.cov() # doctest: +SKIP 

1914 """ 

1915 result = self._op_via_apply( 

1916 "cov", other=other, min_periods=min_periods, ddof=ddof 

1917 ) 

1918 return result 

1919 

1920 @property 

1921 def is_monotonic_increasing(self) -> Series: 

1922 """ 

1923 Return whether each group's values are monotonically increasing. 

1924 

1925 Returns 

1926 ------- 

1927 Series 

1928 

1929 See Also 

1930 -------- 

1931 SeriesGroupBy.is_monotonic_decreasing : Return whether each group's values 

1932 are monotonically decreasing. 

1933 

1934 Examples 

1935 -------- 

1936 >>> s = pd.Series([2, 1, 3, 4], index=["Falcon", "Falcon", "Parrot", "Parrot"]) 

1937 >>> s.groupby(level=0).is_monotonic_increasing 

1938 Falcon False 

1939 Parrot True 

1940 dtype: bool 

1941 """ 

1942 return self.apply(lambda ser: ser.is_monotonic_increasing) 

1943 

1944 @property 

1945 def is_monotonic_decreasing(self) -> Series: 

1946 """ 

1947 Return whether each group's values are monotonically decreasing. 

1948 

1949 Returns 

1950 ------- 

1951 Series 

1952 

1953 See Also 

1954 -------- 

1955 SeriesGroupBy.is_monotonic_increasing : Return whether each group's values 

1956 are monotonically increasing. 

1957 

1958 Examples 

1959 -------- 

1960 >>> s = pd.Series([2, 1, 3, 4], index=["Falcon", "Falcon", "Parrot", "Parrot"]) 

1961 >>> s.groupby(level=0).is_monotonic_decreasing 

1962 Falcon True 

1963 Parrot False 

1964 dtype: bool 

1965 """ 

1966 return self.apply(lambda ser: ser.is_monotonic_decreasing) 

1967 

1968 def hist( 

1969 self, 

1970 by=None, 

1971 ax=None, 

1972 grid: bool = True, 

1973 xlabelsize: int | None = None, 

1974 xrot: float | None = None, 

1975 ylabelsize: int | None = None, 

1976 yrot: float | None = None, 

1977 figsize: tuple[float, float] | None = None, 

1978 bins: int | Sequence[int] = 10, 

1979 backend: str | None = None, 

1980 legend: bool = False, 

1981 **kwargs, 

1982 ): 

1983 """ 

1984 Draw histogram for each group's values using :meth:`Series.hist` API. 

1985 

1986 Parameters 

1987 ---------- 

1988 by : object, optional 

1989 Grouping key. 

1990 ax : matplotlib.axes.Axes, optional 

1991 Axis to draw the histogram on. 

1992 grid : bool, default True 

1993 Show axis grid lines. 

1994 xlabelsize : int, default None 

1995 X axis label size. 

1996 xrot : float, default None 

1997 Rotation for x ticks. 

1998 ylabelsize : int, default None 

1999 Y axis label size. 

2000 yrot : float, default None 

2001 Rotation for y ticks. 

2002 figsize : tuple, optional 

2003 Figure size in inches. 

2004 bins : int or sequence, default 10 

2005 Number of histogram bins or bin edges. 

2006 backend : str or callable or None, optional 

2007 Plotting backend to use (e.g. 'matplotlib'). If None, use the default 

2008 plotting backend. 

2009 legend : bool, default False 

2010 Whether to draw the legend. 

2011 **kwargs 

2012 Additional keyword arguments passed to :meth:`Series.hist`. 

2013 

2014 Returns 

2015 ------- 

2016 matplotlib.axes.Axes or ndarray of Axes 

2017 The returned matplotlib axes or array of axes depending on input. 

2018 

2019 See Also 

2020 -------- 

2021 Series.hist : Equivalent histogram plotting method on Series. 

2022 

2023 Examples 

2024 -------- 

2025 >>> df = pd.DataFrame({"val": [1, 2, 2, 3, 3, 3]}, index=[0, 0, 1, 1, 2, 2]) 

2026 >>> g = df["val"].groupby([0, 0, 1, 1, 2, 2]) 

2027 >>> g.hist() # doctest: +SKIP 

2028 """ 

2029 result = self._op_via_apply( 

2030 "hist", 

2031 by=by, 

2032 ax=ax, 

2033 grid=grid, 

2034 xlabelsize=xlabelsize, 

2035 xrot=xrot, 

2036 ylabelsize=ylabelsize, 

2037 yrot=yrot, 

2038 figsize=figsize, 

2039 bins=bins, 

2040 backend=backend, 

2041 legend=legend, 

2042 **kwargs, 

2043 ) 

2044 return result 

2045 

2046 @property 

2047 def dtype(self) -> Series: 

2048 """ 

2049 Return the dtype object of the underlying data for each group. 

2050 

2051 Mirrors :meth:`Series.dtype` applied group-wise. 

2052 

2053 Returns 

2054 ------- 

2055 Series 

2056 Dtype of each group's values. 

2057 """ 

2058 return self.apply(lambda ser: ser.dtype) 

2059 

2060 def unique(self) -> Series: 

2061 """ 

2062 Return unique values for each group. 

2063 

2064 It returns unique values for each of the grouped values. Returned in 

2065 order of appearance. Hash table-based unique, therefore does NOT sort. 

2066 

2067 Returns 

2068 ------- 

2069 Series 

2070 Unique values for each of the grouped values. 

2071 

2072 See Also 

2073 -------- 

2074 Series.unique : Return unique values of Series object. 

2075 

2076 Examples 

2077 -------- 

2078 >>> df = pd.DataFrame( 

2079 ... [ 

2080 ... ("Chihuahua", "dog", 6.1), 

2081 ... ("Beagle", "dog", 15.2), 

2082 ... ("Chihuahua", "dog", 6.9), 

2083 ... ("Persian", "cat", 9.2), 

2084 ... ("Chihuahua", "dog", 7), 

2085 ... ("Persian", "cat", 8.8), 

2086 ... ], 

2087 ... columns=["breed", "animal", "height_in"], 

2088 ... ) 

2089 >>> df 

2090 breed animal height_in 

2091 0 Chihuahua dog 6.1 

2092 1 Beagle dog 15.2 

2093 2 Chihuahua dog 6.9 

2094 3 Persian cat 9.2 

2095 4 Chihuahua dog 7.0 

2096 5 Persian cat 8.8 

2097 >>> ser = df.groupby("animal")["breed"].unique() 

2098 >>> ser 

2099 animal 

2100 cat [Persian] 

2101 dog [Chihuahua, Beagle] 

2102 Name: breed, dtype: object 

2103 """ 

2104 result = self._op_via_apply("unique") 

2105 return result 

2106 

2107 

2108@set_module("pandas.api.typing") 

2109class DataFrameGroupBy(GroupBy[DataFrame]): 

2110 def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs): 

2111 """ 

2112 Aggregate using one or more operations. 

2113 

2114 The ``aggregate`` function allows the application of one or more aggregation 

2115 operations on groups of data within a DataFrameGroupBy object. It supports 

2116 various aggregation methods, including user-defined functions and predefined 

2117 functions such as 'sum', 'mean', etc. 

2118 

2119 Parameters 

2120 ---------- 

2121 func : function, str, list, dict or None 

2122 Function to use for aggregating the data. If a function, must either 

2123 work when passed a DataFrame or when passed to DataFrame.apply. 

2124 

2125 Accepted combinations are: 

2126 

2127 - function 

2128 - string function name 

2129 - list of functions and/or function names, e.g. ``[np.sum, 'mean']`` 

2130 - dict of index labels -> functions, function names or list of such. 

2131 - None, in which case ``**kwargs`` are used with Named Aggregation. Here the 

2132 output has one column for each element in ``**kwargs``. The name of the 

2133 column is keyword, whereas the value determines the aggregation used to 

2134 compute the values in the column. 

2135 

2136 Can also accept a Numba JIT function with 

2137 ``engine='numba'`` specified. Only passing a single function is supported 

2138 with this engine. 

2139 

2140 If the ``'numba'`` engine is chosen, the function must be 

2141 a user defined function with ``values`` and ``index`` as the 

2142 first and second arguments respectively in the function signature. 

2143 Each group's index will be passed to the user defined function 

2144 and optionally available for use. 

2145 

2146 *args 

2147 Positional arguments to pass to func. 

2148 engine : str, default None 

2149 * ``'cython'`` : Runs the function through C-extensions from cython. 

2150 * ``'numba'`` : Runs the function through JIT compiled code from numba. 

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

2152 ``compute.use_numba`` 

2153 

2154 engine_kwargs : dict, default None 

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

2156 * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil`` 

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

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

2159 ``{'nopython': True, 'nogil': False, 'parallel': False}`` and will be 

2160 applied to the function 

2161 

2162 **kwargs 

2163 * If ``func`` is None, ``**kwargs`` are used to define the output names and 

2164 aggregations via Named Aggregation. See ``func`` entry. 

2165 * Otherwise, keyword arguments to be passed into func. 

2166 

2167 Returns 

2168 ------- 

2169 DataFrame 

2170 Aggregated DataFrame based on the grouping and the applied aggregation 

2171 functions. 

2172 

2173 See Also 

2174 -------- 

2175 DataFrame.groupby.apply : Apply function func group-wise 

2176 and combine the results together. 

2177 DataFrame.groupby.transform : Transforms the Series on each group 

2178 based on the given function. 

2179 DataFrame.aggregate : Aggregate using one or more operations. 

2180 

2181 Notes 

2182 ----- 

2183 When using ``engine='numba'``, there will be no "fall back" behavior internally. 

2184 The group data and group index will be passed as numpy arrays to the JITed 

2185 user defined function, and no alternative execution attempts will be tried. 

2186 

2187 Functions that mutate the passed object can produce unexpected 

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

2189 for more details. 

2190 

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

2192 see the examples below. 

2193 

2194 Examples 

2195 -------- 

2196 >>> data = { 

2197 ... "A": [1, 1, 2, 2], 

2198 ... "B": [1, 2, 3, 4], 

2199 ... "C": [0.362838, 0.227877, 1.267767, -0.562860], 

2200 ... } 

2201 >>> df = pd.DataFrame(data) 

2202 >>> df 

2203 A B C 

2204 0 1 1 0.362838 

2205 1 1 2 0.227877 

2206 2 2 3 1.267767 

2207 3 2 4 -0.562860 

2208 

2209 The aggregation is for each column. 

2210 

2211 >>> df.groupby("A").agg("min") 

2212 B C 

2213 A 

2214 1 1 0.227877 

2215 2 3 -0.562860 

2216 

2217 Multiple aggregations 

2218 

2219 >>> df.groupby("A").agg(["min", "max"]) 

2220 B C 

2221 min max min max 

2222 A 

2223 1 1 2 0.227877 0.362838 

2224 2 3 4 -0.562860 1.267767 

2225 

2226 Select a column for aggregation 

2227 

2228 >>> df.groupby("A").B.agg(["min", "max"]) 

2229 min max 

2230 A 

2231 1 1 2 

2232 2 3 4 

2233 

2234 User-defined function for aggregation 

2235 

2236 >>> df.groupby("A").agg(lambda x: sum(x) + 2) 

2237 B C 

2238 A 

2239 1 5 2.590715 

2240 2 9 2.704907 

2241 

2242 Different aggregations per column 

2243 

2244 >>> df.groupby("A").agg({"B": ["min", "max"], "C": "sum"}) 

2245 B C 

2246 min max sum 

2247 A 

2248 1 1 2 0.590715 

2249 2 3 4 0.704907 

2250 

2251 To control the output names with different aggregations per column, 

2252 pandas supports "named aggregation" 

2253 

2254 >>> df.groupby("A").agg( 

2255 ... b_min=pd.NamedAgg(column="B", aggfunc="min"), 

2256 ... c_sum=pd.NamedAgg(column="C", aggfunc="sum"), 

2257 ... ) 

2258 b_min c_sum 

2259 A 

2260 1 1 0.590715 

2261 2 3 0.704907 

2262 

2263 - The keywords are the *output* column names 

2264 - The values are tuples whose first element is the column to select 

2265 and the second element is the aggregation to apply to that column. 

2266 Pandas provides the ``pandas.NamedAgg`` namedtuple with the fields 

2267 ``['column', 'aggfunc']`` to make it clearer what the arguments are. 

2268 As usual, the aggregation can be a callable or a string alias. 

2269 

2270 See :ref:`groupby.aggregate.named` for more. 

2271 

2272 The resulting dtype will reflect the return value of the aggregating 

2273 function. 

2274 

2275 >>> df.groupby("A")[["B"]].agg(lambda x: x.astype(float).min()) 

2276 B 

2277 A 

2278 1 1.0 

2279 2 3.0 

2280 """ 

2281 relabeling, func, columns, order = reconstruct_func(func, **kwargs) 

2282 func = maybe_mangle_lambdas(func) 

2283 

2284 if maybe_use_numba(engine): 

2285 # Not all agg functions support numba, only propagate numba kwargs 

2286 # if user asks for numba 

2287 kwargs["engine"] = engine 

2288 kwargs["engine_kwargs"] = engine_kwargs 

2289 

2290 op = GroupByApply(self, func, args=args, kwargs=kwargs) 

2291 result = op.agg() 

2292 if not is_dict_like(func) and result is not None: 

2293 # GH #52849 

2294 if not self.as_index and is_list_like(func): 

2295 return result.reset_index() 

2296 else: 

2297 return result 

2298 elif relabeling: 

2299 # this should be the only (non-raising) case with relabeling 

2300 # used reordered index of columns 

2301 result = cast(DataFrame, result) 

2302 result = result.iloc[:, order] 

2303 result = cast(DataFrame, result) 

2304 # error: Incompatible types in assignment (expression has type 

2305 # "Optional[List[str]]", variable has type 

2306 # "Union[Union[Union[ExtensionArray, ndarray[Any, Any]], 

2307 # Index, Series], Sequence[Any]]") 

2308 result.columns = columns # type: ignore[assignment] 

2309 

2310 if result is None: 

2311 # Remove the kwargs we inserted 

2312 # (already stored in engine, engine_kwargs arguments) 

2313 if "engine" in kwargs: 

2314 del kwargs["engine"] 

2315 del kwargs["engine_kwargs"] 

2316 # at this point func is not a str, list-like, dict-like, 

2317 # or a known callable(e.g. sum) 

2318 if maybe_use_numba(engine): 

2319 return self._aggregate_with_numba( 

2320 func, *args, engine_kwargs=engine_kwargs, **kwargs 

2321 ) 

2322 # grouper specific aggregations 

2323 if self._grouper.nkeys > 1: 

2324 # test_groupby_as_index_series_scalar gets here with 'not self.as_index' 

2325 return self._python_agg_general(func, *args, **kwargs) 

2326 elif args or kwargs: 

2327 # test_pass_args_kwargs gets here (with and without as_index) 

2328 # can't return early 

2329 result = self._aggregate_frame(func, *args, **kwargs) 

2330 

2331 else: 

2332 # try to treat as if we are passing a list 

2333 gba = GroupByApply(self, [func], args=(), kwargs={}) 

2334 try: 

2335 result = gba.agg() 

2336 

2337 except ValueError as err: 

2338 if "No objects to concatenate" not in str(err): 

2339 raise 

2340 # _aggregate_frame can fail with e.g. func=Series.mode, 

2341 # where it expects 1D values but would be getting 2D values 

2342 # In other tests, using aggregate_frame instead of GroupByApply 

2343 # would give correct values but incorrect dtypes 

2344 # object vs float64 in test_cython_agg_empty_buckets 

2345 # float64 vs int64 in test_category_order_apply 

2346 result = self._aggregate_frame(func) 

2347 

2348 else: 

2349 # GH#32040, GH#35246 

2350 # e.g. test_groupby_as_index_select_column_sum_empty_df 

2351 result = cast(DataFrame, result) 

2352 result.columns = self._obj_with_exclusions.columns.copy() 

2353 

2354 if not self.as_index: 

2355 result = self._insert_inaxis_grouper(result) 

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

2357 

2358 return result 

2359 

2360 agg = aggregate 

2361 

2362 def _python_agg_general(self, func, *args, **kwargs): 

2363 f = lambda x: func(x, *args, **kwargs) 

2364 

2365 if self.ngroups == 0: 

2366 # e.g. test_evaluate_with_empty_groups different path gets different 

2367 # result dtype in empty case. 

2368 return self._python_apply_general(f, self._selected_obj, is_agg=True) 

2369 

2370 obj = self._obj_with_exclusions 

2371 

2372 if not len(obj.columns): 

2373 # e.g. test_margins_no_values_no_cols 

2374 return self._python_apply_general(f, self._selected_obj) 

2375 

2376 output: dict[int, ArrayLike] = {} 

2377 for idx, (name, ser) in enumerate(obj.items()): 

2378 result = self._grouper.agg_series(ser, f) 

2379 output[idx] = result 

2380 

2381 res = self.obj._constructor(output) 

2382 res.columns = obj.columns.copy(deep=False) 

2383 return self._wrap_aggregated_output(res) 

2384 

2385 def _aggregate_frame(self, func, *args, **kwargs) -> DataFrame: 

2386 if self._grouper.nkeys != 1: 

2387 raise AssertionError("Number of keys must be 1") 

2388 

2389 obj = self._obj_with_exclusions 

2390 

2391 result: dict[Hashable, NDFrame | np.ndarray] = {} 

2392 for name, grp_df in self._grouper.get_iterator(obj): 

2393 fres = func(grp_df, *args, **kwargs) 

2394 result[name] = fres 

2395 

2396 result_index = self._grouper.result_index 

2397 out = self.obj._constructor(result, index=obj.columns, columns=result_index) 

2398 out = out.T 

2399 

2400 return out 

2401 

2402 def _wrap_applied_output( 

2403 self, 

2404 data: DataFrame, 

2405 values: list, 

2406 not_indexed_same: bool = False, 

2407 is_transform: bool = False, 

2408 ): 

2409 if len(values) == 0: 

2410 if is_transform: 

2411 # GH#47787 see test_group_on_empty_multiindex 

2412 res_index = data.index 

2413 elif not self.group_keys: 

2414 res_index = None 

2415 else: 

2416 res_index = self._grouper.result_index 

2417 

2418 result = self.obj._constructor(index=res_index, columns=data.columns) 

2419 result = result.astype(data.dtypes) 

2420 return result 

2421 

2422 # GH12824 

2423 # using values[0] here breaks test_groupby_apply_none_first 

2424 first_not_none = next(com.not_none(*values), None) 

2425 

2426 if first_not_none is None: 

2427 # GH9684 - All values are None, return an empty frame 

2428 # GH57775 - Ensure that columns and dtypes from original frame are kept. 

2429 result = self.obj._constructor(columns=data.columns) 

2430 result = result.astype(data.dtypes) 

2431 return result 

2432 elif isinstance(first_not_none, DataFrame): 

2433 return self._concat_objects( 

2434 values, 

2435 not_indexed_same=not_indexed_same, 

2436 is_transform=is_transform, 

2437 ) 

2438 

2439 key_index = self._grouper.result_index if self.as_index else None 

2440 

2441 if isinstance(first_not_none, (np.ndarray, Index)): 

2442 # GH#1738: values is list of arrays of unequal lengths 

2443 # fall through to the outer else clause 

2444 # TODO: sure this is right? we used to do this 

2445 # after raising AttributeError above 

2446 # GH 18930 

2447 if not is_hashable(self._selection): 

2448 # error: Need type annotation for "name" 

2449 name = tuple(self._selection) # type: ignore[var-annotated, arg-type] 

2450 else: 

2451 # error: Incompatible types in assignment 

2452 # (expression has type "Hashable", variable 

2453 # has type "Tuple[Any, ...]") 

2454 name = self._selection # type: ignore[assignment] 

2455 return self.obj._constructor_sliced(values, index=key_index, name=name) 

2456 elif not isinstance(first_not_none, Series): 

2457 # values are not series or array-like but scalars 

2458 # self._selection not passed through to Series as the 

2459 # result should not take the name of original selection 

2460 # of columns 

2461 if self.as_index: 

2462 return self.obj._constructor_sliced(values, index=key_index) 

2463 else: 

2464 result = self.obj._constructor(values, columns=[self._selection]) 

2465 result = self._insert_inaxis_grouper(result) 

2466 return result 

2467 else: 

2468 # values are Series 

2469 return self._wrap_applied_output_series( 

2470 values, 

2471 not_indexed_same, 

2472 first_not_none, 

2473 key_index, 

2474 is_transform, 

2475 ) 

2476 

2477 def _wrap_applied_output_series( 

2478 self, 

2479 values: list[Series], 

2480 not_indexed_same: bool, 

2481 first_not_none, 

2482 key_index: Index | None, 

2483 is_transform: bool, 

2484 ) -> DataFrame | Series: 

2485 kwargs = first_not_none._construct_axes_dict() 

2486 backup = Series(**kwargs) 

2487 values = [x if (x is not None) else backup for x in values] 

2488 

2489 all_indexed_same = all_indexes_same(x.index for x in values) 

2490 

2491 if not all_indexed_same: 

2492 # GH 8467 

2493 return self._concat_objects( 

2494 values, 

2495 not_indexed_same=True, 

2496 is_transform=is_transform, 

2497 ) 

2498 

2499 # Combine values 

2500 # vstack+constructor is faster than concat and handles MI-columns 

2501 stacked_values = np.vstack([np.asarray(v) for v in values]) 

2502 

2503 index = key_index 

2504 columns = first_not_none.index.copy() 

2505 if columns.name is None: 

2506 # GH6124 - propagate name of Series when it's consistent 

2507 names = {v.name for v in values} 

2508 if len(names) == 1: 

2509 columns.name = next(iter(names)) 

2510 

2511 if stacked_values.dtype == object: 

2512 # We'll have the DataFrame constructor do inference 

2513 stacked_values = stacked_values.tolist() 

2514 result = self.obj._constructor(stacked_values, index=index, columns=columns) 

2515 

2516 if not self.as_index: 

2517 result = self._insert_inaxis_grouper(result) 

2518 

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

2520 

2521 def _cython_transform( 

2522 self, 

2523 how: str, 

2524 numeric_only: bool = False, 

2525 **kwargs, 

2526 ) -> DataFrame: 

2527 # We have multi-block tests 

2528 # e.g. test_rank_min_int, test_cython_transform_frame 

2529 # test_transform_numeric_ret 

2530 mgr: BlockManager = self._get_data_to_aggregate( 

2531 numeric_only=numeric_only, name=how 

2532 ) 

2533 

2534 def arr_func(bvalues: ArrayLike) -> ArrayLike: 

2535 return self._grouper._cython_operation( 

2536 "transform", bvalues, how, 1, **kwargs 

2537 ) 

2538 

2539 res_mgr = mgr.apply(arr_func) 

2540 

2541 res_df = self.obj._constructor_from_mgr(res_mgr, axes=res_mgr.axes) 

2542 return res_df 

2543 

2544 def _transform_general(self, func, engine, engine_kwargs, *args, **kwargs): 

2545 if maybe_use_numba(engine): 

2546 return self._transform_with_numba( 

2547 func, *args, engine_kwargs=engine_kwargs, **kwargs 

2548 ) 

2549 from pandas.core.reshape.concat import concat 

2550 

2551 applied = [] 

2552 obj = self._obj_with_exclusions 

2553 gen = self._grouper.get_iterator(obj) 

2554 fast_path, slow_path = self._define_paths(func, *args, **kwargs) 

2555 

2556 # Determine whether to use slow or fast path by evaluating on the first group. 

2557 # Need to handle the case of an empty generator and process the result so that 

2558 # it does not need to be computed again. 

2559 try: 

2560 name, group = next(gen) 

2561 except StopIteration: 

2562 pass 

2563 else: 

2564 # 2023-02-27 No tests broken by disabling this pinning 

2565 object.__setattr__(group, "name", name) 

2566 try: 

2567 path, res = self._choose_path(fast_path, slow_path, group) 

2568 except ValueError as err: 

2569 # e.g. test_transform_with_non_scalar_group 

2570 msg = "transform must return a scalar value for each group" 

2571 raise ValueError(msg) from err 

2572 if group.size > 0: 

2573 res = _wrap_transform_general_frame(self.obj, group, res) 

2574 applied.append(res) 

2575 

2576 # Compute and process with the remaining groups 

2577 for name, group in gen: 

2578 if group.size == 0: 

2579 continue 

2580 # 2023-02-27 No tests broken by disabling this pinning 

2581 object.__setattr__(group, "name", name) 

2582 res = path(group) 

2583 

2584 res = _wrap_transform_general_frame(self.obj, group, res) 

2585 applied.append(res) 

2586 

2587 concat_index = obj.columns 

2588 concatenated = concat( 

2589 applied, axis=0, verify_integrity=False, ignore_index=True 

2590 ) 

2591 concatenated = concatenated.reindex(concat_index, axis=1) 

2592 return self._set_result_index_ordered(concatenated) 

2593 

2594 __examples_dataframe_doc = dedent( 

2595 """ 

2596 >>> df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar', 

2597 ... 'foo', 'bar'], 

2598 ... 'B' : ['one', 'one', 'two', 'three', 

2599 ... 'two', 'two'], 

2600 ... 'C' : [1, 5, 5, 2, 5, 5], 

2601 ... 'D' : [2.0, 5., 8., 1., 2., 9.]}) 

2602 >>> grouped = df.groupby('A')[['C', 'D']] 

2603 >>> grouped.transform(lambda x: (x - x.mean()) / x.std()) 

2604 C D 

2605 0 -1.154701 -0.577350 

2606 1 0.577350 0.000000 

2607 2 0.577350 1.154701 

2608 3 -1.154701 -1.000000 

2609 4 0.577350 -0.577350 

2610 5 0.577350 1.000000 

2611 

2612 Broadcast result of the transformation 

2613 

2614 >>> grouped.transform(lambda x: x.max() - x.min()) 

2615 C D 

2616 0 4.0 6.0 

2617 1 3.0 8.0 

2618 2 4.0 6.0 

2619 3 3.0 8.0 

2620 4 4.0 6.0 

2621 5 3.0 8.0 

2622 

2623 >>> grouped.transform("mean") 

2624 C D 

2625 0 3.666667 4.0 

2626 1 4.000000 5.0 

2627 2 3.666667 4.0 

2628 3 4.000000 5.0 

2629 4 3.666667 4.0 

2630 5 4.000000 5.0 

2631 

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

2633 for example: 

2634 

2635 >>> grouped.transform(lambda x: x.astype(int).max()) 

2636 C D 

2637 0 5 8 

2638 1 5 9 

2639 2 5 8 

2640 3 5 9 

2641 4 5 8 

2642 5 5 9 

2643 """ 

2644 ) 

2645 

2646 def transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs): 

2647 """ 

2648 Call function producing a same-indexed DataFrame on each group. 

2649 

2650 Returns a DataFrame having the same indexes as the original object 

2651 filled with the transformed values. 

2652 

2653 Parameters 

2654 ---------- 

2655 func : function, str 

2656 Function to apply to each group. 

2657 See the Notes section below for requirements. 

2658 

2659 Accepted inputs are: 

2660 

2661 - String 

2662 - Python function 

2663 - Numba JIT function with ``engine='numba'`` specified. 

2664 

2665 Only passing a single function is supported with this engine. 

2666 If the ``'numba'`` engine is chosen, the function must be 

2667 a user defined function with ``values`` and ``index`` as the 

2668 first and second arguments respectively in the function signature. 

2669 Each group's index will be passed to the user defined function 

2670 and optionally available for use. 

2671 

2672 If a string is chosen, then it needs to be the name 

2673 of the groupby method you want to use. 

2674 *args 

2675 Positional arguments to pass to func. 

2676 engine : str, default None 

2677 * ``'cython'`` : Runs the function through C-extensions from cython. 

2678 * ``'numba'`` : Runs the function through JIT compiled code from numba. 

2679 * ``None`` : Defaults to ``'cython'`` 

2680 or the global setting ``compute.use_numba`` 

2681 

2682 engine_kwargs : dict, default None 

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

2684 * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil`` 

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

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

2687 ``{'nopython': True, 'nogil': False, 'parallel': False}`` and will be 

2688 applied to the function 

2689 

2690 **kwargs 

2691 Keyword arguments to be passed into func. 

2692 

2693 Returns 

2694 ------- 

2695 DataFrame 

2696 DataFrame with the same indexes as the original object filled 

2697 with transformed values. 

2698 

2699 See Also 

2700 -------- 

2701 DataFrame.groupby.apply : Apply function ``func`` group-wise and combine 

2702 the results together. 

2703 DataFrame.groupby.aggregate : Aggregate using one or more operations. 

2704 DataFrame.transform : Call ``func`` on self producing a DataFrame with the 

2705 same axis shape as self. 

2706 

2707 Notes 

2708 ----- 

2709 Each group is endowed the attribute 'name' in case you need to know 

2710 which group you are working on. 

2711 

2712 The current implementation imposes three requirements on f: 

2713 

2714 * f must return a value that either has the same shape as the input 

2715 subframe or can be broadcast to the shape of the input subframe. 

2716 For example, if `f` returns a scalar it will be broadcast to have the 

2717 same shape as the input subframe. 

2718 * if this is a DataFrame, f must support application column-by-column 

2719 in the subframe. If f also supports application to the entire subframe, 

2720 then a fast path is used starting from the second chunk. 

2721 * f must not mutate groups. Mutation is not supported and may 

2722 produce unexpected results. See :ref:`gotchas.udf-mutation` for more details. 

2723 

2724 When using ``engine='numba'``, there will be no "fall back" behavior internally. 

2725 The group data and group index will be passed as numpy arrays to the JITed 

2726 user defined function, and no alternative execution attempts will be tried. 

2727 

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

2729 see the examples below. 

2730 

2731 .. versionchanged:: 2.0.0 

2732 

2733 When using ``.transform`` on a grouped DataFrame 

2734 and the transformation function returns a DataFrame, 

2735 pandas now aligns the result's index with the input's index. 

2736 You can call ``.to_numpy()`` on the result of the 

2737 transformation function to avoid alignment. 

2738 

2739 Examples 

2740 -------- 

2741 

2742 >>> df = pd.DataFrame( 

2743 ... { 

2744 ... "A": ["foo", "bar", "foo", "bar", "foo", "bar"], 

2745 ... "B": ["one", "one", "two", "three", "two", "two"], 

2746 ... "C": [1, 5, 5, 2, 5, 5], 

2747 ... "D": [2.0, 5.0, 8.0, 1.0, 2.0, 9.0], 

2748 ... } 

2749 ... ) 

2750 >>> grouped = df.groupby("A")[["C", "D"]] 

2751 >>> grouped.transform(lambda x: (x - x.mean()) / x.std()) 

2752 C D 

2753 0 -1.154701 -0.577350 

2754 1 0.577350 0.000000 

2755 2 0.577350 1.154701 

2756 3 -1.154701 -1.000000 

2757 4 0.577350 -0.577350 

2758 5 0.577350 1.000000 

2759 

2760 Broadcast result of the transformation 

2761 

2762 >>> grouped.transform(lambda x: x.max() - x.min()) 

2763 C D 

2764 0 4.0 6.0 

2765 1 3.0 8.0 

2766 2 4.0 6.0 

2767 3 3.0 8.0 

2768 4 4.0 6.0 

2769 5 3.0 8.0 

2770 

2771 >>> grouped.transform("mean") 

2772 C D 

2773 0 3.666667 4.0 

2774 1 4.000000 5.0 

2775 2 3.666667 4.0 

2776 3 4.000000 5.0 

2777 4 3.666667 4.0 

2778 5 4.000000 5.0 

2779 

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

2781 for example: 

2782 

2783 >>> grouped.transform(lambda x: x.astype(int).max()) 

2784 C D 

2785 0 5 8 

2786 1 5 9 

2787 2 5 8 

2788 3 5 9 

2789 4 5 8 

2790 5 5 9 

2791 """ 

2792 return self._transform( 

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

2794 ) 

2795 

2796 def _define_paths(self, func, *args, **kwargs): 

2797 if isinstance(func, str): 

2798 fast_path = lambda group: getattr(group, func)(*args, **kwargs) 

2799 slow_path = lambda group: group.apply( 

2800 lambda x: getattr(x, func)(*args, **kwargs), axis=0 

2801 ) 

2802 else: 

2803 fast_path = lambda group: func(group, *args, **kwargs) 

2804 slow_path = lambda group: group.apply( 

2805 lambda x: func(x, *args, **kwargs), axis=0 

2806 ) 

2807 return fast_path, slow_path 

2808 

2809 def _choose_path(self, fast_path: Callable, slow_path: Callable, group: DataFrame): 

2810 path = slow_path 

2811 res = slow_path(group) 

2812 

2813 if self.ngroups == 1: 

2814 # no need to evaluate multiple paths when only 

2815 # a single group exists 

2816 return path, res 

2817 

2818 # if we make it here, test if we can use the fast path 

2819 try: 

2820 res_fast = fast_path(group) 

2821 except AssertionError: 

2822 raise # pragma: no cover 

2823 except Exception: 

2824 # GH#29631 For user-defined function, we can't predict what may be 

2825 # raised; see test_transform.test_transform_fastpath_raises 

2826 return path, res 

2827 

2828 # verify fast path returns either: 

2829 # a DataFrame with columns equal to group.columns 

2830 # OR a Series with index equal to group.columns 

2831 if isinstance(res_fast, DataFrame): 

2832 if not res_fast.columns.equals(group.columns): 

2833 return path, res 

2834 elif isinstance(res_fast, Series): 

2835 if not res_fast.index.equals(group.columns): 

2836 return path, res 

2837 else: 

2838 return path, res 

2839 

2840 if res_fast.equals(res): 

2841 path = fast_path 

2842 

2843 return path, res 

2844 

2845 def filter(self, func, dropna: bool = True, *args, **kwargs) -> DataFrame: 

2846 """ 

2847 Filter elements from groups that don't satisfy a criterion. 

2848 

2849 Elements from groups are filtered if they do not satisfy the 

2850 boolean criterion specified by func. 

2851 

2852 Parameters 

2853 ---------- 

2854 func : function 

2855 Criterion to apply to each group. Should return True or False. 

2856 dropna : bool 

2857 Drop groups that do not pass the filter. True by default; if False, 

2858 groups that evaluate False are filled with NaNs. 

2859 *args : tuple 

2860 Additional positional arguments to pass to `func`. 

2861 **kwargs : dict 

2862 Additional keyword arguments to pass to `func`. 

2863 

2864 Returns 

2865 ------- 

2866 DataFrame 

2867 The filtered subset of the original DataFrame. 

2868 

2869 See Also 

2870 -------- 

2871 DataFrame.filter: Filter elements of ungrouped DataFrame. 

2872 SeriesGroupBy.filter : Filter elements from groups base on criterion. 

2873 

2874 Notes 

2875 ----- 

2876 Each subframe is endowed the attribute 'name' in case you need to know 

2877 which group you are working on. 

2878 

2879 Functions that mutate the passed object can produce unexpected 

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

2881 for more details. 

2882 

2883 Examples 

2884 -------- 

2885 >>> df = pd.DataFrame( 

2886 ... { 

2887 ... "A": ["foo", "bar", "foo", "bar", "foo", "bar"], 

2888 ... "B": [1, 2, 3, 4, 5, 6], 

2889 ... "C": [2.0, 5.0, 8.0, 1.0, 2.0, 9.0], 

2890 ... } 

2891 ... ) 

2892 >>> grouped = df.groupby("A") 

2893 >>> grouped.filter(lambda x: x["B"].mean() > 3.0) 

2894 A B C 

2895 1 bar 2 5.0 

2896 3 bar 4 1.0 

2897 5 bar 6 9.0 

2898 """ 

2899 indices = [] 

2900 

2901 obj = self._selected_obj 

2902 gen = self._grouper.get_iterator(obj) 

2903 

2904 for name, group in gen: 

2905 # 2023-02-27 no tests are broken this pinning, but it is documented in the 

2906 # docstring above. 

2907 object.__setattr__(group, "name", name) 

2908 

2909 res = func(group, *args, **kwargs) 

2910 

2911 try: 

2912 res = res.squeeze() 

2913 except AttributeError: # allow e.g., scalars and frames to pass 

2914 pass 

2915 

2916 # interpret the result of the filter 

2917 if is_bool(res) or (is_scalar(res) and isna(res)): 

2918 if notna(res) and res: 

2919 indices.append(self._get_index(name)) 

2920 else: 

2921 # non scalars aren't allowed 

2922 raise TypeError( 

2923 f"filter function returned a {type(res).__name__}, " 

2924 "but expected a scalar bool" 

2925 ) 

2926 

2927 return self._apply_filter(indices, dropna) 

2928 

2929 def __getitem__(self, key) -> DataFrameGroupBy | SeriesGroupBy: 

2930 # per GH 23566 

2931 if isinstance(key, tuple) and len(key) > 1: 

2932 # if len == 1, then it becomes a SeriesGroupBy and this is actually 

2933 # valid syntax, so don't raise 

2934 raise ValueError( 

2935 "Cannot subset columns with a tuple with more than one element. " 

2936 "Use a list instead." 

2937 ) 

2938 return super().__getitem__(key) 

2939 

2940 def _gotitem(self, key, ndim: int, subset=None): 

2941 """ 

2942 sub-classes to define 

2943 return a sliced object 

2944 

2945 Parameters 

2946 ---------- 

2947 key : string / list of selections 

2948 ndim : {1, 2} 

2949 requested ndim of result 

2950 subset : object, default None 

2951 subset to act on 

2952 """ 

2953 if ndim == 2: 

2954 if subset is None: 

2955 subset = self.obj 

2956 return DataFrameGroupBy( 

2957 subset, 

2958 self.keys, 

2959 level=self.level, 

2960 grouper=self._grouper, 

2961 exclusions=self.exclusions, 

2962 selection=key, 

2963 as_index=self.as_index, 

2964 sort=self.sort, 

2965 group_keys=self.group_keys, 

2966 observed=self.observed, 

2967 dropna=self.dropna, 

2968 ) 

2969 elif ndim == 1: 

2970 if subset is None: 

2971 subset = self.obj[key] 

2972 return SeriesGroupBy( 

2973 subset, 

2974 self.keys, 

2975 level=self.level, 

2976 grouper=self._grouper, 

2977 exclusions=self.exclusions, 

2978 selection=key, 

2979 as_index=self.as_index, 

2980 sort=self.sort, 

2981 group_keys=self.group_keys, 

2982 observed=self.observed, 

2983 dropna=self.dropna, 

2984 ) 

2985 

2986 raise AssertionError("invalid ndim for _gotitem") 

2987 

2988 def _get_data_to_aggregate( 

2989 self, *, numeric_only: bool = False, name: str | None = None 

2990 ) -> BlockManager: 

2991 obj = self._obj_with_exclusions 

2992 mgr = obj._mgr 

2993 if numeric_only: 

2994 mgr = mgr.get_numeric_data() 

2995 return mgr 

2996 

2997 def _wrap_agged_manager(self, mgr: BlockManager) -> DataFrame: 

2998 return self.obj._constructor_from_mgr(mgr, axes=mgr.axes) 

2999 

3000 def _apply_to_column_groupbys(self, func) -> DataFrame: 

3001 from pandas.core.reshape.concat import concat 

3002 

3003 obj = self._obj_with_exclusions 

3004 columns = obj.columns 

3005 sgbs = ( 

3006 SeriesGroupBy( 

3007 obj.iloc[:, i], 

3008 selection=colname, 

3009 grouper=self._grouper, 

3010 exclusions=self.exclusions, 

3011 observed=self.observed, 

3012 ) 

3013 for i, colname in enumerate(obj.columns) 

3014 ) 

3015 results = [func(sgb) for sgb in sgbs] 

3016 

3017 if not results: 

3018 # concat would raise 

3019 res_df = DataFrame([], columns=columns, index=self._grouper.result_index) 

3020 else: 

3021 res_df = concat(results, keys=columns, axis=1) 

3022 

3023 if not self.as_index: 

3024 res_df.index = default_index(len(res_df)) 

3025 res_df = self._insert_inaxis_grouper(res_df) 

3026 return res_df 

3027 

3028 def nunique(self, dropna: bool = True) -> DataFrame: 

3029 """ 

3030 Return DataFrame with counts of unique elements in each position. 

3031 

3032 Parameters 

3033 ---------- 

3034 dropna : bool, default True 

3035 Don't include NaN in the counts. 

3036 

3037 Returns 

3038 ------- 

3039 nunique: DataFrame 

3040 Counts of unique elements in each position. 

3041 

3042 See Also 

3043 -------- 

3044 DataFrame.nunique : Count number of distinct elements in specified axis. 

3045 

3046 Examples 

3047 -------- 

3048 >>> df = pd.DataFrame( 

3049 ... { 

3050 ... "id": ["spam", "egg", "egg", "spam", "ham", "ham"], 

3051 ... "value1": [1, 5, 5, 2, 5, 5], 

3052 ... "value2": list("abbaxy"), 

3053 ... } 

3054 ... ) 

3055 >>> df 

3056 id value1 value2 

3057 0 spam 1 a 

3058 1 egg 5 b 

3059 2 egg 5 b 

3060 3 spam 2 a 

3061 4 ham 5 x 

3062 5 ham 5 y 

3063 

3064 >>> df.groupby("id").nunique() 

3065 value1 value2 

3066 id 

3067 egg 1 1 

3068 ham 1 2 

3069 spam 2 1 

3070 

3071 Check for rows with the same id but conflicting values: 

3072 

3073 >>> df.groupby("id").filter(lambda g: (g.nunique() > 1).any()) 

3074 id value1 value2 

3075 0 spam 1 a 

3076 3 spam 2 a 

3077 4 ham 5 x 

3078 5 ham 5 y 

3079 """ 

3080 return self._apply_to_column_groupbys(lambda sgb: sgb.nunique(dropna)) 

3081 

3082 def idxmax( 

3083 self, 

3084 skipna: bool = True, 

3085 numeric_only: bool = False, 

3086 ) -> DataFrame: 

3087 """ 

3088 Return index of first occurrence of maximum in each group. 

3089 

3090 Parameters 

3091 ---------- 

3092 skipna : bool, default True 

3093 Exclude NA values. 

3094 numeric_only : bool, default False 

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

3096 

3097 Returns 

3098 ------- 

3099 DataFrame 

3100 Indexes of maxima in each column according to the group. 

3101 

3102 Raises 

3103 ------ 

3104 ValueError 

3105 When there are no valid values for a group. Then can happen if: 

3106 

3107 * There is an unobserved group and ``observed=False``. 

3108 * All values for a group are NA. 

3109 * Some values for a group are NA and ``skipna=False``. 

3110 

3111 .. versionchanged:: 3.0.0 

3112 Previously if all values for a group are NA or some values for a group are 

3113 NA and ``skipna=False``, this method would return NA. Now it raises instead. 

3114 

3115 See Also 

3116 -------- 

3117 Series.idxmax : Return index of the maximum element. 

3118 DataFrame.idxmax : Indexes of maxima along the specified axis. 

3119 

3120 Notes 

3121 ----- 

3122 This method is the DataFrame version of ``ndarray.argmax``. 

3123 

3124 Examples 

3125 -------- 

3126 Consider a dataset containing food consumption in Argentina. 

3127 

3128 >>> df = pd.DataFrame( 

3129 ... { 

3130 ... "consumption": [10.51, 103.11, 55.48], 

3131 ... "co2_emissions": [37.2, 19.66, 1712], 

3132 ... "food_type": ["meat", "plant", "meat"], 

3133 ... }, 

3134 ... index=["Pork", "Wheat Products", "Beef"], 

3135 ... ) 

3136 

3137 >>> df 

3138 consumption co2_emissions food_type 

3139 Pork 10.51 37.20 meat 

3140 Wheat Products 103.11 19.66 plant 

3141 Beef 55.48 1712.00 meat 

3142 

3143 By default, it returns the index for the maximum value in each column 

3144 according to the group. 

3145 

3146 >>> df.groupby("food_type").idxmax() 

3147 consumption co2_emissions 

3148 food_type 

3149 meat Beef Beef 

3150 plant Wheat Products Wheat Products 

3151 """ 

3152 return self._idxmax_idxmin("idxmax", numeric_only=numeric_only, skipna=skipna) 

3153 

3154 def idxmin( 

3155 self, 

3156 skipna: bool = True, 

3157 numeric_only: bool = False, 

3158 ) -> DataFrame: 

3159 """ 

3160 Return index of first occurrence of minimum in each group. 

3161 

3162 Parameters 

3163 ---------- 

3164 skipna : bool, default True 

3165 Exclude NA values. 

3166 numeric_only : bool, default False 

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

3168 

3169 Returns 

3170 ------- 

3171 DataFrame 

3172 Indexes of minima in each column according to the group. 

3173 

3174 Raises 

3175 ------ 

3176 ValueError 

3177 When there are no valid values for a group. Then can happen if: 

3178 

3179 * There is an unobserved group and ``observed=False``. 

3180 * All values for a group are NA. 

3181 * Some values for a group are NA and ``skipna=False``. 

3182 

3183 .. versionchanged:: 3.0.0 

3184 Previously if all values for a group are NA or some values for a group are 

3185 NA and ``skipna=False``, this method would return NA. Now it raises instead. 

3186 

3187 See Also 

3188 -------- 

3189 Series.idxmin : Return index of the minimum element. 

3190 DataFrame.idxmin : Indexes of minima along the specified axis. 

3191 

3192 Notes 

3193 ----- 

3194 This method is the DataFrame version of ``ndarray.argmin``. 

3195 

3196 Examples 

3197 -------- 

3198 Consider a dataset containing food consumption in Argentina. 

3199 

3200 >>> df = pd.DataFrame( 

3201 ... { 

3202 ... "consumption": [10.51, 103.11, 55.48], 

3203 ... "co2_emissions": [37.2, 19.66, 1712], 

3204 ... "food_type": ["meat", "plant", "meat"], 

3205 ... }, 

3206 ... index=["Pork", "Wheat Products", "Beef"], 

3207 ... ) 

3208 

3209 >>> df 

3210 consumption co2_emissions food_type 

3211 Pork 10.51 37.20 meat 

3212 Wheat Products 103.11 19.66 plant 

3213 Beef 55.48 1712.00 meat 

3214 

3215 By default, it returns the index for the minimum value in each column 

3216 according to the group. 

3217 

3218 >>> df.groupby("food_type").idxmin() 

3219 consumption co2_emissions 

3220 food_type 

3221 meat Pork Pork 

3222 plant Wheat Products Wheat Products 

3223 """ 

3224 return self._idxmax_idxmin("idxmin", numeric_only=numeric_only, skipna=skipna) 

3225 

3226 boxplot = boxplot_frame_groupby 

3227 

3228 def value_counts( 

3229 self, 

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

3231 normalize: bool = False, 

3232 sort: bool = True, 

3233 ascending: bool = False, 

3234 dropna: bool = True, 

3235 ) -> DataFrame | Series: 

3236 """ 

3237 Return a Series or DataFrame containing counts of unique rows. 

3238 

3239 Parameters 

3240 ---------- 

3241 subset : list-like, optional 

3242 Columns to use when counting unique combinations. 

3243 normalize : bool, default False 

3244 Return proportions rather than frequencies. 

3245 sort : bool, default True 

3246 Stable sort by frequencies when True. When False, non-grouping 

3247 columns will appear in the order they occur in within groups. 

3248 

3249 .. versionchanged:: 3.0.0 

3250 

3251 In prior versions, ``sort=False`` would sort the non-grouping columns 

3252 by label. 

3253 ascending : bool, default False 

3254 Sort in ascending order. 

3255 dropna : bool, default True 

3256 Don't include counts of rows that contain NA values. 

3257 

3258 Returns 

3259 ------- 

3260 Series or DataFrame 

3261 Series if the groupby ``as_index`` is True, otherwise DataFrame. 

3262 

3263 See Also 

3264 -------- 

3265 Series.value_counts: Equivalent method on Series. 

3266 DataFrame.value_counts: Equivalent method on DataFrame. 

3267 SeriesGroupBy.value_counts: Equivalent method on SeriesGroupBy. 

3268 

3269 Notes 

3270 ----- 

3271 - If the groupby ``as_index`` is True then the returned Series will have a 

3272 MultiIndex with one level per input column. 

3273 - If the groupby ``as_index`` is False then the returned DataFrame will have an 

3274 additional column with the value_counts. The column is labelled 'count' or 

3275 'proportion', depending on the ``normalize`` parameter. 

3276 

3277 By default, rows that contain any NA values are omitted from 

3278 the result. 

3279 

3280 By default, the result will be in descending order so that the 

3281 first element of each group is the most frequently-occurring row. 

3282 

3283 Examples 

3284 -------- 

3285 >>> df = pd.DataFrame( 

3286 ... { 

3287 ... "gender": ["male", "male", "female", "male", "female", "male"], 

3288 ... "education": ["low", "medium", "high", "low", "high", "low"], 

3289 ... "country": ["US", "FR", "US", "FR", "FR", "FR"], 

3290 ... } 

3291 ... ) 

3292 

3293 >>> df 

3294 gender education country 

3295 0 male low US 

3296 1 male medium FR 

3297 2 female high US 

3298 3 male low FR 

3299 4 female high FR 

3300 5 male low FR 

3301 

3302 >>> df.groupby("gender").value_counts() 

3303 gender education country 

3304 female high US 1 

3305 FR 1 

3306 male low FR 2 

3307 US 1 

3308 medium FR 1 

3309 Name: count, dtype: int64 

3310 

3311 >>> df.groupby("gender").value_counts(ascending=True) 

3312 gender education country 

3313 female high US 1 

3314 FR 1 

3315 male low US 1 

3316 medium FR 1 

3317 low FR 2 

3318 Name: count, dtype: int64 

3319 

3320 >>> df.groupby("gender").value_counts(normalize=True) 

3321 gender education country 

3322 female high US 0.50 

3323 FR 0.50 

3324 male low FR 0.50 

3325 US 0.25 

3326 medium FR 0.25 

3327 Name: proportion, dtype: float64 

3328 

3329 >>> df.groupby("gender", as_index=False).value_counts() 

3330 gender education country count 

3331 0 female high US 1 

3332 1 female high FR 1 

3333 2 male low FR 2 

3334 3 male low US 1 

3335 4 male medium FR 1 

3336 

3337 >>> df.groupby("gender", as_index=False).value_counts(normalize=True) 

3338 gender education country proportion 

3339 0 female high US 0.50 

3340 1 female high FR 0.50 

3341 2 male low FR 0.50 

3342 3 male low US 0.25 

3343 4 male medium FR 0.25 

3344 """ 

3345 return self._value_counts(subset, normalize, sort, ascending, dropna) 

3346 

3347 def take( 

3348 self, 

3349 indices: TakeIndexer, 

3350 **kwargs, 

3351 ) -> DataFrame: 

3352 """ 

3353 Return the elements in the given *positional* indices in each group. 

3354 

3355 This means that we are not indexing according to actual values in 

3356 the index attribute of the object. We are indexing according to the 

3357 actual position of the element in the object. 

3358 

3359 If a requested index does not exist for some group, this method will raise. 

3360 To get similar behavior that ignores indices that don't exist, see 

3361 :meth:`.DataFrameGroupBy.nth`. 

3362 

3363 Parameters 

3364 ---------- 

3365 indices : array-like 

3366 An array of ints indicating which positions to take. 

3367 

3368 **kwargs 

3369 For compatibility with :meth:`numpy.take`. Has no effect on the 

3370 output. 

3371 

3372 Returns 

3373 ------- 

3374 DataFrame 

3375 A DataFrame containing the elements taken from each group. 

3376 

3377 See Also 

3378 -------- 

3379 DataFrame.take : Take elements from a Series along an axis. 

3380 DataFrame.loc : Select a subset of a DataFrame by labels. 

3381 DataFrame.iloc : Select a subset of a DataFrame by positions. 

3382 numpy.take : Take elements from an array along an axis. 

3383 

3384 Examples 

3385 -------- 

3386 >>> df = pd.DataFrame( 

3387 ... [ 

3388 ... ("falcon", "bird", 389.0), 

3389 ... ("parrot", "bird", 24.0), 

3390 ... ("lion", "mammal", 80.5), 

3391 ... ("monkey", "mammal", np.nan), 

3392 ... ("rabbit", "mammal", 15.0), 

3393 ... ], 

3394 ... columns=["name", "class", "max_speed"], 

3395 ... index=[4, 3, 2, 1, 0], 

3396 ... ) 

3397 >>> df 

3398 name class max_speed 

3399 4 falcon bird 389.0 

3400 3 parrot bird 24.0 

3401 2 lion mammal 80.5 

3402 1 monkey mammal NaN 

3403 0 rabbit mammal 15.0 

3404 >>> gb = df.groupby([1, 1, 2, 2, 2]) 

3405 

3406 Take elements at rows 0 and 1. 

3407 

3408 Note how the indices selected in the result do not correspond to 

3409 our input indices 0 and 1. That's because we are selecting the 0th 

3410 and 1st rows, not rows whose indices equal 0 and 1. 

3411 

3412 >>> gb.take([0, 1]) 

3413 name class max_speed 

3414 1 4 falcon bird 389.0 

3415 3 parrot bird 24.0 

3416 2 2 lion mammal 80.5 

3417 1 monkey mammal NaN 

3418 

3419 The order of the specified indices influences the order in the result. 

3420 Here, the order is swapped from the previous example. 

3421 

3422 >>> gb.take([1, 0]) 

3423 name class max_speed 

3424 1 3 parrot bird 24.0 

3425 4 falcon bird 389.0 

3426 2 1 monkey mammal NaN 

3427 2 lion mammal 80.5 

3428 

3429 We may take elements using negative integers for positive indices, 

3430 starting from the end of the object, just like with Python lists. 

3431 

3432 >>> gb.take([-1, -2]) 

3433 name class max_speed 

3434 1 3 parrot bird 24.0 

3435 4 falcon bird 389.0 

3436 2 0 rabbit mammal 15.0 

3437 1 monkey mammal NaN 

3438 """ 

3439 result = self._op_via_apply("take", indices=indices, **kwargs) 

3440 return result 

3441 

3442 def skew( 

3443 self, 

3444 skipna: bool = True, 

3445 numeric_only: bool = False, 

3446 **kwargs, 

3447 ) -> DataFrame: 

3448 """ 

3449 Return unbiased skew within groups. 

3450 

3451 Normalized by N-1. 

3452 

3453 Parameters 

3454 ---------- 

3455 skipna : bool, default True 

3456 Exclude NA/null values when computing the result. 

3457 

3458 numeric_only : bool, default False 

3459 Include only float, int, boolean columns. 

3460 

3461 **kwargs 

3462 Additional keyword arguments to be passed to the function. 

3463 

3464 Returns 

3465 ------- 

3466 DataFrame 

3467 Unbiased skew within groups. 

3468 

3469 See Also 

3470 -------- 

3471 DataFrame.skew : Return unbiased skew over requested axis. 

3472 

3473 Examples 

3474 -------- 

3475 >>> arrays = [ 

3476 ... ["falcon", "parrot", "cockatoo", "kiwi", "lion", "monkey", "rabbit"], 

3477 ... ["bird", "bird", "bird", "bird", "mammal", "mammal", "mammal"], 

3478 ... ] 

3479 >>> index = pd.MultiIndex.from_arrays(arrays, names=("name", "class")) 

3480 >>> df = pd.DataFrame( 

3481 ... {"max_speed": [389.0, 24.0, 70.0, np.nan, 80.5, 21.5, 15.0]}, 

3482 ... index=index, 

3483 ... ) 

3484 >>> df 

3485 max_speed 

3486 name class 

3487 falcon bird 389.0 

3488 parrot bird 24.0 

3489 cockatoo bird 70.0 

3490 kiwi bird NaN 

3491 lion mammal 80.5 

3492 monkey mammal 21.5 

3493 rabbit mammal 15.0 

3494 >>> gb = df.groupby(["class"]) 

3495 >>> gb.skew() 

3496 max_speed 

3497 class 

3498 bird 1.628296 

3499 mammal 1.669046 

3500 >>> gb.skew(skipna=False) 

3501 max_speed 

3502 class 

3503 bird NaN 

3504 mammal 1.669046 

3505 """ 

3506 

3507 def alt(obj): 

3508 # This should not be reached since the cython path should raise 

3509 # TypeError and not NotImplementedError. 

3510 raise TypeError(f"'skew' is not supported for dtype={obj.dtype}") 

3511 

3512 return self._cython_agg_general( 

3513 "skew", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs 

3514 ) 

3515 

3516 def kurt( 

3517 self, 

3518 skipna: bool = True, 

3519 numeric_only: bool = False, 

3520 **kwargs, 

3521 ) -> DataFrame: 

3522 """ 

3523 Return unbiased kurtosis within groups. 

3524 

3525 Parameters 

3526 ---------- 

3527 skipna : bool, default True 

3528 Exclude NA/null values when computing the result. 

3529 

3530 numeric_only : bool, default False 

3531 Include only float, int, boolean columns. 

3532 

3533 **kwargs 

3534 Additional keyword arguments to be passed to the function. 

3535 

3536 Returns 

3537 ------- 

3538 DataFrame 

3539 Unbiased kurtosis within groups. 

3540 

3541 See Also 

3542 -------- 

3543 DataFrame.kurt : Return unbiased kurtosis over requested axis. 

3544 

3545 Examples 

3546 -------- 

3547 >>> arrays = [ 

3548 ... [ 

3549 ... "falcon", 

3550 ... "parrot", 

3551 ... "cockatoo", 

3552 ... "kiwi", 

3553 ... "eagle", 

3554 ... "lion", 

3555 ... "monkey", 

3556 ... "rabbit", 

3557 ... "dog", 

3558 ... "wolf", 

3559 ... ], 

3560 ... [ 

3561 ... "bird", 

3562 ... "bird", 

3563 ... "bird", 

3564 ... "bird", 

3565 ... "bird", 

3566 ... "mammal", 

3567 ... "mammal", 

3568 ... "mammal", 

3569 ... "mammal", 

3570 ... "mammal", 

3571 ... ], 

3572 ... ] 

3573 >>> index = pd.MultiIndex.from_arrays(arrays, names=("name", "class")) 

3574 >>> df = pd.DataFrame( 

3575 ... { 

3576 ... "max_speed": [ 

3577 ... 389.0, 

3578 ... 24.0, 

3579 ... 70.0, 

3580 ... np.nan, 

3581 ... 350.0, 

3582 ... 80.5, 

3583 ... 21.5, 

3584 ... 15.0, 

3585 ... 40.0, 

3586 ... 50.0, 

3587 ... ] 

3588 ... }, 

3589 ... index=index, 

3590 ... ) 

3591 >>> df 

3592 max_speed 

3593 name class 

3594 falcon bird 389.0 

3595 parrot bird 24.0 

3596 cockatoo bird 70.0 

3597 kiwi bird NaN 

3598 eagle bird 350.0 

3599 lion mammal 80.5 

3600 monkey mammal 21.5 

3601 rabbit mammal 15.0 

3602 dog mammal 40.0 

3603 wolf mammal 50.0 

3604 >>> gb = df.groupby(["class"]) 

3605 >>> gb.kurt() 

3606 max_speed 

3607 class 

3608 bird -5.493277 

3609 mammal 0.204125 

3610 >>> gb.kurt(skipna=False) 

3611 max_speed 

3612 class 

3613 bird NaN 

3614 mammal 0.204125 

3615 """ 

3616 

3617 return self._cython_agg_general( 

3618 "kurt", alt=None, skipna=skipna, numeric_only=numeric_only, **kwargs 

3619 ) 

3620 

3621 @property 

3622 def plot(self) -> GroupByPlot: 

3623 """ 

3624 Make plots of groups from a DataFrame. 

3625 

3626 Uses the backend specified by the option ``plotting.backend``. 

3627 By default, matplotlib is used. 

3628 

3629 Returns 

3630 ------- 

3631 GroupByPlot 

3632 A plotting object that can be used to create plots for each group. 

3633 

3634 See Also 

3635 -------- 

3636 DataFrame.plot : Make plots of DataFrame. 

3637 

3638 Examples 

3639 -------- 

3640 >>> df = pd.DataFrame( 

3641 ... {"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=["a", "a", "b", "b"] 

3642 ... ) 

3643 >>> g = df.groupby(level=0) 

3644 >>> g.plot() # doctest: +SKIP 

3645 """ 

3646 result = GroupByPlot(self) 

3647 return result 

3648 

3649 def corr( 

3650 self, 

3651 method: str | Callable[[np.ndarray, np.ndarray], float] = "pearson", 

3652 min_periods: int = 1, 

3653 numeric_only: bool = False, 

3654 ) -> DataFrame: 

3655 """ 

3656 Compute pairwise correlation of columns, excluding NA/null values. 

3657 

3658 Parameters 

3659 ---------- 

3660 method : {'pearson', 'kendall', 'spearman'} or callable 

3661 Method of correlation: 

3662 

3663 * pearson : standard correlation coefficient 

3664 * kendall : Kendall Tau correlation coefficient 

3665 * spearman : Spearman rank correlation 

3666 * callable: callable with input two 1d ndarrays 

3667 and returning a float. Note that the returned matrix from corr 

3668 will have 1 along the diagonals and will be symmetric 

3669 regardless of the callable's behavior. 

3670 min_periods : int, optional 

3671 Minimum number of observations required per pair of columns 

3672 to have a valid result. Currently only available for Pearson 

3673 and Spearman correlation. 

3674 numeric_only : bool, default False 

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

3676 

3677 .. versionchanged:: 2.0.0 

3678 The default value of ``numeric_only`` is now ``False``. 

3679 

3680 Returns 

3681 ------- 

3682 DataFrame 

3683 Correlation matrix. 

3684 

3685 See Also 

3686 -------- 

3687 DataFrame.corrwith : Compute pairwise correlation with another 

3688 DataFrame or Series. 

3689 Series.corr : Compute the correlation between two Series. 

3690 

3691 Notes 

3692 ----- 

3693 Pearson, Kendall and Spearman correlation are currently computed using 

3694 pairwise complete observations. 

3695 

3696 * `Pearson correlation coefficient <https://en.wikipedia.org/wiki/Pearson_correlation_coefficient>`_ 

3697 * `Kendall rank correlation coefficient <https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient>`_ 

3698 * `Spearman's rank correlation coefficient <https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient>`_ 

3699 

3700 Examples 

3701 -------- 

3702 >>> df = pd.DataFrame( 

3703 ... { 

3704 ... "age": [2, 3, 4, 6, 6, 1, 2, 1], 

3705 ... "weight": [2.1, 3.2, 4.1, 6.5, 3.3, 2.1, 4.1, 1.9], 

3706 ... "pet": ["dog", "cat", "dog", "cat", "dog", "cat", "dog", "cat"], 

3707 ... } 

3708 ... ) 

3709 >>> df 

3710 age weight pet 

3711 0 2 2.1 dog 

3712 1 3 3.2 cat 

3713 2 4 4.1 dog 

3714 3 6 6.5 cat 

3715 4 6 3.3 dog 

3716 5 1 2.1 cat 

3717 6 2 4.1 dog 

3718 7 1 1.9 cat 

3719 >>> df.groupby("pet").corr() 

3720 age weight 

3721 pet 

3722 cat age 1.000000 0.989321 

3723 weight 0.989321 1.000000 

3724 dog age 1.000000 0.184177 

3725 weight 0.184177 1.000000 

3726 """ 

3727 result = self._op_via_apply( 

3728 "corr", method=method, min_periods=min_periods, numeric_only=numeric_only 

3729 ) 

3730 return result 

3731 

3732 def cov( 

3733 self, 

3734 min_periods: int | None = None, 

3735 ddof: int | None = 1, 

3736 numeric_only: bool = False, 

3737 ) -> DataFrame: 

3738 """ 

3739 Compute pairwise covariance of columns, excluding NA/null values. 

3740 

3741 Compute the pairwise covariance among the series of a DataFrame. 

3742 The returned data frame is the `covariance matrix 

3743 <https://en.wikipedia.org/wiki/Covariance_matrix>`__ of the columns 

3744 of the DataFrame. 

3745 

3746 Both NA and null values are automatically excluded from the 

3747 calculation. (See the note below about bias from missing values.) 

3748 A threshold can be set for the minimum number of 

3749 observations for each value created. Comparisons with observations 

3750 below this threshold will be returned as ``NaN``. 

3751 

3752 This method is generally used for the analysis of time series data to 

3753 understand the relationship between different measures 

3754 across time. 

3755 

3756 Parameters 

3757 ---------- 

3758 min_periods : int, optional 

3759 Minimum number of observations required per pair of columns 

3760 to have a valid result. 

3761 

3762 ddof : int, default 1 

3763 Delta degrees of freedom. The divisor used in calculations 

3764 is ``N - ddof``, where ``N`` represents the number of elements. 

3765 This argument is applicable only when no ``nan`` is in the dataframe. 

3766 

3767 numeric_only : bool, default False 

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

3769 

3770 .. versionchanged:: 2.0.0 

3771 The default value of ``numeric_only`` is now ``False``. 

3772 

3773 Returns 

3774 ------- 

3775 DataFrame 

3776 The covariance matrix of the series of the DataFrame. 

3777 

3778 See Also 

3779 -------- 

3780 Series.cov : Compute covariance with another Series. 

3781 core.window.ewm.ExponentialMovingWindow.cov : Exponential weighted sample 

3782 covariance. 

3783 core.window.expanding.Expanding.cov : Expanding sample covariance. 

3784 core.window.rolling.Rolling.cov : Rolling sample covariance. 

3785 

3786 Notes 

3787 ----- 

3788 Returns the covariance matrix of the DataFrame's time series. 

3789 The covariance is normalized by N-ddof. 

3790 

3791 For DataFrames that have Series that are missing data (assuming that 

3792 data is `missing at random 

3793 <https://en.wikipedia.org/wiki/Missing_data#Missing_at_random>`__) 

3794 the returned covariance matrix will be an unbiased estimate 

3795 of the variance and covariance between the member Series. 

3796 

3797 However, for many applications this estimate may not be acceptable 

3798 because the estimate covariance matrix is not guaranteed to be positive 

3799 semi-definite. This could lead to estimate correlations having 

3800 absolute values which are greater than one, and/or a non-invertible 

3801 covariance matrix. See `Estimation of covariance matrices 

3802 <https://en.wikipedia.org/w/index.php?title=Estimation_of_covariance_ 

3803 matrices>`__ for more details. 

3804 

3805 Examples 

3806 -------- 

3807 >>> df = pd.DataFrame( 

3808 ... { 

3809 ... "age": [2, 3, 4, 6, 6, 1, 2, 1], 

3810 ... "weight": [2.1, 3.2, 4.1, 6.5, 3.3, 2.1, 4.1, 1.9], 

3811 ... "pet": ["dog", "cat", "dog", "cat", "dog", "cat", "dog", "cat"], 

3812 ... } 

3813 ... ) 

3814 >>> df 

3815 age weight pet 

3816 0 2 2.1 dog 

3817 1 3 3.2 cat 

3818 2 4 4.1 dog 

3819 3 6 6.5 cat 

3820 4 6 3.3 dog 

3821 5 1 2.1 cat 

3822 6 2 4.1 dog 

3823 7 1 1.9 cat 

3824 >>> df.groupby("pet").cov() 

3825 age weight 

3826 pet 

3827 cat age 5.583333 4.975000 

3828 weight 4.975000 4.529167 

3829 dog age 3.666667 0.333333 

3830 weight 0.333333 0.893333 

3831 """ 

3832 result = self._op_via_apply( 

3833 "cov", min_periods=min_periods, ddof=ddof, numeric_only=numeric_only 

3834 ) 

3835 return result 

3836 

3837 def hist( 

3838 self, 

3839 column: IndexLabel | None = None, 

3840 by=None, 

3841 grid: bool = True, 

3842 xlabelsize: int | None = None, 

3843 xrot: float | None = None, 

3844 ylabelsize: int | None = None, 

3845 yrot: float | None = None, 

3846 ax=None, 

3847 sharex: bool = False, 

3848 sharey: bool = False, 

3849 figsize: tuple[float, float] | None = None, 

3850 layout: tuple[int, int] | None = None, 

3851 bins: int | Sequence[int] = 10, 

3852 backend: str | None = None, 

3853 legend: bool = False, 

3854 **kwargs, 

3855 ): 

3856 """ 

3857 Make a histogram of the DataFrame's columns. 

3858 

3859 A `histogram`_ is a representation of the distribution of data. 

3860 This function calls :meth:`matplotlib.pyplot.hist`, on each series in 

3861 the DataFrame, resulting in one histogram per column. 

3862 

3863 .. _histogram: https://en.wikipedia.org/wiki/Histogram 

3864 

3865 Parameters 

3866 ---------- 

3867 column : str or sequence, optional 

3868 If passed, will be used to limit data to a subset of columns. 

3869 by : object, optional 

3870 If passed, then used to form histograms for separate groups. 

3871 grid : bool, default True 

3872 Whether to show axis grid lines. 

3873 xlabelsize : int, default None 

3874 If specified changes the x-axis label size. 

3875 xrot : float, default None 

3876 Rotation of x axis labels. For example, a value of 90 displays the 

3877 x labels rotated 90 degrees clockwise. 

3878 ylabelsize : int, default None 

3879 If specified changes the y-axis label size. 

3880 yrot : float, default None 

3881 Rotation of y axis labels. For example, a value of 90 displays the 

3882 y labels rotated 90 degrees clockwise. 

3883 ax : Matplotlib axes object, default None 

3884 The axes to plot the histogram on. 

3885 sharex : bool, default True if ax is None else False 

3886 In case subplots=True, share x axis and set some x axis labels to 

3887 invisible; defaults to True if ax is None otherwise False if an ax 

3888 is passed in. 

3889 Note that passing in both an ax and sharex=True will alter all x axis 

3890 labels for all subplots in a figure. 

3891 sharey : bool, default False 

3892 In case subplots=True, share y axis and set some y axis labels to 

3893 invisible. 

3894 figsize : tuple, optional 

3895 The size in inches of the figure to create. Uses the value in 

3896 `matplotlib.rcParams` by default. 

3897 layout : tuple, optional 

3898 Tuple of (rows, columns) for the layout of the histograms. 

3899 bins : int or sequence, default 10 

3900 Number of histogram bins to be used. If an integer is given, bins + 1 

3901 bin edges are calculated and returned. If bins is a sequence, gives 

3902 bin edges, including left edge of first bin and right edge of last 

3903 bin. In this case, bins is returned unmodified. 

3904 

3905 backend : str, default None 

3906 Backend to use instead of the backend specified in the option 

3907 ``plotting.backend``. For instance, 'matplotlib'. Alternatively, to 

3908 specify the ``plotting.backend`` for the whole session, set 

3909 ``pd.options.plotting.backend``. 

3910 

3911 legend : bool, default False 

3912 Whether to show the legend. 

3913 

3914 **kwargs 

3915 All other plotting keyword arguments to be passed to 

3916 :meth:`matplotlib.pyplot.hist`. 

3917 

3918 Returns 

3919 ------- 

3920 matplotlib.Axes or numpy.ndarray 

3921 A ``matplotlib.Axes`` object or an array of ``Axes`` objects, depending on 

3922 the layout and grouping. 

3923 

3924 See Also 

3925 -------- 

3926 matplotlib.pyplot.hist : Plot a histogram using matplotlib. 

3927 

3928 Examples 

3929 -------- 

3930 This example draws a histogram based on the length and width of 

3931 some animals, displayed in three bins 

3932 

3933 .. plot:: 

3934 :context: close-figs 

3935 

3936 >>> data = { 

3937 ... "length": [1.5, 0.5, 1.2, 0.9, 3], 

3938 ... "width": [0.7, 0.2, 0.15, 0.2, 1.1], 

3939 ... } 

3940 >>> index = ["pig", "rabbit", "duck", "chicken", "horse"] 

3941 >>> df = pd.DataFrame(data, index=index) 

3942 >>> hist = df.groupby("length").hist(bins=3) 

3943 """ 

3944 result = self._op_via_apply( 

3945 "hist", 

3946 column=column, 

3947 by=by, 

3948 grid=grid, 

3949 xlabelsize=xlabelsize, 

3950 xrot=xrot, 

3951 ylabelsize=ylabelsize, 

3952 yrot=yrot, 

3953 ax=ax, 

3954 sharex=sharex, 

3955 sharey=sharey, 

3956 figsize=figsize, 

3957 layout=layout, 

3958 bins=bins, 

3959 backend=backend, 

3960 legend=legend, 

3961 **kwargs, 

3962 ) 

3963 return result 

3964 

3965 def corrwith( 

3966 self, 

3967 other: DataFrame | Series, 

3968 drop: bool = False, 

3969 method: CorrelationMethod = "pearson", 

3970 numeric_only: bool = False, 

3971 ) -> DataFrame: 

3972 """ 

3973 Compute pairwise correlation. 

3974 

3975 .. deprecated:: 3.0.0 

3976 

3977 Pairwise correlation is computed between rows or columns of 

3978 DataFrame with rows or columns of Series or DataFrame. DataFrames 

3979 are first aligned along both axes before computing the 

3980 correlations. 

3981 

3982 Parameters 

3983 ---------- 

3984 other : DataFrame, Series 

3985 Object with which to compute correlations. 

3986 drop : bool, default False 

3987 Drop missing indices from result. 

3988 method : {'pearson', 'kendall', 'spearman'} or callable 

3989 Method of correlation: 

3990 

3991 * pearson : standard correlation coefficient 

3992 * kendall : Kendall Tau correlation coefficient 

3993 * spearman : Spearman rank correlation 

3994 * callable: callable with input two 1d ndarrays 

3995 and returning a float. 

3996 

3997 numeric_only : bool, default False 

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

3999 

4000 .. versionchanged:: 2.0.0 

4001 The default value of ``numeric_only`` is now ``False``. 

4002 

4003 Returns 

4004 ------- 

4005 Series 

4006 Pairwise correlations. 

4007 

4008 See Also 

4009 -------- 

4010 DataFrame.corr : Compute pairwise correlation of columns. 

4011 

4012 Examples 

4013 -------- 

4014 >>> df1 = pd.DataFrame( 

4015 ... { 

4016 ... "Day": [1, 1, 1, 2, 2, 2, 3, 3, 3], 

4017 ... "Data": [6, 6, 8, 5, 4, 2, 7, 3, 9], 

4018 ... } 

4019 ... ) 

4020 >>> df2 = pd.DataFrame( 

4021 ... { 

4022 ... "Day": [1, 1, 1, 2, 2, 2, 3, 3, 3], 

4023 ... "Data": [5, 3, 8, 3, 1, 1, 2, 3, 6], 

4024 ... } 

4025 ... ) 

4026 

4027 >>> df1.groupby("Day").corrwith(df2) 

4028 Data Day 

4029 Day 

4030 1 0.917663 NaN 

4031 2 0.755929 NaN 

4032 3 0.576557 NaN 

4033 """ 

4034 warnings.warn( 

4035 "DataFrameGroupBy.corrwith is deprecated", 

4036 Pandas4Warning, 

4037 stacklevel=find_stack_level(), 

4038 ) 

4039 result = self._op_via_apply( 

4040 "corrwith", 

4041 other=other, 

4042 drop=drop, 

4043 method=method, 

4044 numeric_only=numeric_only, 

4045 ) 

4046 return result 

4047 

4048 

4049def _wrap_transform_general_frame( 

4050 obj: DataFrame, group: DataFrame, res: DataFrame | Series 

4051) -> DataFrame: 

4052 from pandas import concat 

4053 

4054 if isinstance(res, Series): 

4055 # we need to broadcast across the 

4056 # other dimension; this will preserve dtypes 

4057 # GH14457 

4058 if res.index.is_(obj.index): 

4059 res_frame = concat([res] * len(group.columns), axis=1, ignore_index=True) 

4060 res_frame.columns = group.columns 

4061 res_frame.index = group.index 

4062 else: 

4063 res_frame = obj._constructor( 

4064 np.tile(res.values, (len(group.index), 1)), 

4065 columns=group.columns, 

4066 index=group.index, 

4067 ) 

4068 assert isinstance(res_frame, DataFrame) 

4069 return res_frame 

4070 elif isinstance(res, DataFrame) and not res.index.is_(group.index): 

4071 return res._align_frame(group)[0] 

4072 else: 

4073 return res