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

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

663 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 typing import ( 

16 TYPE_CHECKING, 

17 Any, 

18 Literal, 

19 TypeAlias, 

20 TypeVar, 

21 cast, 

22) 

23import warnings 

24 

25import numpy as np 

26 

27from pandas._libs.hashtable import duplicated 

28from pandas.errors import ( 

29 Pandas4Warning, 

30 SpecificationError, 

31) 

32from pandas.util._decorators import set_module 

33from pandas.util._exceptions import find_stack_level 

34 

35from pandas.core.dtypes.common import ( 

36 ensure_int64, 

37 is_bool, 

38 is_dict_like, 

39 is_integer_dtype, 

40 is_list_like, 

41 is_numeric_dtype, 

42 is_scalar, 

43) 

44from pandas.core.dtypes.dtypes import ( 

45 CategoricalDtype, 

46 IntervalDtype, 

47) 

48from pandas.core.dtypes.inference import is_hashable 

49from pandas.core.dtypes.missing import ( 

50 isna, 

51 notna, 

52) 

53 

54from pandas.core import algorithms 

55from pandas.core.apply import ( 

56 GroupByApply, 

57 maybe_mangle_lambdas, 

58 reconstruct_func, 

59 validate_func_kwargs, 

60) 

61import pandas.core.common as com 

62from pandas.core.frame import DataFrame 

63from pandas.core.groupby import base 

64from pandas.core.groupby.groupby import ( 

65 GroupBy, 

66 GroupByPlot, 

67) 

68from pandas.core.indexes.api import ( 

69 Index, 

70 MultiIndex, 

71 all_indexes_same, 

72 default_index, 

73) 

74from pandas.core.series import Series 

75from pandas.core.sorting import get_group_index 

76from pandas.core.util.numba_ import maybe_use_numba 

77 

78from pandas.plotting import boxplot_frame_groupby 

79 

80if TYPE_CHECKING: 

81 from collections.abc import ( 

82 Hashable, 

83 Sequence, 

84 ) 

85 

86 from pandas._libs import Interval 

87 from pandas._typing import ( 

88 ArrayLike, 

89 BlockManager, 

90 CorrelationMethod, 

91 IndexLabel, 

92 Manager, 

93 SingleBlockManager, 

94 TakeIndexer, 

95 ) 

96 

97 from pandas import Categorical 

98 

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

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

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

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

103# See note at _mangle_lambda_list 

104ScalarResult = TypeVar("ScalarResult") 

105 

106 

107@set_module("pandas") 

108@dataclasses.dataclass 

109class NamedAgg: 

110 """ 

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

112 

113 A dataclass used with :meth:`DataFrame.groupby().agg() 

114 <pandas.core.groupby.DataFrameGroupBy.aggregate>` to specify an 

115 aggregation on a particular column and assign a custom name to the 

116 resulting column. Additional positional and keyword arguments can be 

117 forwarded to the aggregation function. 

118 

119 Parameters 

120 ---------- 

121 column : Hashable 

122 Column label in the DataFrame to apply aggfunc. 

123 aggfunc : function or str 

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

125 pandas function. 

126 *args, **kwargs : Any 

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

128 

129 See Also 

130 -------- 

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

132 

133 Examples 

134 -------- 

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

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

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

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

139 result_a result_1 

140 key 

141 1 -1 10.5 

142 2 1 12.0 

143 

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

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

146 

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

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

149 count_between 

150 key 

151 1 1 

152 2 1 

153 

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

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

156 count_between_kw 

157 key 

158 1 1 

159 2 1 

160 """ 

161 

162 column: Hashable 

163 aggfunc: AggScalar 

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

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

166 

167 def __init__( 

168 self, 

169 column: Hashable, 

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

171 *args: Any, 

172 **kwargs: Any, 

173 ) -> None: 

174 self.column = column 

175 self.aggfunc = aggfunc 

176 self.args = args 

177 self.kwargs = kwargs 

178 

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

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

181 if key == 0: 

182 return self.column 

183 elif key == 1: 

184 return self.aggfunc 

185 elif key == 2: 

186 return self.args 

187 elif key == 3: 

188 return self.kwargs 

189 raise IndexError("index out of range") 

190 

191 

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

193class SeriesGroupBy(GroupBy[Series]): 

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

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

196 out._name = self.obj.name 

197 return out 

198 

199 def _get_data_to_aggregate( 

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

201 ) -> SingleBlockManager: 

202 ser = self._obj_with_exclusions 

203 single = ser._mgr 

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

205 # GH#41291 match Series behavior 

206 kwd_name = "numeric_only" 

207 raise TypeError( 

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

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

210 ) 

211 return single 

212 

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

214 """ 

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

216 

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

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

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

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

221 grouping method. 

222 

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

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

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

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

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

228 

229 Parameters 

230 ---------- 

231 func : callable 

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

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

234 callable may take positional and keyword arguments. 

235 

236 *args : tuple 

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

238 

239 **kwargs : dict 

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

241 

242 Returns 

243 ------- 

244 Series or DataFrame 

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

246 

247 See Also 

248 -------- 

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

250 group. 

251 aggregate : Apply aggregate function to the GroupBy object. 

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

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

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

255 

256 Notes 

257 ----- 

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

259 

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

261 see the examples below. 

262 

263 Functions that mutate the passed object can produce unexpected 

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

265 for more details. 

266 

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

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

269 identifying the current group, as in the examples below. 

270 

271 Examples 

272 -------- 

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

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

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

276 

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

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

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

280 we can get different grouping results: 

281 

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

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

284 each group together into a new Series. 

285 

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

287 

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

289 a 0.0 

290 a 2.0 

291 b 1.0 

292 dtype: float64 

293 

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

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

296 

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

298 a a 0.0 

299 a 2.0 

300 b b 1.0 

301 dtype: float64 

302 

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

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

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

306 appropriate: 

307 

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

309 a 1 

310 b 0 

311 dtype: int64 

312 

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

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

315 to the input. 

316 

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

318 a 1 

319 b 0 

320 dtype: int64 

321 """ 

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

323 

324 def aggregate( 

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

326 ) -> Series | DataFrame: 

327 """ 

328 Aggregate using one or more operations. 

329 

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

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

332 optimized JIT-compiled functions. 

333 

334 Parameters 

335 ---------- 

336 func : function, str, list or None 

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

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

339 

340 Accepted combinations are: 

341 

342 - function 

343 - string function name 

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

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

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

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

348 used to compute the values in the column. 

349 

350 Can also accept a Numba JIT function with 

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

352 with this engine. 

353 

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

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

356 first and second arguments respectively in the function signature. 

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

358 and optionally available for use. 

359 

360 *args 

361 Positional arguments to pass to func. 

362 engine : str, default None 

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

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

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

366 ``compute.use_numba`` 

367 

368 engine_kwargs : dict, default None 

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

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

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

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

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

374 applied to the function 

375 

376 **kwargs 

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

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

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

380 

381 Returns 

382 ------- 

383 Series 

384 Aggregated Series based on the grouping and the applied aggregation 

385 functions. 

386 

387 See Also 

388 -------- 

389 SeriesGroupBy.apply : Apply function func group-wise 

390 and combine the results together. 

391 SeriesGroupBy.transform : Transforms the Series on each group 

392 based on the given function. 

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

394 

395 Notes 

396 ----- 

397 See :ref:`groupby.aggregate` in the User Guide for more details 

398 and examples. 

399 

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

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

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

403 

404 Functions that mutate the passed object can produce unexpected 

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

406 for more details. 

407 

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

409 see the examples below. 

410 

411 Examples 

412 -------- 

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

414 

415 >>> s 

416 0 1 

417 1 2 

418 2 3 

419 3 4 

420 dtype: int64 

421 

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

423 1 1 

424 2 3 

425 dtype: int64 

426 

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

428 1 1 

429 2 3 

430 dtype: int64 

431 

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

433 min max 

434 1 1 2 

435 2 3 4 

436 

437 The output column names can be controlled by passing 

438 the desired column names and aggregations as keyword arguments. 

439 

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

441 ... minimum="min", 

442 ... maximum="max", 

443 ... ) 

444 minimum maximum 

445 1 1 2 

446 2 3 4 

447 

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

449 function. 

450 

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

452 1 1.0 

453 2 3.0 

454 dtype: float64 

455 """ 

456 relabeling = func is None 

457 columns = None 

458 if relabeling: 

459 columns, func = validate_func_kwargs(kwargs) 

460 kwargs = {} 

461 

462 if isinstance(func, str): 

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

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

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

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

467 # numba is requested via the global option) 

468 kwargs["engine"] = engine 

469 if engine_kwargs is not None: 

470 kwargs["engine_kwargs"] = engine_kwargs 

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

472 

473 elif isinstance(func, abc.Iterable): 

474 # Catch instances of lists / tuples 

475 # but not the class list / tuple itself. 

476 func = maybe_mangle_lambdas(func) 

477 kwargs["engine"] = engine 

478 kwargs["engine_kwargs"] = engine_kwargs 

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

480 if relabeling: 

481 # columns is not narrowed by mypy from relabeling flag 

482 assert columns is not None # for mypy 

483 ret.columns = columns 

484 if not self.as_index: 

485 ret = ret.reset_index() 

486 return ret 

487 

488 else: 

489 if maybe_use_numba(engine): 

490 return self._aggregate_with_numba( 

491 func, *args, engine_kwargs=engine_kwargs, **kwargs 

492 ) 

493 

494 if self.ngroups == 0: 

495 # e.g. test_evaluate_with_empty_groups without any groups to 

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

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

498 # xref GH#51445 

499 obj = self._obj_with_exclusions 

500 return self._wrap_aggregated_output( 

501 self.obj._constructor( 

502 [], 

503 name=self.obj.name, 

504 index=self._grouper.result_index, 

505 dtype=obj.dtype, 

506 ) 

507 ) 

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

509 

510 agg = aggregate 

511 

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

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

514 

515 obj = self._obj_with_exclusions 

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

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

518 return self._wrap_aggregated_output(res) 

519 

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

521 if isinstance(arg, dict): 

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

523 

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

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

526 else: 

527 # list of functions / function names 

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

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

530 

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

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

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

534 # if as_index=False (GH#50724) 

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

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

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

538 

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

540 from pandas import concat 

541 

542 res_df = concat( 

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

544 ) 

545 return res_df 

546 

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

548 # GH#39609 with no funcs, index by the groups 

549 index = None if results else self._grouper.result_index 

550 output = self.obj._constructor_expanddim(indexed_output, index=index) 

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

552 

553 return output 

554 

555 def _wrap_applied_output( 

556 self, 

557 data: Series, 

558 values: list[Any], 

559 not_indexed_same: bool = False, 

560 is_transform: bool = False, 

561 ) -> DataFrame | Series: 

562 """ 

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

564 

565 Parameters 

566 ---------- 

567 data : Series 

568 Input data for groupby operation. 

569 values : List[Any] 

570 Applied output for each group. 

571 not_indexed_same : bool, default False 

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

573 

574 Returns 

575 ------- 

576 DataFrame or Series 

577 """ 

578 if len(values) == 0: 

579 # GH #6265 

580 if is_transform: 

581 # GH#47787 see test_group_on_empty_multiindex 

582 res_index = data.index 

583 elif not self.group_keys: 

584 res_index = None 

585 else: 

586 res_index = self._grouper.result_index 

587 

588 return self.obj._constructor( 

589 [], 

590 name=self.obj.name, 

591 index=res_index, 

592 dtype=data.dtype, 

593 ) 

594 assert values is not None 

595 

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

597 # GH #823 #24880 

598 index = self._grouper.result_index 

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

600 # if self.observed is False, 

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

602 res_ser = res_df.stack() 

603 res_ser.name = self.obj.name 

604 return res_ser 

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

606 result = self._concat_objects( 

607 values, 

608 not_indexed_same=not_indexed_same, 

609 is_transform=is_transform, 

610 ) 

611 if isinstance(result, Series): 

612 result.name = self.obj.name 

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

614 else: 

615 # GH #6265 #24880 

616 result = self.obj._constructor( 

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

618 ) 

619 if not self.as_index: 

620 result = self._insert_inaxis_grouper(result) 

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

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

623 

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

625 """ 

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

627 

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

629 filled with the transformed values. 

630 

631 Parameters 

632 ---------- 

633 func : function, str 

634 Function to apply to each group. 

635 See the Notes section below for requirements. 

636 

637 Accepted inputs are: 

638 

639 - String 

640 - Python function 

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

642 

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

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

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

646 first and second arguments respectively in the function signature. 

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

648 and optionally available for use. 

649 

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

651 of the groupby method you want to use. 

652 *args 

653 Positional arguments to pass to func. 

654 engine : str, default None 

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

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

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

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

659 

660 engine_kwargs : dict, default None 

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

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

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

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

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

666 applied to the function 

667 

668 **kwargs 

669 Keyword arguments to be passed into func. 

670 

671 Returns 

672 ------- 

673 Series 

674 Series with the same indexes as the original object filled 

675 with transformed values. 

676 

677 See Also 

678 -------- 

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

680 the results together. 

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

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

683 same axis shape as self. 

684 

685 Notes 

686 ----- 

687 See :ref:`groupby.transform` in the User Guide for more details and examples. 

688 

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

690 which group you are working on. 

691 

692 ``func`` is passed each group as a whole :class:`Series`. The current 

693 implementation imposes two requirements on ``func``: 

694 

695 * ``func`` must return a value that either has the same shape as the input 

696 group or can be broadcast to the shape of the input group. 

697 For example, if ``func`` returns a scalar it will be broadcast to have 

698 the same shape as the input group. 

699 * ``func`` must not mutate groups. Mutation is not supported and may 

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

701 

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

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

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

705 

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

707 see the examples below. 

708 

709 .. versionchanged:: 2.0.0 

710 

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

712 the transformation function returns a DataFrame, 

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

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

715 the transformation function to avoid alignment. 

716 

717 Examples 

718 -------- 

719 

720 >>> ser = pd.Series( 

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

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

723 ... name="Max Speed", 

724 ... ) 

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

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

727 Falcon 0.707107 

728 Falcon -0.707107 

729 Parrot 0.707107 

730 Parrot -0.707107 

731 Name: Max Speed, dtype: float64 

732 

733 Broadcast result of the transformation 

734 

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

736 Falcon 40.0 

737 Falcon 40.0 

738 Parrot 10.0 

739 Parrot 10.0 

740 Name: Max Speed, dtype: float64 

741 

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

743 Falcon 370.0 

744 Falcon 370.0 

745 Parrot 25.0 

746 Parrot 25.0 

747 Name: Max Speed, dtype: float64 

748 

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

750 for example: 

751 

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

753 Falcon 390 

754 Falcon 390 

755 Parrot 30 

756 Parrot 30 

757 Name: Max Speed, dtype: int64 

758 """ 

759 

760 if isinstance(func, list): 

761 raise NotImplementedError( 

762 "Passing a list to SeriesGroupBy.transform is not yet supported " 

763 "and is intended to be implemented in a future release. " 

764 "See https://github.com/pandas-dev/pandas/issues/58318." 

765 ) 

766 

767 return self._transform( 

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

769 ) 

770 

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

772 obj = self._obj_with_exclusions 

773 

774 try: 

775 result = self._grouper._cython_operation(obj._values, how, 0, **kwargs) 

776 except NotImplementedError as err: 

777 # e.g. test_groupby_raises_string 

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

779 

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

781 

782 def _transform_general( 

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

784 ) -> Series: 

785 """ 

786 Transform with a callable `func`. 

787 """ 

788 if maybe_use_numba(engine): 

789 return self._transform_with_numba( 

790 func, *args, engine_kwargs=engine_kwargs, **kwargs 

791 ) 

792 assert callable(func) 

793 klass = type(self.obj) 

794 

795 results = [] 

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

797 self._obj_with_exclusions, 

798 ): 

799 # this setattr is needed for test_transform_lambda_with_datetimetz 

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

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

802 

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

804 

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

806 if results: 

807 from pandas.core.reshape.concat import concat 

808 

809 concatenated = concat(results, ignore_index=True) 

810 result = self._set_result_index_ordered(concatenated) 

811 else: 

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

813 

814 result.name = self.obj.name 

815 return result 

816 

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

818 """ 

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

820 

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

822 boolean criterion specified by func. 

823 

824 Parameters 

825 ---------- 

826 func : function 

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

828 dropna : bool, optional 

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

830 groups that evaluate False are filled with NaNs. 

831 *args : tuple 

832 Optional positional arguments to pass to `func`. 

833 **kwargs : dict 

834 Optional keyword arguments to pass to `func`. 

835 

836 Returns 

837 ------- 

838 Series 

839 The filtered subset of the original Series. 

840 

841 See Also 

842 -------- 

843 Series.filter: Filter elements of ungrouped Series. 

844 DataFrameGroupBy.filter : Filter elements from groups based on criterion. 

845 

846 Notes 

847 ----- 

848 Functions that mutate the passed object can produce unexpected 

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

850 for more details. 

851 

852 Examples 

853 -------- 

854 >>> df = pd.DataFrame( 

855 ... { 

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

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

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

859 ... } 

860 ... ) 

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

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

863 1 2 

864 3 4 

865 5 6 

866 Name: B, dtype: int64 

867 """ 

868 if isinstance(func, str): 

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

870 else: 

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

872 

873 # Interpret np.nan as False. 

874 def true_and_notna(x) -> bool: 

875 b = wrapper(x) 

876 return notna(b) and b 

877 

878 try: 

879 indices = [ 

880 self._get_index(name) 

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

882 if true_and_notna(group) 

883 ] 

884 except (ValueError, TypeError) as err: 

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

886 

887 filtered = self._apply_filter(indices, dropna) 

888 return filtered 

889 

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

891 """ 

892 Return number of unique elements in the group. 

893 

894 This method counts the number of distinct values within each group, 

895 optionally excluding NaN values. 

896 

897 Parameters 

898 ---------- 

899 dropna : bool, default True 

900 Don't include NaN in the counts. 

901 

902 Returns 

903 ------- 

904 Series 

905 Number of unique values within each group. 

906 

907 See Also 

908 -------- 

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

910 

911 Examples 

912 -------- 

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

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

915 >>> ser 

916 a 1 

917 a 2 

918 b 3 

919 b 3 

920 dtype: int64 

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

922 a 2 

923 b 1 

924 dtype: int64 

925 """ 

926 ids = self._grouper.ids 

927 ngroups = self._grouper.ngroups 

928 val = self.obj._values 

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

930 

931 if self._grouper.has_dropped_na: 

932 mask = ids >= 0 

933 ids = ids[mask] 

934 codes = codes[mask] 

935 

936 group_index = get_group_index( 

937 labels=[ids, codes], 

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

939 sort=False, 

940 xnull=dropna, 

941 ) 

942 

943 if dropna: 

944 mask = group_index >= 0 

945 if (~mask).any(): 

946 ids = ids[mask] 

947 group_index = group_index[mask] 

948 

949 mask = duplicated(group_index, "first") 

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

951 res = ensure_int64(res) 

952 

953 ri = self._grouper.result_index 

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

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

956 ) 

957 if not self.as_index: 

958 result = self._insert_inaxis_grouper(result) 

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

960 return result 

961 

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

963 """ 

964 Generate descriptive statistics for each group. 

965 

966 Within each group, summarize the central tendency, dispersion, 

967 and shape of the Series's distribution, excluding ``NaN`` values. 

968 The per-group statistics depend on the Series's dtype; see Notes. 

969 

970 Parameters 

971 ---------- 

972 percentiles : list-like of numbers, optional 

973 The percentiles to include in the output. All should fall 

974 between 0 and 1. The default, ``None``, returns the 25th, 

975 50th, and 75th percentiles. 

976 include : None 

977 Has no effect on a Series groupby. Deprecated and will be 

978 removed in a future version. 

979 exclude : None 

980 Has no effect on a Series groupby. Deprecated and will be 

981 removed in a future version. 

982 

983 Returns 

984 ------- 

985 DataFrame 

986 One row per group; columns are the per-group statistics. 

987 

988 See Also 

989 -------- 

990 Series.describe : Generate descriptive statistics of a Series. 

991 DataFrameGroupBy.describe : Generate descriptive statistics for 

992 each group of a DataFrame. 

993 

994 Notes 

995 ----- 

996 For numeric Series, the per-group columns are ``count``, ``mean``, 

997 ``std``, ``min``, ``max``, and the requested percentiles. By 

998 default the lower percentile is ``25`` and the upper is ``75``; 

999 the ``50`` percentile is the same as the median. 

1000 

1001 For object Series (e.g. strings), the per-group columns are 

1002 ``count``, ``unique``, ``top``, and ``freq``. The ``top`` is the 

1003 most common value within the group and ``freq`` is its count. 

1004 

1005 Examples 

1006 -------- 

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

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

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

1010 1 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 

1011 2 2.0 3.5 0.707107 3.0 3.25 3.5 3.75 4.0 

1012 """ 

1013 return super().describe( 

1014 percentiles=percentiles, include=include, exclude=exclude 

1015 ) 

1016 

1017 def value_counts( 

1018 self, 

1019 normalize: bool = False, 

1020 sort: bool = True, 

1021 ascending: bool = False, 

1022 bins=None, 

1023 dropna: bool = True, 

1024 ) -> Series | DataFrame: 

1025 """ 

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

1027 

1028 The resulting object will be in descending order by default so that 

1029 the first element in each group is the most frequently-occurring 

1030 value. NA values are excluded from the result by default. 

1031 

1032 Parameters 

1033 ---------- 

1034 normalize : bool, default False 

1035 Return proportions rather than frequencies. 

1036 sort : bool, default True 

1037 Sort by frequencies. 

1038 ascending : bool, default False 

1039 Sort in ascending order. 

1040 bins : int or list of ints, optional 

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

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

1043 dropna : bool, default True 

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

1045 

1046 Returns 

1047 ------- 

1048 Series or DataFrame 

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

1050 

1051 See Also 

1052 -------- 

1053 Series.value_counts: Equivalent method on Series. 

1054 DataFrame.value_counts: Equivalent method on DataFrame. 

1055 DataFrameGroupBy.value_counts: Equivalent method on DataFrameGroupBy. 

1056 

1057 Notes 

1058 ----- 

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

1060 MultiIndex with one level per input column. 

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

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

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

1064 

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

1066 the result. 

1067 

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

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

1070 

1071 Examples 

1072 -------- 

1073 >>> s = pd.Series( 

1074 ... [1, 1, 2, 3, 2, 3, 3, 1, 1, 3, 3, 3], 

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

1076 ... ) 

1077 >>> s 

1078 A 1 

1079 A 1 

1080 A 2 

1081 A 3 

1082 A 2 

1083 A 3 

1084 B 3 

1085 B 1 

1086 B 1 

1087 B 3 

1088 B 3 

1089 B 3 

1090 dtype: int64 

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

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

1093 A (0.997, 2.0] 4 

1094 (2.0, 3.0] 2 

1095 B (2.0, 3.0] 4 

1096 (0.997, 2.0] 2 

1097 Name: count, dtype: int64 

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

1099 A 1 0.333333 

1100 2 0.333333 

1101 3 0.333333 

1102 B 3 0.666667 

1103 1 0.333333 

1104 Name: proportion, dtype: float64 

1105 """ 

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

1107 

1108 if bins is None: 

1109 result = self._value_counts( 

1110 normalize=normalize, sort=sort, ascending=ascending, dropna=dropna 

1111 ) 

1112 result.name = name 

1113 return result 

1114 

1115 from pandas.core.reshape.merge import get_join_indexers 

1116 from pandas.core.reshape.tile import cut 

1117 

1118 ids = self._grouper.ids 

1119 val = self.obj._values 

1120 

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

1122 

1123 if isinstance(val.dtype, CategoricalDtype) or not np.iterable(bins): 

1124 # scalar bins cannot be done at top level 

1125 # in a backward compatible way 

1126 # GH38672 relates to categorical dtype 

1127 ser = self.apply( 

1128 Series.value_counts, 

1129 normalize=normalize, 

1130 sort=sort, 

1131 ascending=ascending, 

1132 bins=bins, 

1133 ) 

1134 ser.name = name 

1135 ser.index.names = index_names 

1136 return ser 

1137 

1138 # groupby removes null keys from groupings 

1139 mask = ids != -1 

1140 ids, val = ids[mask], val[mask] 

1141 

1142 # lab is a Categorical with categories an IntervalIndex 

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

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

1145 lev = cat_obj.categories 

1146 lab = lev.take( 

1147 cat_obj.codes, 

1148 allow_fill=True, 

1149 fill_value=lev._na_value, 

1150 ) 

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

1152 

1153 if isinstance(lab.dtype, IntervalDtype): 

1154 # TODO: should we do this inside II? 

1155 lab_interval = cast("Interval", lab) 

1156 

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

1158 else: 

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

1160 

1161 ids, lab = ids[sorter], lab[sorter] 

1162 

1163 # group boundaries are where group ids change 

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

1165 idx = np.r_[0, idchanges] 

1166 if not len(ids): 

1167 idx = idchanges 

1168 

1169 # new values are where sorted labels change 

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

1171 inc = np.r_[True, lchanges] 

1172 if not len(val): 

1173 inc = lchanges 

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

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

1176 

1177 # num. of times each group should be repeated 

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

1179 

1180 # multi-index components 

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

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

1183 else: 

1184 codes = [ 

1185 algorithms.factorize( 

1186 self._grouper.result_index, 

1187 sort=self._grouper._sort, 

1188 use_na_sentinel=self._grouper.dropna, 

1189 )[0] 

1190 ] 

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

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

1193 

1194 if dropna: 

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

1196 if mask.all(): 

1197 dropna = False 

1198 else: 

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

1200 

1201 if normalize: 

1202 out = out.astype("float") 

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

1204 if dropna: 

1205 m = ids[lab == -1] 

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

1207 acc = rep(d)[mask] 

1208 else: 

1209 acc = rep(d) 

1210 out /= acc 

1211 

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

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

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

1215 for level_codes in codes[:-1]: 

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

1217 

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

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

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

1221 

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

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

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

1225 _, idx = get_join_indexers( 

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

1227 right, 

1228 sort=False, 

1229 how="left", 

1230 ) 

1231 if idx is not None: 

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

1233 

1234 if sort: 

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

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

1237 

1238 # build the multi-index w/ full levels 

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

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

1241 

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

1243 codes.append(left[-1]) 

1244 

1245 mi = MultiIndex( 

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

1247 ) 

1248 

1249 if is_integer_dtype(out.dtype): 

1250 out = ensure_int64(out) 

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

1252 if not self.as_index: 

1253 result = result.reset_index() 

1254 return result 

1255 

1256 def take( 

1257 self, 

1258 indices: TakeIndexer, 

1259 **kwargs, 

1260 ) -> Series: 

1261 """ 

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

1263 

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

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

1266 actual position of the element in the object. 

1267 

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

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

1270 :meth:`.SeriesGroupBy.nth`. 

1271 

1272 Parameters 

1273 ---------- 

1274 indices : array-like 

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

1276 

1277 **kwargs 

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

1279 output. 

1280 

1281 .. deprecated:: 3.1.0 

1282 Passing ``**kwargs`` to SeriesGroupBy.take is deprecated 

1283 and will be removed in a future version of pandas. 

1284 

1285 Returns 

1286 ------- 

1287 Series 

1288 A Series containing the elements taken from each group. 

1289 

1290 See Also 

1291 -------- 

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

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

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

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

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

1297 

1298 Examples 

1299 -------- 

1300 >>> df = pd.DataFrame( 

1301 ... [ 

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

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

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

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

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

1307 ... ], 

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

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

1310 ... ) 

1311 >>> df 

1312 name class max_speed 

1313 4 falcon bird 389.0 

1314 3 parrot bird 24.0 

1315 2 lion mammal 80.5 

1316 1 monkey mammal NaN 

1317 0 rabbit mammal 15.0 

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

1319 

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

1321 

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

1323 1 4 falcon 

1324 3 parrot 

1325 2 2 lion 

1326 1 monkey 

1327 Name: name, dtype: str 

1328 

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

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

1331 

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

1333 1 3 parrot 

1334 4 falcon 

1335 2 0 rabbit 

1336 1 monkey 

1337 Name: name, dtype: str 

1338 """ 

1339 if kwargs: 

1340 warnings.warn( 

1341 "Passing additional arguments to SeriesGroupBy.take is " 

1342 "deprecated and will be removed in a future version of pandas.", 

1343 Pandas4Warning, 

1344 stacklevel=find_stack_level(), 

1345 ) 

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

1347 return result 

1348 

1349 def skew( 

1350 self, 

1351 skipna: bool = True, 

1352 numeric_only: bool = False, 

1353 **kwargs, 

1354 ) -> Series: 

1355 """ 

1356 Return unbiased skew within groups. 

1357 

1358 Normalized by N-1. 

1359 

1360 Parameters 

1361 ---------- 

1362 skipna : bool, default True 

1363 Exclude NA/null values when computing the result. 

1364 

1365 numeric_only : bool, default False 

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

1367 

1368 **kwargs 

1369 Additional keyword arguments to be passed to the function. 

1370 

1371 .. deprecated:: 3.1.0 

1372 Passing ``**kwargs`` to SeriesGroupBy.skew is deprecated 

1373 and will be removed in a future version of pandas. 

1374 

1375 Returns 

1376 ------- 

1377 Series 

1378 Unbiased skew within groups. 

1379 

1380 See Also 

1381 -------- 

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

1383 

1384 Examples 

1385 -------- 

1386 >>> ser = pd.Series( 

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

1388 ... index=[ 

1389 ... "Falcon", 

1390 ... "Falcon", 

1391 ... "Falcon", 

1392 ... "Falcon", 

1393 ... "Parrot", 

1394 ... "Parrot", 

1395 ... "Parrot", 

1396 ... ], 

1397 ... name="Max Speed", 

1398 ... ) 

1399 >>> ser 

1400 Falcon 390.0 

1401 Falcon 350.0 

1402 Falcon 357.0 

1403 Falcon NaN 

1404 Parrot 22.0 

1405 Parrot 20.0 

1406 Parrot 30.0 

1407 Name: Max Speed, dtype: float64 

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

1409 Falcon 1.525174 

1410 Parrot 1.457863 

1411 Name: Max Speed, dtype: float64 

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

1413 Falcon NaN 

1414 Parrot 1.457863 

1415 Name: Max Speed, dtype: float64 

1416 """ 

1417 if kwargs: 

1418 warnings.warn( 

1419 "Passing additional arguments to SeriesGroupBy.skew is " 

1420 "deprecated and will be removed in a future version of pandas.", 

1421 Pandas4Warning, 

1422 stacklevel=find_stack_level(), 

1423 ) 

1424 

1425 return self._cython_agg_general( 

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

1427 ) 

1428 

1429 def kurt( 

1430 self, 

1431 skipna: bool = True, 

1432 numeric_only: bool = False, 

1433 **kwargs, 

1434 ) -> Series: 

1435 """ 

1436 Return unbiased kurtosis within groups. 

1437 

1438 Kurtosis measures the tailedness of a distribution. This method 

1439 computes Fisher's definition of kurtosis (normal distribution has 

1440 a kurtosis of zero) for each group, using the unbiased estimator. 

1441 

1442 Parameters 

1443 ---------- 

1444 skipna : bool, default True 

1445 Exclude NA/null values when computing the result. 

1446 

1447 numeric_only : bool, default False 

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

1449 

1450 **kwargs 

1451 Additional keyword arguments to be passed to the function. 

1452 

1453 Returns 

1454 ------- 

1455 Series 

1456 Unbiased kurtosis within groups. 

1457 

1458 See Also 

1459 -------- 

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

1461 

1462 Examples 

1463 -------- 

1464 >>> ser = pd.Series( 

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

1466 ... index=[ 

1467 ... "Falcon", 

1468 ... "Falcon", 

1469 ... "Falcon", 

1470 ... "Falcon", 

1471 ... "Falcon", 

1472 ... "Parrot", 

1473 ... "Parrot", 

1474 ... "Parrot", 

1475 ... "Parrot", 

1476 ... "Parrot", 

1477 ... ], 

1478 ... name="Max Speed", 

1479 ... ) 

1480 >>> ser 

1481 Falcon 390.0 

1482 Falcon 350.0 

1483 Falcon 357.0 

1484 Falcon 333.0 

1485 Falcon NaN 

1486 Parrot 22.0 

1487 Parrot 20.0 

1488 Parrot 30.0 

1489 Parrot 40.0 

1490 Parrot 41.0 

1491 Name: Max Speed, dtype: float64 

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

1493 Falcon 1.622109 

1494 Parrot -2.878714 

1495 Name: Max Speed, dtype: float64 

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

1497 Falcon NaN 

1498 Parrot -2.878714 

1499 Name: Max Speed, dtype: float64 

1500 """ 

1501 

1502 def alt(obj): 

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

1504 # TypeError and not NotImplementedError. 

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

1506 

1507 return self._cython_agg_general( 

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

1509 ) 

1510 

1511 @property 

1512 def plot(self) -> GroupByPlot: 

1513 """ 

1514 Make plots of groups from a Series. 

1515 

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

1517 By default, matplotlib is used. 

1518 

1519 Returns 

1520 ------- 

1521 GroupByPlot 

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

1523 

1524 See Also 

1525 -------- 

1526 Series.plot : Make plots of Series. 

1527 

1528 Examples 

1529 -------- 

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

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

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

1533 """ 

1534 result = GroupByPlot(self) 

1535 return result 

1536 

1537 def nlargest( 

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

1539 ) -> Series: 

1540 """ 

1541 Return the largest `n` elements. 

1542 

1543 Within each group, returns the `n` largest values sorted in 

1544 descending order. The ``keep`` parameter controls how ties are 

1545 handled when there are duplicate values at the boundary. 

1546 

1547 Parameters 

1548 ---------- 

1549 n : int, default 5 

1550 Return this many descending sorted values. 

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

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

1553 Series of `n` elements: 

1554 

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

1556 of appearance. 

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

1558 order of appearance. 

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

1560 size larger than `n`. 

1561 

1562 Returns 

1563 ------- 

1564 Series 

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

1566 

1567 See Also 

1568 -------- 

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

1570 Series.sort_values: Sort Series by values. 

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

1572 

1573 Notes 

1574 ----- 

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

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

1577 

1578 Examples 

1579 -------- 

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

1581 

1582 >>> s 

1583 0 1 

1584 1 2 

1585 2 3 

1586 3 4 

1587 4 5 

1588 5 6 

1589 dtype: int64 

1590 

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

1592 1 2 3 

1593 1 2 

1594 2 5 6 

1595 4 5 

1596 dtype: int64 

1597 """ 

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

1599 data = self._obj_with_exclusions 

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

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

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

1603 return result 

1604 

1605 def nsmallest( 

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

1607 ) -> Series: 

1608 """ 

1609 Return the smallest `n` elements. 

1610 

1611 Within each group, returns the `n` smallest values sorted in 

1612 ascending order. The ``keep`` parameter controls how ties are 

1613 handled when there are duplicate values at the boundary. 

1614 

1615 Parameters 

1616 ---------- 

1617 n : int, default 5 

1618 Return this many ascending sorted values. 

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

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

1621 Series of `n` elements: 

1622 

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

1624 of appearance. 

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

1626 order of appearance. 

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

1628 size larger than `n`. 

1629 

1630 Returns 

1631 ------- 

1632 Series 

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

1634 

1635 See Also 

1636 -------- 

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

1638 Series.sort_values: Sort Series by values. 

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

1640 

1641 Notes 

1642 ----- 

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

1644 the size of the ``Series`` object. 

1645 

1646 Examples 

1647 -------- 

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

1649 

1650 >>> s 

1651 0 1 

1652 1 2 

1653 2 3 

1654 3 4 

1655 4 5 

1656 5 6 

1657 dtype: int64 

1658 

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

1660 1 0 1 

1661 1 2 

1662 2 3 4 

1663 4 5 

1664 dtype: int64 

1665 """ 

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

1667 data = self._obj_with_exclusions 

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

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

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

1671 return result 

1672 

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

1674 """ 

1675 Return the row label of the minimum value. 

1676 

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

1678 value is returned. 

1679 

1680 Parameters 

1681 ---------- 

1682 skipna : bool, default True 

1683 Exclude NA values. 

1684 

1685 Returns 

1686 ------- 

1687 Series 

1688 Indexes of minima in each group. 

1689 

1690 Raises 

1691 ------ 

1692 ValueError 

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

1694 

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

1696 * All values for a group are NA. 

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

1698 

1699 .. versionchanged:: 3.0.0 

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

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

1702 

1703 See Also 

1704 -------- 

1705 numpy.argmin : Return indices of the minimum values 

1706 along the given axis. 

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

1708 over requested axis. 

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

1710 of maximum of values. 

1711 

1712 Examples 

1713 -------- 

1714 >>> ser = pd.Series( 

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

1716 ... index=pd.DatetimeIndex( 

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

1718 ... ), 

1719 ... ) 

1720 >>> ser 

1721 2023-01-01 1 

1722 2023-01-15 2 

1723 2023-02-01 3 

1724 2023-02-15 4 

1725 dtype: int64 

1726 

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

1728 a 2023-01-01 

1729 b 2023-02-01 

1730 dtype: datetime64[us] 

1731 """ 

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

1733 

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

1735 """ 

1736 Return the row label of the maximum value. 

1737 

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

1739 value is returned. 

1740 

1741 Parameters 

1742 ---------- 

1743 skipna : bool, default True 

1744 Exclude NA values. 

1745 

1746 Returns 

1747 ------- 

1748 Series 

1749 Indexes of maxima in each group. 

1750 

1751 Raises 

1752 ------ 

1753 ValueError 

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

1755 

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

1757 * All values for a group are NA. 

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

1759 

1760 .. versionchanged:: 3.0.0 

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

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

1763 

1764 See Also 

1765 -------- 

1766 numpy.argmax : Return indices of the maximum values 

1767 along the given axis. 

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

1769 over requested axis. 

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

1771 of minimum of values. 

1772 

1773 Examples 

1774 -------- 

1775 >>> ser = pd.Series( 

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

1777 ... index=pd.DatetimeIndex( 

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

1779 ... ), 

1780 ... ) 

1781 >>> ser 

1782 2023-01-01 1 

1783 2023-01-15 2 

1784 2023-02-01 3 

1785 2023-02-15 4 

1786 dtype: int64 

1787 

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

1789 a 2023-01-15 

1790 b 2023-02-15 

1791 dtype: datetime64[us] 

1792 """ 

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

1794 

1795 def corr( 

1796 self, 

1797 other: Series, 

1798 method: CorrelationMethod = "pearson", 

1799 min_periods: int | None = None, 

1800 ) -> Series: 

1801 """ 

1802 Compute correlation between each group and another Series. 

1803 

1804 This method computes the pairwise correlation between each group's 

1805 values and the corresponding values in ``other``, using the specified 

1806 correlation method. 

1807 

1808 Parameters 

1809 ---------- 

1810 other : Series 

1811 Series to compute correlation with. 

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

1813 Method of correlation to use. 

1814 min_periods : int, optional 

1815 Minimum number of observations required per pair of columns to 

1816 have a valid result. 

1817 

1818 Returns 

1819 ------- 

1820 Series 

1821 Correlation value for each group. 

1822 

1823 See Also 

1824 -------- 

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

1826 

1827 Examples 

1828 -------- 

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

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

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

1832 """ 

1833 result = self._op_via_apply( 

1834 "corr", other=other, method=method, min_periods=min_periods 

1835 ) 

1836 return result 

1837 

1838 def cov( 

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

1840 ) -> Series: 

1841 """ 

1842 Compute covariance between each group and another Series. 

1843 

1844 This method computes the sample covariance between each group's 

1845 values and the corresponding values in ``other``. 

1846 

1847 Parameters 

1848 ---------- 

1849 other : Series 

1850 Series to compute covariance with. 

1851 min_periods : int, optional 

1852 Minimum number of observations required per pair of columns to 

1853 have a valid result. 

1854 ddof : int, optional 

1855 Delta degrees of freedom for variance calculation. 

1856 

1857 Returns 

1858 ------- 

1859 Series 

1860 Covariance value for each group. 

1861 

1862 See Also 

1863 -------- 

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

1865 

1866 Examples 

1867 -------- 

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

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

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

1871 """ 

1872 result = self._op_via_apply( 

1873 "cov", other=other, min_periods=min_periods, ddof=ddof 

1874 ) 

1875 return result 

1876 

1877 @property 

1878 def is_monotonic_increasing(self) -> Series: 

1879 """ 

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

1881 

1882 A group is considered monotonically increasing if each successive 

1883 element is greater than or equal to the previous one. 

1884 

1885 Returns 

1886 ------- 

1887 Series 

1888 

1889 See Also 

1890 -------- 

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

1892 are monotonically decreasing. 

1893 

1894 Examples 

1895 -------- 

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

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

1898 Falcon False 

1899 Parrot True 

1900 dtype: bool 

1901 """ 

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

1903 

1904 @property 

1905 def is_monotonic_decreasing(self) -> Series: 

1906 """ 

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

1908 

1909 A group is considered monotonically decreasing if each successive 

1910 element is less than or equal to the previous one. 

1911 

1912 Returns 

1913 ------- 

1914 Series 

1915 

1916 See Also 

1917 -------- 

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

1919 are monotonically increasing. 

1920 

1921 Examples 

1922 -------- 

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

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

1925 Falcon True 

1926 Parrot False 

1927 dtype: bool 

1928 """ 

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

1930 

1931 def hist( 

1932 self, 

1933 by=None, 

1934 ax=None, 

1935 grid: bool = True, 

1936 xlabelsize: int | None = None, 

1937 xrot: float | None = None, 

1938 ylabelsize: int | None = None, 

1939 yrot: float | None = None, 

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

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

1942 backend: str | None = None, 

1943 legend: bool = False, 

1944 **kwargs, 

1945 ): 

1946 """ 

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

1948 

1949 A separate histogram subplot is generated for each group, making it 

1950 easy to visually compare the distribution of values across groups. 

1951 

1952 Parameters 

1953 ---------- 

1954 by : object, optional 

1955 Grouping key. 

1956 ax : matplotlib.axes.Axes, optional 

1957 Axis to draw the histogram on. 

1958 grid : bool, default True 

1959 Show axis grid lines. 

1960 xlabelsize : int, default None 

1961 X axis label size. 

1962 xrot : float, default None 

1963 Rotation for x ticks. 

1964 ylabelsize : int, default None 

1965 Y axis label size. 

1966 yrot : float, default None 

1967 Rotation for y ticks. 

1968 figsize : tuple, optional 

1969 Figure size in inches. 

1970 bins : int or sequence, default 10 

1971 Number of histogram bins or bin edges. 

1972 backend : str or callable or None, optional 

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

1974 plotting backend. 

1975 legend : bool, default False 

1976 Whether to draw the legend. 

1977 **kwargs 

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

1979 

1980 Returns 

1981 ------- 

1982 matplotlib.axes.Axes or ndarray of Axes 

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

1984 

1985 See Also 

1986 -------- 

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

1988 

1989 Examples 

1990 -------- 

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

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

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

1994 """ 

1995 result = self._op_via_apply( 

1996 "hist", 

1997 by=by, 

1998 ax=ax, 

1999 grid=grid, 

2000 xlabelsize=xlabelsize, 

2001 xrot=xrot, 

2002 ylabelsize=ylabelsize, 

2003 yrot=yrot, 

2004 figsize=figsize, 

2005 bins=bins, 

2006 backend=backend, 

2007 legend=legend, 

2008 **kwargs, 

2009 ) 

2010 return result 

2011 

2012 @property 

2013 def dtype(self) -> Series: 

2014 """ 

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

2016 

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

2018 

2019 Returns 

2020 ------- 

2021 Series 

2022 Dtype of each group's values. 

2023 """ 

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

2025 

2026 def unique(self) -> Series: 

2027 """ 

2028 Return unique values for each group. 

2029 

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

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

2032 

2033 Returns 

2034 ------- 

2035 Series 

2036 Unique values for each of the grouped values. 

2037 

2038 See Also 

2039 -------- 

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

2041 

2042 Examples 

2043 -------- 

2044 >>> df = pd.DataFrame( 

2045 ... [ 

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

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

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

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

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

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

2052 ... ], 

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

2054 ... ) 

2055 >>> df 

2056 breed animal height_in 

2057 0 Chihuahua dog 6.1 

2058 1 Beagle dog 15.2 

2059 2 Chihuahua dog 6.9 

2060 3 Persian cat 9.2 

2061 4 Chihuahua dog 7.0 

2062 5 Persian cat 8.8 

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

2064 >>> ser 

2065 animal 

2066 cat [Persian] 

2067 dog [Chihuahua, Beagle] 

2068 Name: breed, dtype: object 

2069 """ 

2070 result = self._op_via_apply("unique") 

2071 return result 

2072 

2073 

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

2075class DataFrameGroupBy(GroupBy[DataFrame]): 

2076 def aggregate( 

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

2078 ) -> DataFrame: 

2079 """ 

2080 Aggregate using one or more operations. 

2081 

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

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

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

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

2086 

2087 Parameters 

2088 ---------- 

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

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

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

2092 

2093 Accepted combinations are: 

2094 

2095 - function 

2096 - string function name 

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

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

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

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

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

2102 compute the values in the column. 

2103 

2104 Can also accept a Numba JIT function with 

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

2106 with this engine. 

2107 

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

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

2110 first and second arguments respectively in the function signature. 

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

2112 and optionally available for use. 

2113 

2114 *args 

2115 Positional arguments to pass to func. 

2116 engine : str, default None 

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

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

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

2120 ``compute.use_numba`` 

2121 

2122 engine_kwargs : dict, default None 

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

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

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

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

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

2128 applied to the function 

2129 

2130 **kwargs 

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

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

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

2134 

2135 Returns 

2136 ------- 

2137 DataFrame 

2138 Aggregated DataFrame based on the grouping and the applied aggregation 

2139 functions. 

2140 

2141 See Also 

2142 -------- 

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

2144 and combine the results together. 

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

2146 based on the given function. 

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

2148 

2149 Notes 

2150 ----- 

2151 See :ref:`groupby.aggregate` in the User Guide for more details 

2152 and examples. 

2153 

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

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

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

2157 

2158 Functions that mutate the passed object can produce unexpected 

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

2160 for more details. 

2161 

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

2163 see the examples below. 

2164 

2165 Examples 

2166 -------- 

2167 >>> data = { 

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

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

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

2171 ... } 

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

2173 >>> df 

2174 A B C 

2175 0 1 1 0.362838 

2176 1 1 2 0.227877 

2177 2 2 3 1.267767 

2178 3 2 4 -0.562860 

2179 

2180 The aggregation is for each column. 

2181 

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

2183 B C 

2184 A 

2185 1 1 0.227877 

2186 2 3 -0.562860 

2187 

2188 Multiple aggregations 

2189 

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

2191 B C 

2192 min max min max 

2193 A 

2194 1 1 2 0.227877 0.362838 

2195 2 3 4 -0.562860 1.267767 

2196 

2197 Select a column for aggregation 

2198 

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

2200 min max 

2201 A 

2202 1 1 2 

2203 2 3 4 

2204 

2205 User-defined function for aggregation 

2206 

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

2208 B C 

2209 A 

2210 1 5 2.590715 

2211 2 9 2.704907 

2212 

2213 Different aggregations per column 

2214 

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

2216 B C 

2217 min max sum 

2218 A 

2219 1 1 2 0.590715 

2220 2 3 4 0.704907 

2221 

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

2223 pandas supports "named aggregation" 

2224 

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

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

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

2228 ... ) 

2229 b_min c_sum 

2230 A 

2231 1 1 0.590715 

2232 2 3 0.704907 

2233 

2234 - The keywords are the *output* column names 

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

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

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

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

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

2240 

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

2242 

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

2244 function. 

2245 

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

2247 B 

2248 A 

2249 1 1.0 

2250 2 3.0 

2251 """ 

2252 # This method can consume the un-normalized {column: aggfunc} form, but only 

2253 # when the columns are unique: dict aggregation fans a single key out to 

2254 # every matching column, while named aggregation must produce exactly one 

2255 # output per keyword. The columns must also be unnamed, since the dict 

2256 # form keeps columns.name and the normalized form drops it. 

2257 # `func is None` is a precondition for relabeling at all, and short-circuits 

2258 # materializing _obj_with_exclusions on the far more common plain-agg path. 

2259 allow_skip_normalization = func is None and ( 

2260 self._obj_with_exclusions.columns.is_unique 

2261 and self._obj_with_exclusions.columns.name is None 

2262 ) 

2263 relabeling, func, columns, order = reconstruct_func( 

2264 func, allow_skip_normalization, **kwargs 

2265 ) 

2266 func = maybe_mangle_lambdas(func) 

2267 

2268 if maybe_use_numba(engine): 

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

2270 # if user asks for numba 

2271 kwargs["engine"] = engine 

2272 kwargs["engine_kwargs"] = engine_kwargs 

2273 

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

2275 result = op.agg() 

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

2277 # GH #52849 

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

2279 return result.reset_index() 

2280 else: 

2281 return cast("DataFrame", result) 

2282 elif relabeling: 

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

2284 # used reordered index of columns 

2285 result = cast("DataFrame", result) 

2286 result = result.iloc[:, order] 

2287 result = cast("DataFrame", result) 

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

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

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

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

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

2293 

2294 if result is None: 

2295 # Remove the kwargs we inserted 

2296 # (already stored in engine, engine_kwargs arguments) 

2297 if "engine" in kwargs: 

2298 del kwargs["engine"] 

2299 del kwargs["engine_kwargs"] 

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

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

2302 if maybe_use_numba(engine): 

2303 return self._aggregate_with_numba( 

2304 func, *args, engine_kwargs=engine_kwargs, **kwargs 

2305 ) 

2306 result = self._python_agg_general(func, *args, **kwargs) 

2307 

2308 if not self.as_index: 

2309 result = self._insert_inaxis_grouper(result) 

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

2311 

2312 return cast("DataFrame", result) 

2313 

2314 agg = aggregate 

2315 

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

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

2318 

2319 obj = self._obj_with_exclusions 

2320 

2321 if self.ngroups == 0 or len(obj.columns) == 0: 

2322 res_index = self._grouper.result_index 

2323 res = self.obj._constructor(index=res_index, columns=obj.columns).astype( 

2324 obj.dtypes 

2325 ) 

2326 else: 

2327 output: dict[int, ArrayLike] = { 

2328 idx: self._grouper.agg_series(ser, f) 

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

2330 } 

2331 res = self.obj._constructor(output) 

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

2333 return self._wrap_aggregated_output(res) 

2334 

2335 def _wrap_applied_output( 

2336 self, 

2337 data: DataFrame, 

2338 values: list, 

2339 not_indexed_same: bool = False, 

2340 is_transform: bool = False, 

2341 ): 

2342 if len(values) == 0: 

2343 if is_transform: 

2344 # GH#47787 see test_group_on_empty_multiindex 

2345 res_index = data.index 

2346 elif not self.group_keys or not self.as_index: 

2347 res_index = None 

2348 else: 

2349 res_index = self._grouper.result_index 

2350 

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

2352 result = result.astype(data.dtypes) 

2353 return result 

2354 

2355 # GH12824 

2356 # using values[0] here breaks test_groupby_apply_none_first 

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

2358 

2359 if first_not_none is None: 

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

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

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

2363 result = result.astype(data.dtypes) 

2364 return result 

2365 elif isinstance(first_not_none, DataFrame): 

2366 return self._concat_objects( 

2367 values, 

2368 not_indexed_same=not_indexed_same, 

2369 is_transform=is_transform, 

2370 ) 

2371 

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

2373 

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

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

2376 # fall through to the outer else clause 

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

2378 # after raising AttributeError above 

2379 # GH 18930 

2380 if not is_hashable(self._selection): 

2381 # error: Need type annotation for "name" 

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

2383 else: 

2384 # error: Incompatible types in assignment 

2385 # (expression has type "Hashable", variable 

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

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

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

2389 elif not isinstance(first_not_none, Series): 

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

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

2392 # result should not take the name of original selection 

2393 # of columns 

2394 if self.as_index: 

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

2396 else: 

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

2398 result = self._insert_inaxis_grouper(result) 

2399 return result 

2400 else: 

2401 # values are Series 

2402 return self._wrap_applied_output_series( 

2403 values, 

2404 not_indexed_same, 

2405 first_not_none, 

2406 key_index, 

2407 is_transform, 

2408 ) 

2409 

2410 def _wrap_applied_output_series( 

2411 self, 

2412 values: list[Series], 

2413 not_indexed_same: bool, 

2414 first_not_none, 

2415 key_index: Index | None, 

2416 is_transform: bool, 

2417 ) -> DataFrame | Series: 

2418 kwargs = first_not_none._construct_axes_dict() 

2419 backup = Series(**kwargs) 

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

2421 

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

2423 

2424 if not all_indexed_same: 

2425 # GH 8467 

2426 return self._concat_objects( 

2427 values, 

2428 not_indexed_same=True, 

2429 is_transform=is_transform, 

2430 ) 

2431 

2432 # Combine values 

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

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

2435 

2436 index = key_index 

2437 columns = first_not_none.index.copy() 

2438 if columns.name is None: 

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

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

2441 if len(names) == 1: 

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

2443 

2444 if stacked_values.dtype == object: 

2445 # We'll have the DataFrame constructor do inference 

2446 stacked_values = stacked_values.tolist() 

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

2448 

2449 if not self.as_index: 

2450 result = self._insert_inaxis_grouper(result) 

2451 

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

2453 

2454 def _cython_transform( 

2455 self, 

2456 how: str, 

2457 numeric_only: bool = False, 

2458 **kwargs, 

2459 ) -> DataFrame: 

2460 # We have multi-block tests 

2461 # e.g. test_rank_min_int, test_cython_transform_frame 

2462 # test_transform_numeric_ret 

2463 mgr: BlockManager = self._get_data_to_aggregate( 

2464 numeric_only=numeric_only, name=how 

2465 ) 

2466 

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

2468 return self._grouper._cython_operation(bvalues, how, 1, **kwargs) 

2469 

2470 res_mgr = mgr.apply(arr_func) 

2471 

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

2473 return res_df 

2474 

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

2476 if maybe_use_numba(engine): 

2477 return self._transform_with_numba( 

2478 func, *args, engine_kwargs=engine_kwargs, **kwargs 

2479 ) 

2480 from pandas.core.reshape.concat import concat 

2481 

2482 applied = [] 

2483 obj = self._obj_with_exclusions 

2484 gen = self._grouper.get_iterator(obj) 

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

2486 

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

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

2489 # it does not need to be computed again. 

2490 try: 

2491 name, group = next(gen) 

2492 except StopIteration: 

2493 pass 

2494 else: 

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

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

2497 try: 

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

2499 except ValueError as err: 

2500 # e.g. test_transform_with_non_scalar_group 

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

2502 raise ValueError(msg) from err 

2503 if group.size > 0: 

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

2505 applied.append(res) 

2506 

2507 # Compute and process with the remaining groups 

2508 for name, group in gen: 

2509 if group.size == 0: 

2510 continue 

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

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

2513 res = path(group) 

2514 

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

2516 applied.append(res) 

2517 

2518 concat_index = obj.columns 

2519 concatenated = concat( 

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

2521 ) 

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

2523 return self._set_result_index_ordered(concatenated) 

2524 

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

2526 """ 

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

2528 

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

2530 filled with the transformed values. 

2531 

2532 When ``func`` is a user-defined function, by default it operates on each 

2533 column of each group separately (each column is passed as a 

2534 :class:`Series`); see the Notes section below. 

2535 

2536 Parameters 

2537 ---------- 

2538 func : function, str, list, or dict 

2539 Function to apply to each group. 

2540 See the Notes section below for requirements. 

2541 

2542 Accepted inputs are: 

2543 

2544 - String 

2545 - Python function 

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

2547 - List of strings/functions: applied to every non-key column, 

2548 returning a MultiIndex-column DataFrame ``(column, func)``. 

2549 - Dict ``{column: func}`` or ``{name: NamedFunc(column, func)}``: 

2550 applied per-column as specified. 

2551 

2552 .. versionchanged:: 3.1.0 

2553 Added support for list-like, dict, and :class:`NamedFunc` arguments. 

2554 

2555 Only passing a single function is supported with the numba engine. 

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

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

2558 first and second arguments respectively in the function signature. 

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

2560 and optionally available for use. 

2561 

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

2563 of the groupby method you want to use. 

2564 

2565 *args 

2566 Positional arguments to pass to func. 

2567 engine : str, default None 

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

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

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

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

2572 

2573 engine_kwargs : dict, default None 

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

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

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

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

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

2579 applied to the function 

2580 

2581 **kwargs 

2582 Keyword arguments to be passed into func. 

2583 When ``func=None``, ``**kwargs`` should be pairs of 

2584 ``output_name=NamedFunc(column, func)``. 

2585 

2586 Returns 

2587 ------- 

2588 DataFrame 

2589 DataFrame with the same indexes as the original object filled 

2590 with transformed values. 

2591 

2592 See Also 

2593 -------- 

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

2595 the results together. 

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

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

2598 same axis shape as self. 

2599 

2600 Notes 

2601 ----- 

2602 See :ref:`groupby.transform` in the User Guide for more details and examples. 

2603 

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

2605 which group you are working on. 

2606 

2607 The current implementation imposes three requirements on ``func``: 

2608 

2609 * ``func`` must return a value that either has the same shape as the input 

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

2611 For example, if ``func`` returns a scalar it will be broadcast to have 

2612 the same shape as the input subframe. 

2613 * ``func`` is applied to each column of the group separately (each column 

2614 is passed as a :class:`Series`). If ``func`` also supports application to 

2615 the entire group :class:`DataFrame` and returns the same result, a faster 

2616 path operating on the whole group is used starting from the second group. 

2617 * ``func`` must not mutate groups. Mutation is not supported and may 

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

2619 

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

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

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

2623 

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

2625 see the examples below. 

2626 

2627 .. versionchanged:: 2.0.0 

2628 

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

2630 and the transformation function returns a DataFrame, 

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

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

2633 transformation function to avoid alignment. 

2634 

2635 Examples 

2636 -------- 

2637 

2638 >>> df = pd.DataFrame( 

2639 ... { 

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

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

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

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

2644 ... } 

2645 ... ) 

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

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

2648 C D 

2649 0 -1.154701 -0.577350 

2650 1 0.577350 0.000000 

2651 2 0.577350 1.154701 

2652 3 -1.154701 -1.000000 

2653 4 0.577350 -0.577350 

2654 5 0.577350 1.000000 

2655 

2656 Broadcast result of the transformation 

2657 

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

2659 C D 

2660 0 4.0 6.0 

2661 1 3.0 8.0 

2662 2 4.0 6.0 

2663 3 3.0 8.0 

2664 4 4.0 6.0 

2665 5 3.0 8.0 

2666 

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

2668 C D 

2669 0 3.666667 4.0 

2670 1 4.000000 5.0 

2671 2 3.666667 4.0 

2672 3 4.000000 5.0 

2673 4 3.666667 4.0 

2674 5 4.000000 5.0 

2675 

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

2677 for example: 

2678 

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

2680 C D 

2681 0 5 8 

2682 1 5 9 

2683 2 5 8 

2684 3 5 9 

2685 4 5 8 

2686 5 5 9 

2687 

2688 List-like arguments 

2689 

2690 >>> df2 = pd.DataFrame({"col": list("aab"), "val": range(3), "other": range(3)}) 

2691 >>> df2.groupby("col").transform(["sum", "min"]) 

2692 val other 

2693 sum min sum min 

2694 0 1 0 1 0 

2695 1 1 0 1 0 

2696 2 2 2 2 2 

2697 

2698 Dictionary arguments 

2699 

2700 >>> df2.groupby("col").transform({"val": "sum", "other": "min"}) 

2701 val other 

2702 0 1 0 

2703 1 1 0 

2704 2 2 2 

2705 

2706 Named aggregation 

2707 

2708 >>> df2.groupby("col").transform( 

2709 ... val_sum=pd.NamedAgg(column="val", aggfunc="sum"), 

2710 ... other_min=pd.NamedAgg(column="other", aggfunc="min"), 

2711 ... ) 

2712 val_sum other_min 

2713 0 1 0 

2714 1 1 0 

2715 2 2 2 

2716 """ 

2717 # GH#58318 - extended to accept list, dict, and NamedAgg kwargs. 

2718 

2719 if func is None: 

2720 # Named-aggregation style: 

2721 # .transform(val_sum=NamedAgg(column="val", aggfunc="sum"), ...) 

2722 return self._transform_multiple_funcs( 

2723 kwargs, *args, engine=engine, engine_kwargs=engine_kwargs 

2724 ) 

2725 elif is_dict_like(func): 

2726 # e.g. .transform({"val": "sum"}) or {"name": NamedAgg(...)} 

2727 # Dict-of-lists is not yet supported. 

2728 for val in func.values(): 

2729 if is_list_like(val): 

2730 raise NotImplementedError( 

2731 "Passing a dict of lists to DataFrameGroupBy.transform is " 

2732 "not yet supported and is intended to be implemented in a " 

2733 "future release. " 

2734 "See https://github.com/pandas-dev/pandas/issues/58318" 

2735 ) 

2736 return self._transform_multiple_funcs( 

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

2738 ) 

2739 elif is_list_like(func): 

2740 # e.g. .transform(["sum", "min"]) 

2741 func = maybe_mangle_lambdas(func) 

2742 return self._transform_multiple_funcs( 

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

2744 ) 

2745 else: 

2746 return self._transform( 

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

2748 ) 

2749 

2750 def _transform_multiple_funcs( 

2751 self, 

2752 func: list | dict, 

2753 *args, 

2754 engine: str | None = None, 

2755 engine_kwargs: dict | None = None, 

2756 **kwargs, 

2757 ) -> DataFrame: 

2758 """ 

2759 Handle list-like and dict dispatch for DataFrameGroupBy.transform. 

2760 

2761 Parameters 

2762 ---------- 

2763 func : list or dict 

2764 - list of str/callable: applied to every non-key column, producing 

2765 a MultiIndex-column DataFrame (column, func_name). 

2766 - dict mapping output_name -> str/callable or NamedAgg. 

2767 """ 

2768 from pandas.core.reshape.concat import concat 

2769 

2770 if is_dict_like(func): 

2771 # Also includes NamedAgg / NamedFunc 

2772 func = cast("dict", func) 

2773 results: list[Series] = [] 

2774 for name, agg in func.items(): 

2775 if isinstance(agg, NamedAgg): 

2776 column_name = agg.column 

2777 agg_func = agg.aggfunc 

2778 elif isinstance(agg, tuple) and len(agg) == 2: 

2779 # plain tuple (column, func) — same semantics as NamedAgg 

2780 column_name = agg[0] 

2781 agg_func = agg[1] 

2782 else: 

2783 # plain {"col": "sum"} — column IS the key 

2784 column_name = name 

2785 agg_func = agg 

2786 result = self._transform_single_column( 

2787 column_name, 

2788 agg_func, 

2789 *args, 

2790 engine=engine, 

2791 engine_kwargs=engine_kwargs, 

2792 **kwargs, 

2793 ) 

2794 result.name = name 

2795 results.append(result) 

2796 return concat(results, axis=1) 

2797 

2798 # list path 

2799 # Apply every func to every non-key column. 

2800 assert is_list_like(func) 

2801 results_list: list[Series] = [] 

2802 col_order: list[tuple] = [] 

2803 for column in self._obj_with_exclusions.columns: 

2804 for agg_func in func: 

2805 col_result = self._transform_single_column( 

2806 column, 

2807 agg_func, 

2808 *args, 

2809 engine=engine, 

2810 engine_kwargs=engine_kwargs, 

2811 **kwargs, 

2812 ) 

2813 col_result.name = (column, agg_func) 

2814 results_list.append(col_result) 

2815 col_order.append((column, agg_func)) 

2816 

2817 # Use ignore_index=True then assign MultiIndex — avoids redundant work. 

2818 output = concat(results_list, ignore_index=True, axis=1) 

2819 arrays = [list(x) for x in zip(*col_order, strict=False)] 

2820 output.columns = MultiIndex.from_arrays(arrays) 

2821 return output 

2822 

2823 def _transform_single_column( 

2824 self, 

2825 column_name: Hashable, 

2826 agg_func: Callable | str, 

2827 *args, 

2828 engine: str | None = None, 

2829 engine_kwargs: dict | None = None, 

2830 **kwargs, 

2831 ) -> Series: 

2832 """ 

2833 Apply a single transform function to one column via SeriesGroupBy. 

2834 

2835 Parameters 

2836 ---------- 

2837 column_name : Hashable 

2838 Column label to select from the grouped DataFrame. 

2839 agg_func : callable or str 

2840 Transform function to apply to the selected column. 

2841 *args 

2842 Positional arguments to pass to ``agg_func``. 

2843 engine : str, default None 

2844 Passed through to ``SeriesGroupBy.transform``. 

2845 engine_kwargs : dict, default None 

2846 Passed through to ``SeriesGroupBy.transform``. 

2847 **kwargs 

2848 Keyword arguments to pass to ``agg_func``. 

2849 

2850 Returns 

2851 ------- 

2852 Series 

2853 Transformed Series with the same index as the original DataFrame. 

2854 """ 

2855 data = self._gotitem(column_name, ndim=1) 

2856 return data.transform( 

2857 agg_func, *args, engine=engine, engine_kwargs=engine_kwargs, **kwargs 

2858 ) 

2859 

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

2861 if isinstance(func, str): 

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

2863 slow_path = lambda group: group.apply( 

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

2865 ) 

2866 else: 

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

2868 slow_path = lambda group: group.apply( 

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

2870 ) 

2871 return fast_path, slow_path 

2872 

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

2874 path = slow_path 

2875 res = slow_path(group) 

2876 

2877 if self.ngroups == 1: 

2878 # no need to evaluate multiple paths when only 

2879 # a single group exists 

2880 return path, res 

2881 

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

2883 try: 

2884 res_fast = fast_path(group) 

2885 except AssertionError: 

2886 raise # pragma: no cover 

2887 except Exception: 

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

2889 # raised; see test_transform.test_transform_fastpath_raises 

2890 return path, res 

2891 

2892 # verify fast path returns either: 

2893 # a DataFrame with columns equal to group.columns 

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

2895 if isinstance(res_fast, DataFrame): 

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

2897 return path, res 

2898 elif isinstance(res_fast, Series): 

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

2900 return path, res 

2901 else: 

2902 return path, res 

2903 

2904 if res_fast.equals(res): 

2905 path = fast_path 

2906 

2907 return path, res 

2908 

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

2910 """ 

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

2912 

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

2914 boolean criterion specified by func. 

2915 

2916 Parameters 

2917 ---------- 

2918 func : function 

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

2920 dropna : bool 

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

2922 groups that evaluate False are filled with NaNs. 

2923 *args : tuple 

2924 Additional positional arguments to pass to `func`. 

2925 **kwargs : dict 

2926 Additional keyword arguments to pass to `func`. 

2927 

2928 Returns 

2929 ------- 

2930 DataFrame 

2931 The filtered subset of the original DataFrame. 

2932 

2933 See Also 

2934 -------- 

2935 DataFrame.filter: Filter elements of ungrouped DataFrame. 

2936 SeriesGroupBy.filter : Filter elements from groups based on criterion. 

2937 

2938 Notes 

2939 ----- 

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

2941 which group you are working on. 

2942 

2943 Functions that mutate the passed object can produce unexpected 

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

2945 for more details. 

2946 

2947 Examples 

2948 -------- 

2949 >>> df = pd.DataFrame( 

2950 ... { 

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

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

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

2954 ... } 

2955 ... ) 

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

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

2958 A B C 

2959 1 bar 2 5.0 

2960 3 bar 4 1.0 

2961 5 bar 6 9.0 

2962 """ 

2963 indices = [] 

2964 

2965 obj = self._selected_obj 

2966 gen = self._grouper.get_iterator(obj) 

2967 

2968 for name, group in gen: 

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

2970 # docstring above. 

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

2972 

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

2974 

2975 try: 

2976 res = res.squeeze() 

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

2978 pass 

2979 

2980 # interpret the result of the filter 

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

2982 if notna(res) and res: 

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

2984 else: 

2985 # non scalars aren't allowed 

2986 raise TypeError( 

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

2988 "but expected a scalar bool" 

2989 ) 

2990 

2991 return self._apply_filter(indices, dropna) 

2992 

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

2994 # per GH 23566 

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

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

2997 # valid syntax, so don't raise 

2998 raise ValueError( 

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

3000 "Use a list instead." 

3001 ) 

3002 return super().__getitem__(key) 

3003 

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

3005 """ 

3006 sub-classes to define 

3007 return a sliced object 

3008 

3009 Parameters 

3010 ---------- 

3011 key : string / list of selections 

3012 ndim : {1, 2} 

3013 requested ndim of result 

3014 subset : object, default None 

3015 subset to act on 

3016 """ 

3017 if ndim == 2: 

3018 if subset is None: 

3019 subset = self.obj 

3020 return DataFrameGroupBy( 

3021 subset, 

3022 self.keys, 

3023 level=self.level, 

3024 grouper=self._grouper, 

3025 exclusions=self.exclusions, 

3026 selection=key, 

3027 as_index=self.as_index, 

3028 sort=self.sort, 

3029 group_keys=self.group_keys, 

3030 observed=self.observed, 

3031 dropna=self.dropna, 

3032 ) 

3033 elif ndim == 1: 

3034 if subset is None: 

3035 subset = self.obj[key] 

3036 return SeriesGroupBy( 

3037 subset, 

3038 self.keys, 

3039 level=self.level, 

3040 grouper=self._grouper, 

3041 exclusions=self.exclusions, 

3042 selection=key, 

3043 as_index=self.as_index, 

3044 sort=self.sort, 

3045 group_keys=self.group_keys, 

3046 observed=self.observed, 

3047 dropna=self.dropna, 

3048 ) 

3049 

3050 raise AssertionError("invalid ndim for _gotitem") 

3051 

3052 def _get_data_to_aggregate( 

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

3054 ) -> BlockManager: 

3055 obj = self._obj_with_exclusions 

3056 mgr = obj._mgr 

3057 if numeric_only: 

3058 mgr = mgr.get_numeric_data() 

3059 return mgr 

3060 

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

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

3063 

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

3065 from pandas.core.reshape.concat import concat 

3066 

3067 obj = self._obj_with_exclusions 

3068 columns = obj.columns 

3069 sgbs = ( 

3070 SeriesGroupBy( 

3071 obj.iloc[:, i], 

3072 selection=colname, 

3073 grouper=self._grouper, 

3074 exclusions=self.exclusions, 

3075 observed=self.observed, 

3076 ) 

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

3078 ) 

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

3080 

3081 if not results: 

3082 # concat would raise 

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

3084 else: 

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

3086 

3087 if not self.as_index: 

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

3089 res_df = self._insert_inaxis_grouper(res_df) 

3090 return res_df 

3091 

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

3093 """ 

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

3095 

3096 This method counts the number of distinct values for each column 

3097 within each group, optionally excluding NaN values. 

3098 

3099 Parameters 

3100 ---------- 

3101 dropna : bool, default True 

3102 Don't include NaN in the counts. 

3103 

3104 Returns 

3105 ------- 

3106 nunique: DataFrame 

3107 Counts of unique elements in each position. 

3108 

3109 See Also 

3110 -------- 

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

3112 

3113 Examples 

3114 -------- 

3115 >>> df = pd.DataFrame( 

3116 ... { 

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

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

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

3120 ... } 

3121 ... ) 

3122 >>> df 

3123 id value1 value2 

3124 0 spam 1 a 

3125 1 egg 5 b 

3126 2 egg 5 b 

3127 3 spam 2 a 

3128 4 ham 5 x 

3129 5 ham 5 y 

3130 

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

3132 value1 value2 

3133 id 

3134 egg 1 1 

3135 ham 1 2 

3136 spam 2 1 

3137 

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

3139 

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

3141 id value1 value2 

3142 0 spam 1 a 

3143 3 spam 2 a 

3144 4 ham 5 x 

3145 5 ham 5 y 

3146 """ 

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

3148 

3149 def idxmax( 

3150 self, 

3151 skipna: bool = True, 

3152 numeric_only: bool = False, 

3153 ) -> DataFrame: 

3154 """ 

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

3156 

3157 For each group and each column, identifies the row label where the 

3158 maximum value first occurs. NA values are excluded by default. 

3159 

3160 Parameters 

3161 ---------- 

3162 skipna : bool, default True 

3163 Exclude NA values. 

3164 numeric_only : bool, default False 

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

3166 

3167 Returns 

3168 ------- 

3169 DataFrame 

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

3171 

3172 Raises 

3173 ------ 

3174 ValueError 

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

3176 

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

3178 * All values for a group are NA. 

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

3180 

3181 .. versionchanged:: 3.0.0 

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

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

3184 

3185 See Also 

3186 -------- 

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

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

3189 

3190 Notes 

3191 ----- 

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

3193 

3194 Examples 

3195 -------- 

3196 Consider a dataset containing food consumption in Argentina. 

3197 

3198 >>> df = pd.DataFrame( 

3199 ... { 

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

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

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

3203 ... }, 

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

3205 ... ) 

3206 

3207 >>> df 

3208 consumption co2_emissions food_type 

3209 Pork 10.51 37.20 meat 

3210 Wheat Products 103.11 19.66 plant 

3211 Beef 55.48 1712.00 meat 

3212 

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

3214 according to the group. 

3215 

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

3217 consumption co2_emissions 

3218 food_type 

3219 meat Beef Beef 

3220 plant Wheat Products Wheat Products 

3221 """ 

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

3223 

3224 def idxmin( 

3225 self, 

3226 skipna: bool = True, 

3227 numeric_only: bool = False, 

3228 ) -> DataFrame: 

3229 """ 

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

3231 

3232 For each group and each column, identifies the row label where the 

3233 minimum value first occurs. NA values are excluded by default. 

3234 

3235 Parameters 

3236 ---------- 

3237 skipna : bool, default True 

3238 Exclude NA values. 

3239 numeric_only : bool, default False 

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

3241 

3242 Returns 

3243 ------- 

3244 DataFrame 

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

3246 

3247 Raises 

3248 ------ 

3249 ValueError 

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

3251 

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

3253 * All values for a group are NA. 

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

3255 

3256 .. versionchanged:: 3.0.0 

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

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

3259 

3260 See Also 

3261 -------- 

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

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

3264 

3265 Notes 

3266 ----- 

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

3268 

3269 Examples 

3270 -------- 

3271 Consider a dataset containing food consumption in Argentina. 

3272 

3273 >>> df = pd.DataFrame( 

3274 ... { 

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

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

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

3278 ... }, 

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

3280 ... ) 

3281 

3282 >>> df 

3283 consumption co2_emissions food_type 

3284 Pork 10.51 37.20 meat 

3285 Wheat Products 103.11 19.66 plant 

3286 Beef 55.48 1712.00 meat 

3287 

3288 By default, it returns the index for the minimum value in each column 

3289 according to the group. 

3290 

3291 >>> df.groupby("food_type").idxmin() 

3292 consumption co2_emissions 

3293 food_type 

3294 meat Pork Pork 

3295 plant Wheat Products Wheat Products 

3296 """ 

3297 return self._idxmax_idxmin("idxmin", numeric_only=numeric_only, skipna=skipna) 

3298 

3299 boxplot = boxplot_frame_groupby 

3300 

3301 def describe( 

3302 self, 

3303 percentiles=None, 

3304 include=None, 

3305 exclude=None, 

3306 ) -> DataFrame: 

3307 """ 

3308 Generate descriptive statistics for each group. 

3309 

3310 Within each group, summarize the central tendency, dispersion, 

3311 and shape of each analyzed column's distribution, excluding 

3312 ``NaN`` values. By default only numeric columns are analyzed; 

3313 pass ``include`` to also analyze non-numeric columns (or 

3314 ``exclude`` to omit columns by dtype). 

3315 

3316 Parameters 

3317 ---------- 

3318 percentiles : list-like of numbers, optional 

3319 The percentiles to include in the output. All should fall 

3320 between 0 and 1. The default, ``None``, returns the 25th, 

3321 50th, and 75th percentiles. 

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

3323 Which column dtypes to include. Options: 

3324 

3325 - ``'all'`` : Include all columns, including non-numeric ones. 

3326 - list-like of dtypes : Limit the result to columns of the 

3327 given dtypes, in the style of 

3328 :meth:`DataFrame.select_dtypes` (e.g. ``include=[np.number]`` 

3329 or ``include=["category"]``). 

3330 - ``None`` (default) : Include only numeric columns, falling 

3331 back to object and categorical columns if there are no 

3332 numeric columns. 

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

3334 Column dtypes to omit from the result, in the style of 

3335 :meth:`DataFrame.select_dtypes`. ``None`` (default) excludes 

3336 nothing. 

3337 

3338 Returns 

3339 ------- 

3340 DataFrame 

3341 One row per group. The columns form a MultiIndex whose 

3342 outer level is the analyzed column and whose inner level is 

3343 the statistic name. 

3344 

3345 See Also 

3346 -------- 

3347 DataFrame.describe : Generate descriptive statistics of a DataFrame. 

3348 SeriesGroupBy.describe : Generate descriptive statistics for each 

3349 group of a Series. 

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

3351 columns based on their dtype. 

3352 

3353 Notes 

3354 ----- 

3355 For numeric columns, the per-group statistics are ``count``, 

3356 ``mean``, ``std``, ``min``, ``max``, and the requested 

3357 percentiles. By default the lower percentile is ``25`` and the 

3358 upper is ``75``; the ``50`` percentile is the same as the median. 

3359 

3360 For object columns, the per-group statistics are ``count``, 

3361 ``unique``, ``top``, and ``freq``. The ``top`` is the most common 

3362 value within the group and ``freq`` is its count. 

3363 

3364 Examples 

3365 -------- 

3366 >>> df = pd.DataFrame({"group": ["a", "a", "b", "b"], "value": [1, 2, 3, 4]}) 

3367 >>> df.groupby("group").describe() 

3368 value 

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

3370 group 

3371 a 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 

3372 b 2.0 3.5 0.707107 3.0 3.25 3.5 3.75 4.0 

3373 """ 

3374 return super().describe( 

3375 percentiles=percentiles, include=include, exclude=exclude 

3376 ) 

3377 

3378 def value_counts( 

3379 self, 

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

3381 normalize: bool = False, 

3382 sort: bool = True, 

3383 ascending: bool = False, 

3384 dropna: bool = True, 

3385 ) -> DataFrame | Series: 

3386 """ 

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

3388 

3389 The resulting object will be in descending order so that the 

3390 first element in each group is the most frequently-occurring row. 

3391 

3392 Parameters 

3393 ---------- 

3394 subset : list-like, optional 

3395 Columns to use when counting unique combinations. 

3396 normalize : bool, default False 

3397 Return proportions rather than frequencies. 

3398 sort : bool, default True 

3399 Stable sort by frequencies when True. When False, non-grouping 

3400 columns will appear in the order they occur in within groups. 

3401 

3402 .. versionchanged:: 3.0.0 

3403 

3404 In prior versions, ``sort=False`` would sort the non-grouping columns 

3405 by label. 

3406 ascending : bool, default False 

3407 Sort in ascending order. 

3408 dropna : bool, default True 

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

3410 

3411 Returns 

3412 ------- 

3413 Series or DataFrame 

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

3415 

3416 See Also 

3417 -------- 

3418 Series.value_counts: Equivalent method on Series. 

3419 DataFrame.value_counts: Equivalent method on DataFrame. 

3420 SeriesGroupBy.value_counts: Equivalent method on SeriesGroupBy. 

3421 

3422 Notes 

3423 ----- 

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

3425 MultiIndex with one level per input column. 

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

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

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

3429 

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

3431 the result. 

3432 

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

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

3435 

3436 Examples 

3437 -------- 

3438 >>> df = pd.DataFrame( 

3439 ... { 

3440 ... "gender": ["male", "male", "female", "male", "female", "male"], 

3441 ... "education": ["low", "medium", "high", "low", "high", "low"], 

3442 ... "country": ["US", "FR", "US", "FR", "FR", "FR"], 

3443 ... } 

3444 ... ) 

3445 

3446 >>> df 

3447 gender education country 

3448 0 male low US 

3449 1 male medium FR 

3450 2 female high US 

3451 3 male low FR 

3452 4 female high FR 

3453 5 male low FR 

3454 

3455 >>> df.groupby("gender").value_counts() 

3456 gender education country 

3457 female high US 1 

3458 FR 1 

3459 male low FR 2 

3460 US 1 

3461 medium FR 1 

3462 Name: count, dtype: int64 

3463 

3464 >>> df.groupby("gender").value_counts(ascending=True) 

3465 gender education country 

3466 female high US 1 

3467 FR 1 

3468 male low US 1 

3469 medium FR 1 

3470 low FR 2 

3471 Name: count, dtype: int64 

3472 

3473 >>> df.groupby("gender").value_counts(normalize=True) 

3474 gender education country 

3475 female high US 0.50 

3476 FR 0.50 

3477 male low FR 0.50 

3478 US 0.25 

3479 medium FR 0.25 

3480 Name: proportion, dtype: float64 

3481 

3482 >>> df.groupby("gender", as_index=False).value_counts() 

3483 gender education country count 

3484 0 female high US 1 

3485 1 female high FR 1 

3486 2 male low FR 2 

3487 3 male low US 1 

3488 4 male medium FR 1 

3489 

3490 >>> df.groupby("gender", as_index=False).value_counts(normalize=True) 

3491 gender education country proportion 

3492 0 female high US 0.50 

3493 1 female high FR 0.50 

3494 2 male low FR 0.50 

3495 3 male low US 0.25 

3496 4 male medium FR 0.25 

3497 """ 

3498 return self._value_counts(subset, normalize, sort, ascending, dropna) 

3499 

3500 def take( 

3501 self, 

3502 indices: TakeIndexer, 

3503 **kwargs, 

3504 ) -> DataFrame: 

3505 """ 

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

3507 

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

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

3510 actual position of the element in the object. 

3511 

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

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

3514 :meth:`.DataFrameGroupBy.nth`. 

3515 

3516 Parameters 

3517 ---------- 

3518 indices : array-like 

3519 An array of ints indicating which positions to take. 

3520 

3521 **kwargs 

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

3523 output. 

3524 

3525 .. deprecated:: 3.1.0 

3526 Passing ``**kwargs`` to DataFrameGroupBy.take is deprecated 

3527 and will be removed in a future version of pandas. 

3528 

3529 Returns 

3530 ------- 

3531 DataFrame 

3532 A DataFrame containing the elements taken from each group. 

3533 

3534 See Also 

3535 -------- 

3536 DataFrame.take : Take elements from a Series along an axis. 

3537 DataFrame.loc : Select a subset of a DataFrame by labels. 

3538 DataFrame.iloc : Select a subset of a DataFrame by positions. 

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

3540 

3541 Examples 

3542 -------- 

3543 >>> df = pd.DataFrame( 

3544 ... [ 

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

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

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

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

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

3550 ... ], 

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

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

3553 ... ) 

3554 >>> df 

3555 name class max_speed 

3556 4 falcon bird 389.0 

3557 3 parrot bird 24.0 

3558 2 lion mammal 80.5 

3559 1 monkey mammal NaN 

3560 0 rabbit mammal 15.0 

3561 >>> gb = df.groupby([1, 1, 2, 2, 2]) 

3562 

3563 Take elements at rows 0 and 1. 

3564 

3565 Note how the indices selected in the result do not correspond to 

3566 our input indices 0 and 1. That's because we are selecting the 0th 

3567 and 1st rows, not rows whose indices equal 0 and 1. 

3568 

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

3570 name class max_speed 

3571 1 4 falcon bird 389.0 

3572 3 parrot bird 24.0 

3573 2 2 lion mammal 80.5 

3574 1 monkey mammal NaN 

3575 

3576 The order of the specified indices influences the order in the result. 

3577 Here, the order is swapped from the previous example. 

3578 

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

3580 name class max_speed 

3581 1 3 parrot bird 24.0 

3582 4 falcon bird 389.0 

3583 2 1 monkey mammal NaN 

3584 2 lion mammal 80.5 

3585 

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

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

3588 

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

3590 name class max_speed 

3591 1 3 parrot bird 24.0 

3592 4 falcon bird 389.0 

3593 2 0 rabbit mammal 15.0 

3594 1 monkey mammal NaN 

3595 """ 

3596 if kwargs: 

3597 warnings.warn( 

3598 "Passing additional arguments to DataFrameGroupBy.take is " 

3599 "deprecated and will be removed in a future version of pandas.", 

3600 Pandas4Warning, 

3601 stacklevel=find_stack_level(), 

3602 ) 

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

3604 return result 

3605 

3606 def skew( 

3607 self, 

3608 skipna: bool = True, 

3609 numeric_only: bool = False, 

3610 **kwargs, 

3611 ) -> DataFrame: 

3612 """ 

3613 Return unbiased skew within groups. 

3614 

3615 Normalized by N-1. 

3616 

3617 Parameters 

3618 ---------- 

3619 skipna : bool, default True 

3620 Exclude NA/null values when computing the result. 

3621 

3622 numeric_only : bool, default False 

3623 Include only float, int, boolean columns. 

3624 

3625 **kwargs 

3626 Additional keyword arguments to be passed to the function. 

3627 

3628 .. deprecated:: 3.1.0 

3629 Passing ``**kwargs`` to DataFrameGroupBy.skew is deprecated 

3630 and will be removed in a future version of pandas. 

3631 

3632 Returns 

3633 ------- 

3634 DataFrame 

3635 Unbiased skew within groups. 

3636 

3637 See Also 

3638 -------- 

3639 DataFrame.skew : Return unbiased skew over requested axis. 

3640 

3641 Examples 

3642 -------- 

3643 >>> arrays = [ 

3644 ... ["falcon", "parrot", "cockatoo", "kiwi", "lion", "monkey", "rabbit"], 

3645 ... ["bird", "bird", "bird", "bird", "mammal", "mammal", "mammal"], 

3646 ... ] 

3647 >>> index = pd.MultiIndex.from_arrays(arrays, names=("name", "class")) 

3648 >>> df = pd.DataFrame( 

3649 ... {"max_speed": [389.0, 24.0, 70.0, np.nan, 80.5, 21.5, 15.0]}, 

3650 ... index=index, 

3651 ... ) 

3652 >>> df 

3653 max_speed 

3654 name class 

3655 falcon bird 389.0 

3656 parrot bird 24.0 

3657 cockatoo bird 70.0 

3658 kiwi bird NaN 

3659 lion mammal 80.5 

3660 monkey mammal 21.5 

3661 rabbit mammal 15.0 

3662 >>> gb = df.groupby(["class"]) 

3663 >>> gb.skew() 

3664 max_speed 

3665 class 

3666 bird 1.628296 

3667 mammal 1.669046 

3668 >>> gb.skew(skipna=False) 

3669 max_speed 

3670 class 

3671 bird NaN 

3672 mammal 1.669046 

3673 """ 

3674 if kwargs: 

3675 warnings.warn( 

3676 "Passing additional arguments to DataFrameGroupBy.skew is " 

3677 "deprecated and will be removed in a future version of pandas.", 

3678 Pandas4Warning, 

3679 stacklevel=find_stack_level(), 

3680 ) 

3681 

3682 def alt(obj): 

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

3684 # TypeError and not NotImplementedError. 

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

3686 

3687 return self._cython_agg_general( 

3688 "skew", alt=alt, skipna=skipna, numeric_only=numeric_only, **kwargs 

3689 ) 

3690 

3691 def kurt( 

3692 self, 

3693 skipna: bool = True, 

3694 numeric_only: bool = False, 

3695 **kwargs, 

3696 ) -> DataFrame: 

3697 """ 

3698 Return unbiased kurtosis within groups. 

3699 

3700 Kurtosis obtained using Fisher's definition (kurtosis of normal == 0.0), 

3701 normalized by N-1. Values are computed for each numeric column 

3702 within each group. 

3703 

3704 Parameters 

3705 ---------- 

3706 skipna : bool, default True 

3707 Exclude NA/null values when computing the result. 

3708 

3709 numeric_only : bool, default False 

3710 Include only float, int, boolean columns. 

3711 

3712 **kwargs 

3713 Additional keyword arguments to be passed to the function. 

3714 

3715 Returns 

3716 ------- 

3717 DataFrame 

3718 Unbiased kurtosis within groups. 

3719 

3720 See Also 

3721 -------- 

3722 DataFrame.kurt : Return unbiased kurtosis over requested axis. 

3723 

3724 Examples 

3725 -------- 

3726 >>> arrays = [ 

3727 ... [ 

3728 ... "falcon", 

3729 ... "parrot", 

3730 ... "cockatoo", 

3731 ... "kiwi", 

3732 ... "eagle", 

3733 ... "lion", 

3734 ... "monkey", 

3735 ... "rabbit", 

3736 ... "dog", 

3737 ... "wolf", 

3738 ... ], 

3739 ... [ 

3740 ... "bird", 

3741 ... "bird", 

3742 ... "bird", 

3743 ... "bird", 

3744 ... "bird", 

3745 ... "mammal", 

3746 ... "mammal", 

3747 ... "mammal", 

3748 ... "mammal", 

3749 ... "mammal", 

3750 ... ], 

3751 ... ] 

3752 >>> index = pd.MultiIndex.from_arrays(arrays, names=("name", "class")) 

3753 >>> df = pd.DataFrame( 

3754 ... { 

3755 ... "max_speed": [ 

3756 ... 389.0, 

3757 ... 24.0, 

3758 ... 70.0, 

3759 ... np.nan, 

3760 ... 350.0, 

3761 ... 80.5, 

3762 ... 21.5, 

3763 ... 15.0, 

3764 ... 40.0, 

3765 ... 50.0, 

3766 ... ] 

3767 ... }, 

3768 ... index=index, 

3769 ... ) 

3770 >>> df 

3771 max_speed 

3772 name class 

3773 falcon bird 389.0 

3774 parrot bird 24.0 

3775 cockatoo bird 70.0 

3776 kiwi bird NaN 

3777 eagle bird 350.0 

3778 lion mammal 80.5 

3779 monkey mammal 21.5 

3780 rabbit mammal 15.0 

3781 dog mammal 40.0 

3782 wolf mammal 50.0 

3783 >>> gb = df.groupby(["class"]) 

3784 >>> gb.kurt() 

3785 max_speed 

3786 class 

3787 bird -5.493277 

3788 mammal 0.204125 

3789 >>> gb.kurt(skipna=False) 

3790 max_speed 

3791 class 

3792 bird NaN 

3793 mammal 0.204125 

3794 """ 

3795 

3796 return self._cython_agg_general( 

3797 "kurt", alt=None, skipna=skipna, numeric_only=numeric_only, **kwargs 

3798 ) 

3799 

3800 @property 

3801 def plot(self) -> GroupByPlot: 

3802 """ 

3803 Make plots of groups from a DataFrame. 

3804 

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

3806 By default, matplotlib is used. 

3807 

3808 Returns 

3809 ------- 

3810 GroupByPlot 

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

3812 

3813 See Also 

3814 -------- 

3815 DataFrame.plot : Make plots of DataFrame. 

3816 

3817 Examples 

3818 -------- 

3819 >>> df = pd.DataFrame( 

3820 ... {"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=["a", "a", "b", "b"] 

3821 ... ) 

3822 >>> g = df.groupby(level=0) 

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

3824 """ 

3825 result = GroupByPlot(self) 

3826 return result 

3827 

3828 def corr( 

3829 self, 

3830 method: str | Callable[[np.ndarray, np.ndarray], float] = "pearson", 

3831 min_periods: int = 1, 

3832 numeric_only: bool = False, 

3833 ) -> DataFrame: 

3834 """ 

3835 Compute pairwise correlation of columns, excluding NA/null values. 

3836 

3837 Computes a correlation matrix for each group, measuring the linear 

3838 or rank-based relationship between columns. 

3839 

3840 Parameters 

3841 ---------- 

3842 method : {'pearson', 'kendall', 'spearman'} or callable 

3843 Method of correlation: 

3844 

3845 * pearson : standard correlation coefficient 

3846 * kendall : Kendall Tau correlation coefficient 

3847 * spearman : Spearman rank correlation 

3848 * callable: callable with input two 1d ndarrays 

3849 and returning a float. Note that the returned matrix from corr 

3850 will have 1 along the diagonals and will be symmetric 

3851 regardless of the callable's behavior. 

3852 min_periods : int, optional 

3853 Minimum number of observations required per pair of columns 

3854 to have a valid result. Currently only available for Pearson 

3855 and Spearman correlation. 

3856 numeric_only : bool, default False 

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

3858 

3859 .. versionchanged:: 2.0.0 

3860 The default value of ``numeric_only`` is now ``False``. 

3861 

3862 Returns 

3863 ------- 

3864 DataFrame 

3865 Correlation matrix. 

3866 

3867 See Also 

3868 -------- 

3869 DataFrame.corrwith : Compute pairwise correlation with another 

3870 DataFrame or Series. 

3871 Series.corr : Compute the correlation between two Series. 

3872 

3873 Notes 

3874 ----- 

3875 Pearson, Kendall and Spearman correlation are currently computed using 

3876 pairwise complete observations. 

3877 

3878 * `Pearson correlation coefficient <https://en.wikipedia.org/wiki/Pearson_correlation_coefficient>`_ 

3879 * `Kendall rank correlation coefficient <https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient>`_ 

3880 * `Spearman's rank correlation coefficient <https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient>`_ 

3881 

3882 Examples 

3883 -------- 

3884 >>> df = pd.DataFrame( 

3885 ... { 

3886 ... "age": [2, 3, 4, 6, 6, 1, 2, 1], 

3887 ... "weight": [2.1, 3.2, 4.1, 6.5, 3.3, 2.1, 4.1, 1.9], 

3888 ... "pet": ["dog", "cat", "dog", "cat", "dog", "cat", "dog", "cat"], 

3889 ... } 

3890 ... ) 

3891 >>> df 

3892 age weight pet 

3893 0 2 2.1 dog 

3894 1 3 3.2 cat 

3895 2 4 4.1 dog 

3896 3 6 6.5 cat 

3897 4 6 3.3 dog 

3898 5 1 2.1 cat 

3899 6 2 4.1 dog 

3900 7 1 1.9 cat 

3901 >>> df.groupby("pet").corr() 

3902 age weight 

3903 pet 

3904 cat age 1.000000 0.989321 

3905 weight 0.989321 1.000000 

3906 dog age 1.000000 0.184177 

3907 weight 0.184177 1.000000 

3908 """ 

3909 result = self._op_via_apply( 

3910 "corr", method=method, min_periods=min_periods, numeric_only=numeric_only 

3911 ) 

3912 return result 

3913 

3914 def cov( 

3915 self, 

3916 min_periods: int | None = None, 

3917 ddof: int | None = 1, 

3918 numeric_only: bool = False, 

3919 ) -> DataFrame: 

3920 """ 

3921 Compute pairwise covariance of columns, excluding NA/null values. 

3922 

3923 Compute the pairwise covariance among the series of a DataFrame. 

3924 The returned data frame is the `covariance matrix 

3925 <https://en.wikipedia.org/wiki/Covariance_matrix>`__ of the columns 

3926 of the DataFrame. 

3927 

3928 Both NA and null values are automatically excluded from the 

3929 calculation. (See the note below about bias from missing values.) 

3930 A threshold can be set for the minimum number of 

3931 observations for each value created. Comparisons with observations 

3932 below this threshold will be returned as ``NaN``. 

3933 

3934 This method is generally used for the analysis of time series data to 

3935 understand the relationship between different measures 

3936 across time. 

3937 

3938 Parameters 

3939 ---------- 

3940 min_periods : int, optional 

3941 Minimum number of observations required per pair of columns 

3942 to have a valid result. 

3943 

3944 ddof : int, default 1 

3945 Delta degrees of freedom. The divisor used in calculations 

3946 is ``N - ddof``, where ``N`` represents the number of elements. 

3947 This argument is applicable only when no ``nan`` is in the dataframe. 

3948 

3949 numeric_only : bool, default False 

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

3951 

3952 .. versionchanged:: 2.0.0 

3953 The default value of ``numeric_only`` is now ``False``. 

3954 

3955 Returns 

3956 ------- 

3957 DataFrame 

3958 The covariance matrix of the series of the DataFrame. 

3959 

3960 See Also 

3961 -------- 

3962 Series.cov : Compute covariance with another Series. 

3963 core.window.ewm.ExponentialMovingWindow.cov : Exponential weighted sample 

3964 covariance. 

3965 core.window.expanding.Expanding.cov : Expanding sample covariance. 

3966 core.window.rolling.Rolling.cov : Rolling sample covariance. 

3967 

3968 Notes 

3969 ----- 

3970 Returns the covariance matrix of the DataFrame's time series. 

3971 The covariance is normalized by N-ddof. 

3972 

3973 For DataFrames that have Series that are missing data (assuming that 

3974 data is `missing at random 

3975 <https://en.wikipedia.org/wiki/Missing_data#Missing_at_random>`__) 

3976 the returned covariance matrix will be an unbiased estimate 

3977 of the variance and covariance between the member Series. 

3978 

3979 However, for many applications this estimate may not be acceptable 

3980 because the estimate covariance matrix is not guaranteed to be positive 

3981 semi-definite. This could lead to estimate correlations having 

3982 absolute values which are greater than one, and/or a non-invertible 

3983 covariance matrix. See `Estimation of covariance matrices 

3984 <https://en.wikipedia.org/w/index.php?title=Estimation_of_covariance_ 

3985 matrices>`__ for more details. 

3986 

3987 Examples 

3988 -------- 

3989 >>> df = pd.DataFrame( 

3990 ... { 

3991 ... "age": [2, 3, 4, 6, 6, 1, 2, 1], 

3992 ... "weight": [2.1, 3.2, 4.1, 6.5, 3.3, 2.1, 4.1, 1.9], 

3993 ... "pet": ["dog", "cat", "dog", "cat", "dog", "cat", "dog", "cat"], 

3994 ... } 

3995 ... ) 

3996 >>> df 

3997 age weight pet 

3998 0 2 2.1 dog 

3999 1 3 3.2 cat 

4000 2 4 4.1 dog 

4001 3 6 6.5 cat 

4002 4 6 3.3 dog 

4003 5 1 2.1 cat 

4004 6 2 4.1 dog 

4005 7 1 1.9 cat 

4006 >>> df.groupby("pet").cov() 

4007 age weight 

4008 pet 

4009 cat age 5.583333 4.975000 

4010 weight 4.975000 4.529167 

4011 dog age 3.666667 0.333333 

4012 weight 0.333333 0.893333 

4013 """ 

4014 result = self._op_via_apply( 

4015 "cov", min_periods=min_periods, ddof=ddof, numeric_only=numeric_only 

4016 ) 

4017 return result 

4018 

4019 def hist( 

4020 self, 

4021 column: IndexLabel | None = None, 

4022 by=None, 

4023 grid: bool = True, 

4024 xlabelsize: int | None = None, 

4025 xrot: float | None = None, 

4026 ylabelsize: int | None = None, 

4027 yrot: float | None = None, 

4028 ax=None, 

4029 sharex: bool = False, 

4030 sharey: bool = False, 

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

4032 layout: tuple[int, int] | None = None, 

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

4034 backend: str | None = None, 

4035 legend: bool = False, 

4036 **kwargs, 

4037 ): 

4038 """ 

4039 Draw histogram of the DataFrame's columns for each group. 

4040 

4041 A separate histogram subplot is generated for each group, making it 

4042 easier to visually compare the distribution of each numeric column 

4043 across groups. Internally this calls :meth:`DataFrame.hist` on every 

4044 group's frame, so the same matplotlib options are accepted. 

4045 

4046 Parameters 

4047 ---------- 

4048 column : str or sequence, optional 

4049 If passed, will be used to limit data to a subset of columns. 

4050 by : object, optional 

4051 If passed, then used to form histograms for separate groups. 

4052 grid : bool, default True 

4053 Whether to show axis grid lines. 

4054 xlabelsize : int, default None 

4055 If specified changes the x-axis label size. 

4056 xrot : float, default None 

4057 Rotation of x axis labels. For example, a value of 90 displays the 

4058 x labels rotated 90 degrees clockwise. 

4059 ylabelsize : int, default None 

4060 If specified changes the y-axis label size. 

4061 yrot : float, default None 

4062 Rotation of y axis labels. For example, a value of 90 displays the 

4063 y labels rotated 90 degrees clockwise. 

4064 ax : Matplotlib axes object, default None 

4065 The axes to plot the histogram on. 

4066 sharex : bool, default True if ax is None else False 

4067 In case subplots=True, share x axis and set some x axis labels to 

4068 invisible; defaults to True if ax is None otherwise False if an ax 

4069 is passed in. 

4070 Note that passing in both an ax and sharex=True will alter all x axis 

4071 labels for all subplots in a figure. 

4072 sharey : bool, default False 

4073 In case subplots=True, share y axis and set some y axis labels to 

4074 invisible. 

4075 figsize : tuple, optional 

4076 The size in inches of the figure to create. Uses the value in 

4077 `matplotlib.rcParams` by default. 

4078 layout : tuple, optional 

4079 Tuple of (rows, columns) for the layout of the histograms. 

4080 bins : int or sequence, default 10 

4081 Number of histogram bins to be used. If an integer is given, bins + 1 

4082 bin edges are calculated and returned. If bins is a sequence, gives 

4083 bin edges, including left edge of first bin and right edge of last 

4084 bin. In this case, bins is returned unmodified. 

4085 

4086 backend : str, default None 

4087 Backend to use instead of the backend specified in the option 

4088 ``plotting.backend``. For instance, 'matplotlib'. Alternatively, to 

4089 specify the ``plotting.backend`` for the whole session, set 

4090 ``pd.options.plotting.backend``. 

4091 

4092 legend : bool, default False 

4093 Whether to show the legend. 

4094 

4095 **kwargs 

4096 All other plotting keyword arguments to be passed to 

4097 :meth:`matplotlib.pyplot.hist`. 

4098 

4099 Returns 

4100 ------- 

4101 matplotlib.Axes or numpy.ndarray 

4102 A ``matplotlib.Axes`` object or an array of ``Axes`` objects, depending on 

4103 the layout and grouping. 

4104 

4105 See Also 

4106 -------- 

4107 DataFrame.hist : Equivalent histogram plotting method on DataFrame. 

4108 matplotlib.pyplot.hist : Plot a histogram using matplotlib. 

4109 

4110 Examples 

4111 -------- 

4112 For each animal category, draw a histogram of the numeric columns. 

4113 The grouping column is used to split the data; one set of histogram 

4114 subplots is produced per group. 

4115 

4116 .. plot:: 

4117 :context: close-figs 

4118 

4119 >>> df = pd.DataFrame( 

4120 ... { 

4121 ... "animal": ["cat", "cat", "dog", "dog", "dog"], 

4122 ... "length": [1.0, 1.2, 1.5, 1.7, 2.0], 

4123 ... "weight": [4.5, 5.0, 10.0, 12.5, 15.0], 

4124 ... } 

4125 ... ) 

4126 >>> hist = df.groupby("animal").hist(bins=3) 

4127 """ 

4128 result = self._op_via_apply( 

4129 "hist", 

4130 column=column, 

4131 by=by, 

4132 grid=grid, 

4133 xlabelsize=xlabelsize, 

4134 xrot=xrot, 

4135 ylabelsize=ylabelsize, 

4136 yrot=yrot, 

4137 ax=ax, 

4138 sharex=sharex, 

4139 sharey=sharey, 

4140 figsize=figsize, 

4141 layout=layout, 

4142 bins=bins, 

4143 backend=backend, 

4144 legend=legend, 

4145 **kwargs, 

4146 ) 

4147 return result 

4148 

4149 def corrwith( 

4150 self, 

4151 other: DataFrame | Series, 

4152 drop: bool = False, 

4153 method: CorrelationMethod = "pearson", 

4154 numeric_only: bool = False, 

4155 ) -> DataFrame: 

4156 """ 

4157 Compute pairwise correlation. 

4158 

4159 .. deprecated:: 3.0.0 

4160 

4161 Pairwise correlation is computed between rows or columns of 

4162 DataFrame with rows or columns of Series or DataFrame. DataFrames 

4163 are first aligned along both axes before computing the 

4164 correlations. 

4165 

4166 Parameters 

4167 ---------- 

4168 other : DataFrame, Series 

4169 Object with which to compute correlations. 

4170 drop : bool, default False 

4171 Drop missing indices from result. 

4172 method : {'pearson', 'kendall', 'spearman'} or callable 

4173 Method of correlation: 

4174 

4175 * pearson : standard correlation coefficient 

4176 * kendall : Kendall Tau correlation coefficient 

4177 * spearman : Spearman rank correlation 

4178 * callable: callable with input two 1d ndarrays 

4179 and returning a float. 

4180 

4181 numeric_only : bool, default False 

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

4183 

4184 .. versionchanged:: 2.0.0 

4185 The default value of ``numeric_only`` is now ``False``. 

4186 

4187 Returns 

4188 ------- 

4189 Series 

4190 Pairwise correlations. 

4191 

4192 See Also 

4193 -------- 

4194 DataFrame.corr : Compute pairwise correlation of columns. 

4195 

4196 Examples 

4197 -------- 

4198 >>> df1 = pd.DataFrame( 

4199 ... { 

4200 ... "Day": [1, 1, 1, 2, 2, 2, 3, 3, 3], 

4201 ... "Data": [6, 6, 8, 5, 4, 2, 7, 3, 9], 

4202 ... } 

4203 ... ) 

4204 >>> df2 = pd.DataFrame( 

4205 ... { 

4206 ... "Day": [1, 1, 1, 2, 2, 2, 3, 3, 3], 

4207 ... "Data": [5, 3, 8, 3, 1, 1, 2, 3, 6], 

4208 ... } 

4209 ... ) 

4210 

4211 >>> df1.groupby("Day").corrwith(df2) 

4212 Data Day 

4213 Day 

4214 1 0.917663 NaN 

4215 2 0.755929 NaN 

4216 3 0.576557 NaN 

4217 """ 

4218 warnings.warn( 

4219 "DataFrameGroupBy.corrwith is deprecated", 

4220 Pandas4Warning, 

4221 stacklevel=find_stack_level(), 

4222 ) 

4223 result = self._op_via_apply( 

4224 "corrwith", 

4225 other=other, 

4226 drop=drop, 

4227 method=method, 

4228 numeric_only=numeric_only, 

4229 ) 

4230 return result 

4231 

4232 

4233def _wrap_transform_general_frame( 

4234 obj: DataFrame, group: DataFrame, res: DataFrame | Series 

4235) -> DataFrame: 

4236 from pandas import concat 

4237 

4238 if isinstance(res, Series): 

4239 # we need to broadcast across the 

4240 # other dimension; this will preserve dtypes 

4241 # GH14457 

4242 if res.index.is_(obj.index): 

4243 res_frame = concat([res] * len(group.columns), axis=1, ignore_index=True) 

4244 res_frame.columns = group.columns 

4245 res_frame.index = group.index 

4246 else: 

4247 res_frame = obj._constructor( 

4248 np.tile(res.values, (len(group.index), 1)), 

4249 columns=group.columns, 

4250 index=group.index, 

4251 ) 

4252 assert isinstance(res_frame, DataFrame) 

4253 return res_frame 

4254 elif isinstance(res, DataFrame) and not res.index.is_(group.index): 

4255 return res._align_frame(group)[0] 

4256 else: 

4257 return res