Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/apply.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

965 statements  

1from __future__ import annotations 

2 

3import abc 

4from collections import defaultdict 

5import functools 

6from functools import partial 

7import inspect 

8from typing import ( 

9 TYPE_CHECKING, 

10 Any, 

11 Literal, 

12 TypeAlias, 

13 cast, 

14) 

15 

16import numpy as np 

17 

18from pandas._libs.internals import BlockValuesRefs 

19from pandas.compat._optional import import_optional_dependency 

20from pandas.errors import SpecificationError 

21from pandas.util._decorators import ( 

22 cache_readonly, 

23 set_module, 

24) 

25 

26from pandas.core.dtypes.cast import ( 

27 find_common_type, 

28 is_nested_object, 

29) 

30from pandas.core.dtypes.common import ( 

31 is_dict_like, 

32 is_extension_array_dtype, 

33 is_list_like, 

34 is_numeric_dtype, 

35 is_sequence, 

36) 

37from pandas.core.dtypes.dtypes import ( 

38 ArrowDtype, 

39 BaseMaskedDtype, 

40 ExtensionDtype, 

41) 

42from pandas.core.dtypes.generic import ( 

43 ABCDataFrame, 

44 ABCNDFrame, 

45 ABCSeries, 

46) 

47 

48from pandas.core._numba.executor import generate_apply_looper 

49import pandas.core.common as com 

50from pandas.core.construction import ensure_wrapped_if_datetimelike 

51from pandas.core.reshape.concat import concat 

52from pandas.core.util.numba_ import ( 

53 get_jit_arguments, 

54 prepare_function_arguments, 

55) 

56 

57if TYPE_CHECKING: 

58 from collections.abc import ( 

59 Callable, 

60 Generator, 

61 Hashable, 

62 Iterable, 

63 MutableMapping, 

64 Sequence, 

65 ) 

66 

67 from pandas._typing import ( 

68 AggFuncType, 

69 AggFuncTypeBase, 

70 AggFuncTypeDict, 

71 AggObjType, 

72 Axis, 

73 AxisInt, 

74 DtypeObj, 

75 NDFrameT, 

76 npt, 

77 ) 

78 

79 from pandas import ( 

80 DataFrame, 

81 Index, 

82 Series, 

83 ) 

84 from pandas.core.arrays import ( 

85 ArrowExtensionArray, 

86 BaseMaskedArray, 

87 ExtensionArray, 

88 ) 

89 from pandas.core.groupby import GroupBy 

90 from pandas.core.groupby.groupby import BaseGroupBy 

91 from pandas.core.resample import Resampler 

92 from pandas.core.window.rolling import BaseWindow 

93 

94ResType: TypeAlias = dict[int, Any] 

95 

96# DataFrame methods that reduce column-wise to a Series indexed by the 

97# columns, making them eligible for the fast path in 

98# FrameApply._agg_list_like_frame_reductions. 

99_frame_reduction_names = frozenset( 

100 { 

101 "all", 

102 "any", 

103 "count", 

104 "idxmax", 

105 "idxmin", 

106 "kurt", 

107 "kurtosis", 

108 "max", 

109 "mean", 

110 "median", 

111 "min", 

112 "nunique", 

113 "prod", 

114 "product", 

115 "quantile", 

116 "sem", 

117 "skew", 

118 "std", 

119 "sum", 

120 "var", 

121 } 

122) 

123 

124 

125def _stack_as_object(dtypes: list[DtypeObj]) -> bool: 

126 """ 

127 Whether results of these dtypes must be stacked as object to keep their 

128 values: bools mixed with other dtypes can be cast to numbers, and signed 

129 with unsigned integers to a float, which rounds values above 2**53. 

130 """ 

131 kinds = {dtype.kind for dtype in dtypes} 

132 if "b" in kinds and len(kinds) > 1: 

133 return True 

134 return {"i", "u"} <= kinds and find_common_type(dtypes).kind == "f" 

135 

136 

137@set_module("pandas.api.executors") 

138class BaseExecutionEngine(abc.ABC): 

139 """ 

140 Base class for execution engines for map and apply methods. 

141 

142 An execution engine receives all the parameters of a call to 

143 ``apply`` or ``map``, such as the data container, the function, 

144 etc. and takes care of running the execution. 

145 

146 Supporting different engines allows functions to be JIT compiled, 

147 run in parallel, and others. Besides the default executor which 

148 simply runs the code with the Python interpreter and pandas. 

149 """ 

150 

151 @staticmethod 

152 @abc.abstractmethod 

153 def map( 

154 data: Series | DataFrame | np.ndarray, 

155 func: AggFuncType, 

156 args: tuple, 

157 kwargs: dict[str, Any], 

158 decorator: Callable | None, 

159 skip_na: bool, 

160 ): 

161 """ 

162 Executor method to run functions elementwise. 

163 

164 In general, pandas uses ``map`` for running functions elementwise, 

165 but ``Series.apply`` with the default ``by_row='compat'`` will also 

166 call this executor function. 

167 

168 Parameters 

169 ---------- 

170 data : Series, DataFrame or NumPy ndarray 

171 The object to use for the data. Some methods implement a ``raw`` 

172 parameter which will convert the original pandas object to a 

173 NumPy array, which will then be passed here to the executor. 

174 func : function or NumPy ufunc 

175 The function to execute. 

176 args : tuple 

177 Positional arguments to be passed to ``func``. 

178 kwargs : dict 

179 Keyword arguments to be passed to ``func``. 

180 decorator : function, optional 

181 For JIT compilers and other engines that need to decorate the 

182 function ``func``, this is the decorator to use. While the 

183 executor may already know which is the decorator to use, this 

184 is useful as for a single executor the user can specify for 

185 example ``numba.jit`` or ``numba.njit(nogil=True)``, and this 

186 decorator parameter will contain the exact decorator from the 

187 executor the user wants to use. 

188 skip_na : bool 

189 Whether the function should be called for missing values or not. 

190 This is specified by the pandas user as ``map(na_action=None)`` 

191 or ``map(na_action='ignore')``. 

192 """ 

193 

194 @staticmethod 

195 @abc.abstractmethod 

196 def apply( 

197 data: Series | DataFrame | np.ndarray, 

198 func: AggFuncType, 

199 args: tuple, 

200 kwargs: dict[str, Any], 

201 decorator: Callable, 

202 axis: Axis, 

203 ): 

204 """ 

205 Executor method to run functions by an axis. 

206 

207 While we can see ``map`` as executing the function for each cell 

208 in a ``DataFrame`` (or ``Series``), ``apply`` will execute the 

209 function for each column (or row). 

210 

211 Parameters 

212 ---------- 

213 data : Series, DataFrame or NumPy ndarray 

214 The object to use for the data. Some methods implement a ``raw`` 

215 parameter which will convert the original pandas object to a 

216 NumPy array, which will then be passed here to the executor. 

217 func : function or NumPy ufunc 

218 The function to execute. 

219 args : tuple 

220 Positional arguments to be passed to ``func``. 

221 kwargs : dict 

222 Keyword arguments to be passed to ``func``. 

223 decorator : function, optional 

224 For JIT compilers and other engines that need to decorate the 

225 function ``func``, this is the decorator to use. While the 

226 executor may already know which is the decorator to use, this 

227 is useful as for a single executor the user can specify for 

228 example ``numba.jit`` or ``numba.njit(nogil=True)``, and this 

229 decorator parameter will contain the exact decorator from the 

230 executor the user wants to use. 

231 axis : {0 or 'index', 1 or 'columns'} 

232 0 or 'index' should execute the function passing each column as 

233 parameter. 1 or 'columns' should execute the function passing 

234 each row as parameter. The default executor engine passes rows 

235 as pandas ``Series``. Other executor engines should probably 

236 expect functions to be implemented this way for compatibility. 

237 But passing rows as other data structures is technically possible 

238 as far as the function ``func`` is implemented accordingly. 

239 """ 

240 

241 

242def frame_apply( 

243 obj: DataFrame, 

244 func: AggFuncType, 

245 axis: Axis = 0, 

246 raw: bool = False, 

247 result_type: str | None = None, 

248 by_row: Literal[False, "compat"] = "compat", 

249 engine: str = "python", 

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

251 args=None, 

252 kwargs=None, 

253) -> FrameApply: 

254 """construct and return a row or column based frame apply object""" 

255 _, func, columns, _ = reconstruct_func(func, **kwargs) 

256 

257 axis = obj._get_axis_number(axis) 

258 klass: type[FrameApply] 

259 if axis == 0: 

260 klass = FrameRowApply 

261 elif axis == 1: 

262 if columns: 

263 raise NotImplementedError( 

264 f"Named aggregation is not supported when {axis=}." 

265 ) 

266 klass = FrameColumnApply 

267 

268 return klass( 

269 obj, 

270 func, 

271 raw=raw, 

272 result_type=result_type, 

273 by_row=by_row, 

274 engine=engine, 

275 engine_kwargs=engine_kwargs, 

276 args=args, 

277 kwargs=kwargs, 

278 ) 

279 

280 

281class Apply(metaclass=abc.ABCMeta): 

282 axis: AxisInt 

283 

284 def __init__( 

285 self, 

286 obj: AggObjType, 

287 func: AggFuncType, 

288 raw: bool, 

289 result_type: str | None, 

290 *, 

291 by_row: Literal[False, "compat", "_compat"] = "compat", 

292 engine: str = "python", 

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

294 args, 

295 kwargs, 

296 ) -> None: 

297 self.obj = obj 

298 self.raw = raw 

299 

300 assert by_row is False or by_row in ["compat", "_compat"] 

301 self.by_row = by_row 

302 

303 self.args = args or () 

304 self.kwargs = kwargs or {} 

305 

306 self.engine = engine 

307 self.engine_kwargs = {} if engine_kwargs is None else engine_kwargs 

308 

309 if result_type not in [None, "reduce", "broadcast", "expand"]: 

310 raise ValueError( 

311 "invalid value for result_type, must be one " 

312 "of {None, 'reduce', 'broadcast', 'expand'}" 

313 ) 

314 

315 self.result_type = result_type 

316 

317 self.func = func 

318 

319 @abc.abstractmethod 

320 def apply(self) -> DataFrame | Series: 

321 pass 

322 

323 @abc.abstractmethod 

324 def agg_or_apply_list_like( 

325 self, op_name: Literal["agg", "apply"] 

326 ) -> DataFrame | Series: 

327 pass 

328 

329 @abc.abstractmethod 

330 def agg_or_apply_dict_like( 

331 self, op_name: Literal["agg", "apply"] 

332 ) -> DataFrame | Series: 

333 pass 

334 

335 def agg(self) -> DataFrame | Series | None: 

336 """ 

337 Provide an implementation for the aggregators. 

338 

339 Returns 

340 ------- 

341 Result of aggregation, or None if agg cannot be performed by 

342 this method. 

343 """ 

344 func = self.func 

345 

346 if isinstance(func, str): 

347 return self.apply_str() 

348 

349 if is_dict_like(func): 

350 return self.agg_dict_like() 

351 elif is_list_like(func): 

352 # we require a list, but not a 'str' 

353 return self.agg_list_like() 

354 

355 # caller can react 

356 return None 

357 

358 def transform(self) -> DataFrame | Series: 

359 """ 

360 Transform a DataFrame or Series. 

361 

362 Returns 

363 ------- 

364 DataFrame or Series 

365 Result of applying ``func`` along the given axis of the 

366 Series or DataFrame. 

367 

368 Raises 

369 ------ 

370 ValueError 

371 If the transform function fails or does not transform. 

372 """ 

373 obj = self.obj 

374 func = self.func 

375 axis = self.axis 

376 args = self.args 

377 kwargs = self.kwargs 

378 

379 is_series = obj.ndim == 1 

380 

381 if obj._get_axis_number(axis) == 1: 

382 assert not is_series 

383 return obj.T.transform(func, 0, *args, **kwargs).T 

384 

385 if is_list_like(func) and not is_dict_like(func): 

386 func = cast("list[AggFuncTypeBase]", func) 

387 # GH#54929 - raise if duplicate function names are passed 

388 if len(func) > len(set(func)): 

389 raise SpecificationError( 

390 "Function names must be unique if there is no new column names " 

391 "assigned" 

392 ) 

393 # Convert func equivalent dict 

394 if is_series: 

395 func = {com.get_callable_name(v) or v: v for v in func} 

396 else: 

397 func = dict.fromkeys(obj, func) 

398 

399 if is_dict_like(func): 

400 func = cast("AggFuncTypeDict", func) 

401 return self.transform_dict_like(func) 

402 

403 # func is either str or callable 

404 func = cast("AggFuncTypeBase", func) 

405 try: 

406 result = self.transform_str_or_callable(func) 

407 except TypeError: 

408 raise 

409 except Exception as err: 

410 raise ValueError("Transform function failed") from err 

411 

412 # Functions that transform may return empty Series/DataFrame 

413 # when the dtype is not appropriate 

414 if ( 

415 isinstance(result, (ABCSeries, ABCDataFrame)) 

416 and result.empty 

417 and not obj.empty 

418 ): 

419 raise ValueError("Transform function failed") 

420 if not isinstance(result, (ABCSeries, ABCDataFrame)) or not result.index.equals( 

421 obj.index 

422 ): 

423 raise ValueError("Function did not transform") 

424 

425 return result 

426 

427 def transform_dict_like(self, func) -> DataFrame: 

428 """ 

429 Compute transform in the case of a dict-like func 

430 """ 

431 

432 obj = self.obj 

433 args = self.args 

434 kwargs = self.kwargs 

435 

436 # transform is currently only for Series/DataFrame 

437 assert isinstance(obj, ABCNDFrame) 

438 

439 if len(func) == 0: 

440 raise ValueError("No transform functions were provided") 

441 

442 func = self.normalize_dictlike_arg("transform", obj, func) 

443 

444 results: dict[Hashable, DataFrame | Series] = {} 

445 for name, how in func.items(): 

446 colg = obj._gotitem(name, ndim=1) 

447 results[name] = colg.transform(how, 0, *args, **kwargs) 

448 return concat(results, axis=1) 

449 

450 def transform_str_or_callable(self, func) -> DataFrame | Series: 

451 """ 

452 Compute transform in the case of a string or callable func 

453 """ 

454 obj = self.obj 

455 args = self.args 

456 kwargs = self.kwargs 

457 

458 if isinstance(func, str): 

459 return self._apply_str(obj, func, *args, **kwargs) 

460 

461 # Two possible ways to use a UDF - apply or call directly 

462 try: 

463 return obj.apply(func, args=args, **kwargs) 

464 except Exception: 

465 return func(obj, *args, **kwargs) 

466 

467 def agg_list_like(self) -> DataFrame | Series: 

468 """ 

469 Compute aggregation in the case of a list-like argument. 

470 

471 Returns 

472 ------- 

473 Result of aggregation. 

474 """ 

475 return self.agg_or_apply_list_like(op_name="agg") 

476 

477 def compute_list_like( 

478 self, 

479 op_name: Literal["agg", "apply"], 

480 selected_obj: Series | DataFrame, 

481 kwargs: dict[str, Any], 

482 ) -> tuple[list[Hashable] | Index, list[Any]]: 

483 """ 

484 Compute agg/apply results for like-like input. 

485 

486 Parameters 

487 ---------- 

488 op_name : {"agg", "apply"} 

489 Operation being performed. 

490 selected_obj : Series or DataFrame 

491 Data to perform operation on. 

492 kwargs : dict 

493 Keyword arguments to pass to the functions. 

494 

495 Returns 

496 ------- 

497 keys : list[Hashable] or Index 

498 Index labels for result. 

499 results : list 

500 Data for result. When aggregating with a Series, this can contain any 

501 Python objects. 

502 """ 

503 func = cast("list[AggFuncTypeBase]", self.func) 

504 obj = self.obj 

505 

506 results = [] 

507 keys: list[Hashable] = [] 

508 

509 # degenerate case 

510 if selected_obj.ndim == 1: 

511 for a in func: 

512 colg = obj._gotitem(selected_obj.name, ndim=1, subset=selected_obj) 

513 args = ( 

514 [self.axis, *self.args] 

515 if include_axis(op_name, colg) 

516 else self.args 

517 ) 

518 new_res = getattr(colg, op_name)(a, *args, **kwargs) 

519 results.append(new_res) 

520 

521 # make sure we find a good name 

522 name = com.get_callable_name(a) or a 

523 keys.append(name) 

524 

525 else: 

526 indices = [] 

527 for index, col in enumerate(selected_obj): 

528 colg = obj._gotitem(col, ndim=1, subset=selected_obj.iloc[:, index]) 

529 args = ( 

530 [self.axis, *self.args] 

531 if include_axis(op_name, colg) 

532 else self.args 

533 ) 

534 new_res = getattr(colg, op_name)(func, *args, **kwargs) 

535 results.append(new_res) 

536 indices.append(index) 

537 # error: Incompatible types in assignment (expression has type "Any | 

538 # Index", variable has type "list[Any | Callable[..., Any] | str]") 

539 keys = selected_obj.columns.take(indices) # type: ignore[assignment] 

540 

541 return keys, results 

542 

543 def wrap_results_list_like( 

544 self, keys: Iterable[Hashable], results: list[Series | DataFrame] 

545 ): 

546 from pandas import Index 

547 from pandas.core.groupby.groupby import BaseGroupBy 

548 

549 obj = self.obj 

550 

551 if not results: 

552 # GH#39609 

553 if isinstance(obj, ABCSeries): 

554 return obj._constructor(index=Index([]), name=obj.name) 

555 elif isinstance(obj, BaseGroupBy) and obj._selected_obj.ndim == 1: 

556 return obj._selected_obj._constructor_expanddim( 

557 index=_groupby_result_index(obj), columns=Index([]) 

558 ) 

559 

560 try: 

561 return concat(results, keys=keys, axis=1, sort=False) 

562 except TypeError as err: 

563 # we are concatting non-NDFrame objects, 

564 # e.g. a list of scalars 

565 from pandas import Series 

566 

567 result = Series(results, index=keys, name=obj.name) 

568 if is_nested_object(result): 

569 raise ValueError( 

570 "cannot combine transform and aggregation operations" 

571 ) from err 

572 return result 

573 

574 def agg_dict_like(self) -> DataFrame | Series: 

575 """ 

576 Compute aggregation in the case of a dict-like argument. 

577 

578 Returns 

579 ------- 

580 Result of aggregation. 

581 """ 

582 return self.agg_or_apply_dict_like(op_name="agg") 

583 

584 def compute_dict_like( 

585 self, 

586 op_name: Literal["agg", "apply"], 

587 selected_obj: Series | DataFrame, 

588 selection: Hashable | Sequence[Hashable], 

589 kwargs: dict[str, Any], 

590 ) -> tuple[list[Hashable], list[Any]]: 

591 """ 

592 Compute agg/apply results for dict-like input. 

593 

594 Parameters 

595 ---------- 

596 op_name : {"agg", "apply"} 

597 Operation being performed. 

598 selected_obj : Series or DataFrame 

599 Data to perform operation on. 

600 selection : hashable or sequence of hashables 

601 Used by GroupBy, Window, and Resample if selection is applied to the object. 

602 kwargs : dict 

603 Keyword arguments to pass to the functions. 

604 

605 Returns 

606 ------- 

607 keys : list[hashable] 

608 Index labels for result. 

609 results : list 

610 Data for result. When aggregating with a Series, this can contain any 

611 Python object. 

612 """ 

613 from pandas.core.groupby.generic import ( 

614 DataFrameGroupBy, 

615 SeriesGroupBy, 

616 ) 

617 

618 obj = self.obj 

619 is_groupby = isinstance(obj, (DataFrameGroupBy, SeriesGroupBy)) 

620 func = cast("AggFuncTypeDict", self.func) 

621 func = self.normalize_dictlike_arg(op_name, selected_obj, func) 

622 

623 is_non_unique_col = ( 

624 selected_obj.ndim == 2 

625 and selected_obj.columns.nunique() < len(selected_obj.columns) 

626 ) 

627 

628 if selected_obj.ndim == 1: 

629 # key only used for output 

630 colg = obj._gotitem(selection, ndim=1) 

631 results = [getattr(colg, op_name)(how, **kwargs) for _, how in func.items()] 

632 keys = list(func.keys()) 

633 elif not is_groupby and is_non_unique_col: 

634 # key used for column selection and output 

635 # GH#51099 

636 results = [] 

637 keys = [] 

638 for key, how in func.items(): 

639 indices = selected_obj.columns.get_indexer_for([key]) 

640 labels = selected_obj.columns.take(indices) 

641 label_to_indices = defaultdict(list) 

642 for index, label in zip(indices, labels, strict=True): 

643 label_to_indices[label].append(index) 

644 

645 key_data = [ 

646 getattr(selected_obj._ixs(indice, axis=1), op_name)(how, **kwargs) 

647 for label, indices in label_to_indices.items() 

648 for indice in indices 

649 ] 

650 

651 keys += [key] * len(key_data) 

652 results += key_data 

653 elif is_groupby: 

654 # key used for column selection and output 

655 

656 df = selected_obj 

657 results, keys = [], [] 

658 for key, how in func.items(): 

659 cols = df[key] 

660 

661 if cols.ndim == 1: 

662 series = obj._gotitem(key, ndim=1, subset=cols) 

663 results.append(getattr(series, op_name)(how, **kwargs)) 

664 keys.append(key) 

665 else: 

666 for _, col in cols.items(): 

667 series = obj._gotitem(key, ndim=1, subset=col) 

668 results.append(getattr(series, op_name)(how, **kwargs)) 

669 keys.append(key) 

670 else: 

671 results = [ 

672 getattr(obj._gotitem(key, ndim=1), op_name)(how, **kwargs) 

673 for key, how in func.items() 

674 ] 

675 keys = list(func.keys()) 

676 

677 return keys, results 

678 

679 def wrap_results_dict_like( 

680 self, 

681 selected_obj: Series | DataFrame, 

682 result_index: list[Hashable], 

683 result_data: list, 

684 ): 

685 from pandas import Index 

686 from pandas.core.groupby.groupby import BaseGroupBy 

687 

688 obj = self.obj 

689 

690 if not result_data: 

691 # GH#39609 

692 if isinstance(obj, ABCSeries): 

693 return obj._constructor(index=Index([]), name=obj.name) 

694 elif isinstance(obj, ABCDataFrame): 

695 return obj._constructor(index=Index([]), columns=obj.columns[:0]) 

696 elif isinstance(obj, BaseGroupBy): 

697 index = _groupby_result_index(obj) 

698 if selected_obj.ndim == 2: 

699 frame = cast("DataFrame", selected_obj) 

700 return frame._constructor(index=index, columns=frame.columns[:0]) 

701 return selected_obj._constructor_expanddim( 

702 index=index, columns=Index([]) 

703 ) 

704 

705 # Avoid making two isinstance calls in all and any below 

706 is_ndframe = [isinstance(r, ABCNDFrame) for r in result_data] 

707 

708 if all(is_ndframe): 

709 results = [result for result in result_data if not result.empty] 

710 keys_to_use: Iterable[Hashable] 

711 keys_to_use = [ 

712 k for k, v in zip(result_index, result_data, strict=True) if not v.empty 

713 ] 

714 # Have to check, if at least one DataFrame is not empty. 

715 if keys_to_use == []: 

716 keys_to_use = result_index 

717 results = result_data 

718 

719 if selected_obj.ndim == 2: 

720 # keys are columns, so we can preserve names 

721 ktu = Index(keys_to_use) 

722 ktu._set_names(selected_obj.columns.names) 

723 keys_to_use = ktu 

724 

725 axis: AxisInt = 0 if isinstance(obj, ABCSeries) else 1 

726 result = concat( 

727 results, 

728 axis=axis, 

729 keys=keys_to_use, 

730 sort=False, 

731 ) 

732 elif any(is_ndframe): 

733 # There is a mix of NDFrames and scalars 

734 raise ValueError( 

735 "cannot perform both aggregation " 

736 "and transformation operations " 

737 "simultaneously" 

738 ) 

739 else: 

740 from pandas import Series 

741 

742 # we have a list of scalars 

743 # GH 36212 use name only if obj is a series 

744 if obj.ndim == 1: 

745 obj = cast("Series", obj) 

746 name = obj.name 

747 else: 

748 name = None 

749 

750 result = Series(result_data, index=result_index, name=name) 

751 

752 return result 

753 

754 def apply_str(self) -> DataFrame | Series: 

755 """ 

756 Compute apply in case of a string. 

757 

758 Returns 

759 ------- 

760 result: Series or DataFrame 

761 """ 

762 # Caller is responsible for checking isinstance(self.f, str) 

763 func = cast("str", self.func) 

764 

765 obj = self.obj 

766 

767 from pandas.core.groupby.generic import ( 

768 DataFrameGroupBy, 

769 SeriesGroupBy, 

770 ) 

771 

772 # Support for `frame.transform('method')` 

773 # Some methods (shift, etc.) require the axis argument, others 

774 # don't, so inspect and insert if necessary. 

775 method = getattr(obj, func, None) 

776 if callable(method): 

777 sig = inspect.getfullargspec(method) 

778 arg_names = (*sig.args, *sig.kwonlyargs) 

779 if self.axis != 0 and ( 

780 "axis" not in arg_names or func in ("corrwith", "skew") 

781 ): 

782 raise ValueError(f"Operation {func} does not support axis=1") 

783 if "axis" in arg_names and not isinstance( 

784 obj, (SeriesGroupBy, DataFrameGroupBy) 

785 ): 

786 self.kwargs["axis"] = self.axis 

787 return self._apply_str(obj, func, *self.args, **self.kwargs) 

788 

789 def apply_list_or_dict_like(self) -> DataFrame | Series: 

790 """ 

791 Compute apply in case of a list-like or dict-like. 

792 

793 Returns 

794 ------- 

795 result: Series, DataFrame, or None 

796 Result when self.func is a list-like or dict-like, None otherwise. 

797 """ 

798 

799 if self.engine == "numba": 

800 raise NotImplementedError( 

801 "The 'numba' engine doesn't support list-like/" 

802 "dict likes of callables yet." 

803 ) 

804 

805 if self.axis == 1 and isinstance(self.obj, ABCDataFrame): 

806 return self.obj.T.apply(self.func, 0, args=self.args, **self.kwargs).T 

807 

808 func = self.func 

809 kwargs = self.kwargs 

810 

811 if is_dict_like(func): 

812 result = self.agg_or_apply_dict_like(op_name="apply") 

813 else: 

814 result = self.agg_or_apply_list_like(op_name="apply") 

815 

816 result = reconstruct_and_relabel_result(result, func, **kwargs) 

817 

818 return result 

819 

820 def normalize_dictlike_arg( 

821 self, how: str, obj: DataFrame | Series, func: AggFuncTypeDict 

822 ) -> AggFuncTypeDict: 

823 """ 

824 Handler for dict-like argument. 

825 

826 Ensures that necessary columns exist if obj is a DataFrame, and 

827 that a nested renamer is not passed. Also normalizes to all lists 

828 when values consists of a mix of list and non-lists. 

829 """ 

830 assert how in ("apply", "agg", "transform") 

831 

832 # Can't use func.values(); wouldn't work for a Series 

833 if ( 

834 how == "agg" 

835 and isinstance(obj, ABCSeries) 

836 and any(is_list_like(v) for _, v in func.items()) 

837 ) or (any(is_dict_like(v) for _, v in func.items())): 

838 # GH 15931 - deprecation of renaming keys 

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

840 

841 if obj.ndim != 1: 

842 # Check for missing columns on a frame 

843 from pandas import Index 

844 

845 cols = Index(list(func.keys())).difference(obj.columns, sort=True) 

846 if len(cols) > 0: 

847 # GH 58474 

848 raise KeyError(f"Label(s) {list(cols)} do not exist") 

849 

850 aggregator_types = (list, tuple, dict) 

851 

852 # if we have a dict of any non-scalars 

853 # eg. {'A' : ['mean']}, normalize all to 

854 # be list-likes 

855 # Cannot use func.values() because arg may be a Series 

856 if any(isinstance(x, aggregator_types) for _, x in func.items()): 

857 new_func: AggFuncTypeDict = {} 

858 for k, v in func.items(): 

859 if not isinstance(v, aggregator_types): 

860 new_func[k] = [v] 

861 else: 

862 new_func[k] = v 

863 func = new_func 

864 return func 

865 

866 def _apply_str(self, obj, func: str, *args, **kwargs): 

867 """ 

868 if arg is a string, then try to operate on it: 

869 - try to find a function (or attribute) on obj 

870 - try to find a numpy function 

871 - raise 

872 """ 

873 assert isinstance(func, str) 

874 

875 if hasattr(obj, func): 

876 f = getattr(obj, func) 

877 if callable(f): 

878 return f(*args, **kwargs) 

879 

880 # people may aggregate on a non-callable attribute 

881 # but don't let them think they can pass args to it 

882 assert len(args) == 0 

883 assert not any(kwarg == "axis" for kwarg in kwargs) 

884 return f 

885 elif hasattr(np, func) and hasattr(obj, "__array__"): 

886 # in particular exclude Window 

887 f = getattr(np, func) 

888 return f(obj, *args, **kwargs) 

889 else: 

890 msg = f"'{func}' is not a valid function for '{type(obj).__name__}' object" 

891 raise AttributeError(msg) 

892 

893 

894class NDFrameApply(Apply): 

895 """ 

896 Methods shared by FrameApply and SeriesApply but 

897 not GroupByApply or ResamplerWindowApply 

898 """ 

899 

900 obj: DataFrame | Series 

901 

902 @property 

903 def index(self) -> Index: 

904 return self.obj.index 

905 

906 @property 

907 def agg_axis(self) -> Index: 

908 return self.obj._get_agg_axis(self.axis) 

909 

910 def agg_or_apply_list_like( 

911 self, op_name: Literal["agg", "apply"] 

912 ) -> DataFrame | Series: 

913 obj = self.obj 

914 kwargs = self.kwargs 

915 

916 if op_name == "apply": 

917 if isinstance(self, FrameApply): 

918 by_row = self.by_row 

919 

920 elif isinstance(self, SeriesApply): 

921 by_row = "_compat" if self.by_row else False 

922 else: 

923 by_row = False 

924 kwargs = {**kwargs, "by_row": by_row} 

925 

926 if getattr(obj, "axis", 0) == 1: 

927 raise NotImplementedError("axis other than 0 is not supported") 

928 

929 if op_name == "agg" and isinstance(self, FrameApply): 

930 result = self._agg_list_like_frame_reductions() 

931 if result is not None: 

932 return result 

933 

934 keys, results = self.compute_list_like(op_name, obj, kwargs) 

935 result = self.wrap_results_list_like(keys, results) 

936 return result 

937 

938 def agg_or_apply_dict_like( 

939 self, op_name: Literal["agg", "apply"] 

940 ) -> DataFrame | Series: 

941 assert op_name in ["agg", "apply"] 

942 obj = self.obj 

943 

944 kwargs = {} 

945 if op_name == "apply": 

946 by_row = "_compat" if self.by_row else False 

947 kwargs.update({"by_row": by_row}) 

948 

949 if getattr(obj, "axis", 0) == 1: 

950 raise NotImplementedError("axis other than 0 is not supported") 

951 

952 selection = None 

953 result_index, result_data = self.compute_dict_like( 

954 op_name, obj, selection, kwargs 

955 ) 

956 result = self.wrap_results_dict_like(obj, result_index, result_data) 

957 return result 

958 

959 

960class FrameApply(NDFrameApply): 

961 obj: DataFrame 

962 

963 def __init__( 

964 self, 

965 obj: AggObjType, 

966 func: AggFuncType, 

967 raw: bool, 

968 result_type: str | None, 

969 *, 

970 by_row: Literal[False, "compat"] = False, 

971 engine: str = "python", 

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

973 args, 

974 kwargs, 

975 ) -> None: 

976 if by_row is not False and by_row != "compat": 

977 raise ValueError(f"by_row={by_row} not allowed") 

978 super().__init__( 

979 obj, 

980 func, 

981 raw, 

982 result_type, 

983 by_row=by_row, 

984 engine=engine, 

985 engine_kwargs=engine_kwargs, 

986 args=args, 

987 kwargs=kwargs, 

988 ) 

989 

990 # --------------------------------------------------------------- 

991 # Abstract Methods 

992 

993 @property 

994 @abc.abstractmethod 

995 def result_index(self) -> Index: 

996 pass 

997 

998 @property 

999 @abc.abstractmethod 

1000 def result_columns(self) -> Index: 

1001 pass 

1002 

1003 @property 

1004 @abc.abstractmethod 

1005 def series_generator(self) -> Generator[Series]: 

1006 pass 

1007 

1008 @staticmethod 

1009 @functools.cache 

1010 @abc.abstractmethod 

1011 def generate_numba_apply_func( 

1012 func, nogil: bool = True, parallel: bool = False 

1013 ) -> Callable[[npt.NDArray, Index, Index], dict[int, Any]]: 

1014 pass 

1015 

1016 @abc.abstractmethod 

1017 def apply_with_numba(self): 

1018 pass 

1019 

1020 def validate_values_for_numba(self) -> None: 

1021 # Validate column dtypes all OK 

1022 for colname, dtype in self.obj.dtypes.items(): 

1023 if not is_numeric_dtype(dtype): 

1024 raise ValueError( 

1025 f"Column {colname} must have a numeric dtype. " 

1026 f"Found '{dtype}' instead" 

1027 ) 

1028 if is_extension_array_dtype(dtype): 

1029 raise ValueError( 

1030 f"Column {colname} is backed by an extension array, " 

1031 f"which is not supported by the numba engine." 

1032 ) 

1033 

1034 @abc.abstractmethod 

1035 def wrap_results_for_axis( 

1036 self, results: ResType, res_index: Index 

1037 ) -> DataFrame | Series: 

1038 pass 

1039 

1040 # --------------------------------------------------------------- 

1041 

1042 @property 

1043 def res_columns(self) -> Index: 

1044 return self.result_columns 

1045 

1046 @property 

1047 def columns(self) -> Index: 

1048 return self.obj.columns 

1049 

1050 @cache_readonly 

1051 def values(self): 

1052 return self.obj.values 

1053 

1054 def apply(self) -> DataFrame | Series: 

1055 """compute the results""" 

1056 

1057 # dispatch to handle list-like or dict-like 

1058 if is_list_like(self.func): 

1059 if self.engine == "numba": 

1060 raise NotImplementedError( 

1061 "the 'numba' engine doesn't support lists of callables yet" 

1062 ) 

1063 return self.apply_list_or_dict_like() 

1064 

1065 # all empty 

1066 if len(self.columns) == 0 and len(self.index) == 0: 

1067 return self.apply_empty_result() 

1068 

1069 # string dispatch 

1070 if isinstance(self.func, str): 

1071 if self.engine == "numba": 

1072 raise NotImplementedError( 

1073 "the 'numba' engine doesn't support using " 

1074 "a string as the callable function" 

1075 ) 

1076 return self.apply_str() 

1077 

1078 # ufunc 

1079 elif isinstance(self.func, np.ufunc): 

1080 if self.engine == "numba": 

1081 raise NotImplementedError( 

1082 "the 'numba' engine doesn't support " 

1083 "using a numpy ufunc as the callable function" 

1084 ) 

1085 with np.errstate(all="ignore"): 

1086 results = self.obj._mgr.apply("apply", func=self.func) 

1087 # _constructor will retain self.index and self.columns 

1088 return self.obj._constructor_from_mgr(results, axes=results.axes) 

1089 

1090 # broadcasting 

1091 if self.result_type == "broadcast": 

1092 if self.engine == "numba": 

1093 raise NotImplementedError( 

1094 "the 'numba' engine doesn't support result_type='broadcast'" 

1095 ) 

1096 return self.apply_broadcast(self.obj) 

1097 

1098 # one axis empty 

1099 elif not all(self.obj.shape): 

1100 return self.apply_empty_result() 

1101 

1102 # raw 

1103 elif self.raw: 

1104 return self.apply_raw(engine=self.engine, engine_kwargs=self.engine_kwargs) 

1105 

1106 return self.apply_standard() 

1107 

1108 def agg(self): 

1109 obj = self.obj 

1110 axis = self.axis 

1111 

1112 # TODO: Avoid having to change state 

1113 self.obj = self.obj if self.axis == 0 else self.obj.T 

1114 self.axis = 0 

1115 

1116 result = None 

1117 try: 

1118 result = super().agg() 

1119 finally: 

1120 self.obj = obj 

1121 self.axis = axis 

1122 

1123 if axis == 1: 

1124 result = result.T if result is not None else result 

1125 

1126 if result is None: 

1127 result = self.obj.apply(self.func, axis, args=self.args, **self.kwargs) 

1128 

1129 return result 

1130 

1131 def _agg_list_like_frame_reductions(self) -> DataFrame | None: 

1132 """ 

1133 Aggregate a list of named functions using DataFrame-level reductions. 

1134 

1135 Instead of extracting each column as a Series and calling 

1136 Series.agg per column, call DataFrame-level reductions directly. 

1137 Operates per dtype group to preserve per-column dtypes. 

1138 

1139 Returns None if the fast path cannot be used (e.g. non-string 

1140 functions or functions that aren't known column-wise reductions). 

1141 """ 

1142 func = cast("list[AggFuncTypeBase]", self.func) 

1143 

1144 if not all(isinstance(f, str) for f in func): 

1145 return None 

1146 

1147 obj = self.obj 

1148 func_names = cast("list[str]", func) 

1149 

1150 # Cannot reindex with duplicate column names 

1151 if not obj.columns.is_unique: 

1152 return None 

1153 

1154 for func_name in func_names: 

1155 if func_name not in _frame_reduction_names: 

1156 # GH#65031 anything that isn't a known column-wise reduction 

1157 # (e.g. value_counts, duplicated) must use the per-column 

1158 # fallback; a Series-returning method could otherwise slip 

1159 # through the shape checks below and give wrong results. 

1160 return None 

1161 

1162 if self.kwargs.get("numeric_only"): 

1163 obj = obj._get_numeric_data() 

1164 elif self.kwargs.get("bool_only"): 

1165 obj = obj._get_bool_data() 

1166 

1167 if obj.columns.empty or len(func_names) == 0: 

1168 return obj._constructor(index=func_names, columns=obj.columns) 

1169 

1170 # Compute reductions per dtype group to preserve per-column dtypes. 

1171 groups = obj.columns.groupby(obj.dtypes) 

1172 pieces: list[DataFrame] = [] 

1173 for dtype in groups: 

1174 cols = groups[dtype] 

1175 sub = obj[cols] 

1176 rows: list[Series] = [] 

1177 for func_name in func_names: 

1178 try: 

1179 row = getattr(sub, func_name)(*self.args, **self.kwargs) 

1180 except TypeError: 

1181 return None 

1182 if not isinstance(row, ABCSeries): 

1183 # Not a reduction result for these args (e.g. quantile 

1184 # with a list q returns a DataFrame); fall back 

1185 return None 

1186 if not row.index.equals(sub.columns): 

1187 # Backstop: a genuine column-wise reduction is indexed 

1188 # by the columns; anything else would silently misalign 

1189 # in the concat below. 

1190 return None 

1191 rows.append(row) 

1192 

1193 # to_frame().T avoids the slow DataFrame(list-of-Series) path 

1194 frames = [ 

1195 row.to_frame(name).T for name, row in zip(func_names, rows, strict=True) 

1196 ] 

1197 if _stack_as_object([row.dtype for row in rows]): 

1198 # GH#65031 see test_agg_list_like_unsigned_and_signed_is_object 

1199 # and test_agg_list_like_bool_with_numeric_is_object 

1200 frames = [frame.astype(object) for frame in frames] 

1201 pieces.append(concat(frames)) 

1202 

1203 result = concat(pieces, axis=1) 

1204 result = result.reindex(columns=obj.columns) 

1205 return result 

1206 

1207 def apply_empty_result(self): 

1208 """ 

1209 we have an empty result; at least 1 axis is 0 

1210 

1211 we will try to apply the function to an empty 

1212 series in order to see if this is a reduction function 

1213 """ 

1214 assert callable(self.func) 

1215 

1216 # we are not asked to reduce or infer reduction 

1217 # so just return a copy of the existing object 

1218 if self.result_type not in ["reduce", None]: 

1219 return self.obj.copy() 

1220 

1221 # we may need to infer 

1222 should_reduce = self.result_type == "reduce" 

1223 

1224 from pandas import Series 

1225 

1226 if not should_reduce: 

1227 try: 

1228 if self.axis == 0: 

1229 r = self.func( 

1230 Series([], dtype=np.float64), *self.args, **self.kwargs 

1231 ) 

1232 else: 

1233 r = self.func( 

1234 Series(index=self.columns, dtype=np.float64), 

1235 *self.args, 

1236 **self.kwargs, 

1237 ) 

1238 except Exception: 

1239 pass 

1240 else: 

1241 should_reduce = not isinstance(r, Series) 

1242 

1243 if should_reduce: 

1244 if len(self.agg_axis): 

1245 r = self.func(Series([], dtype=np.float64), *self.args, **self.kwargs) 

1246 else: 

1247 r = np.nan 

1248 

1249 return self.obj._constructor_sliced(r, index=self.agg_axis) 

1250 else: 

1251 return self.obj.copy() 

1252 

1253 def apply_raw(self, engine="python", engine_kwargs=None): 

1254 """apply to the values as a numpy array""" 

1255 

1256 def wrap_function(func): 

1257 """ 

1258 Wrap user supplied function to work around numpy issue. 

1259 

1260 see https://github.com/numpy/numpy/issues/8352 

1261 """ 

1262 

1263 def wrapper(*args, **kwargs): 

1264 result = func(*args, **kwargs) 

1265 if isinstance(result, str): 

1266 result = np.array(result, dtype=object) 

1267 return result 

1268 

1269 return wrapper 

1270 

1271 if engine == "numba": 

1272 args, kwargs = prepare_function_arguments( 

1273 self.func, # type: ignore[arg-type] 

1274 self.args, 

1275 self.kwargs, 

1276 num_required_args=1, 

1277 ) 

1278 # error: Argument 1 to "__call__" of "_lru_cache_wrapper" has 

1279 # incompatible type "Callable[..., Any] | str | list[Callable 

1280 # [..., Any] | str] | dict[Hashable,Callable[..., Any] | str | 

1281 # list[Callable[..., Any] | str]]"; expected "Hashable" 

1282 nb_looper = generate_apply_looper( 

1283 self.func, # type: ignore[arg-type] 

1284 **get_jit_arguments(engine_kwargs), 

1285 ) 

1286 result = nb_looper(self.values, self.axis, *args) 

1287 # If we made the result 2-D, squeeze it back to 1-D 

1288 result = np.squeeze(result) 

1289 else: 

1290 result = np.apply_along_axis( 

1291 wrap_function(self.func), 

1292 self.axis, 

1293 self.values, 

1294 *self.args, 

1295 **self.kwargs, 

1296 ) 

1297 

1298 # TODO: mixed type case 

1299 if result.ndim == 2: 

1300 return self.obj._constructor(result, index=self.index, columns=self.columns) 

1301 else: 

1302 return self.obj._constructor_sliced(result, index=self.agg_axis) 

1303 

1304 def apply_broadcast(self, target: DataFrame) -> DataFrame: 

1305 assert callable(self.func) 

1306 

1307 result_values = np.empty_like(target.values) 

1308 

1309 # axis which we want to compare compliance 

1310 result_compare = target.shape[0] 

1311 

1312 for i, col in enumerate(target.columns): 

1313 res = self.func(target[col], *self.args, **self.kwargs) 

1314 ares = np.asarray(res).ndim 

1315 

1316 # must be a scalar or 1d 

1317 if ares > 1: 

1318 raise ValueError("too many dims to broadcast") 

1319 if ares == 1: 

1320 # must match return dim 

1321 if result_compare != len(res): 

1322 raise ValueError("cannot broadcast result") 

1323 

1324 result_values[:, i] = res 

1325 

1326 # we *always* preserve the original index / columns 

1327 result = self.obj._constructor( 

1328 result_values, index=target.index, columns=target.columns 

1329 ) 

1330 return result 

1331 

1332 def apply_standard(self): 

1333 if self.engine == "python": 

1334 results, res_index = self.apply_series_generator() 

1335 else: 

1336 results, res_index = self.apply_series_numba() 

1337 

1338 # wrap results 

1339 return self.wrap_results(results, res_index) 

1340 

1341 def apply_series_generator(self) -> tuple[ResType, Index]: 

1342 assert callable(self.func) 

1343 

1344 series_gen = self.series_generator 

1345 res_index = self.result_index 

1346 

1347 results = {} 

1348 

1349 for i, v in enumerate(series_gen): 

1350 results[i] = self.func(v, *self.args, **self.kwargs) 

1351 if isinstance(results[i], ABCSeries): 

1352 # If we have a view on v, we need to make a copy because 

1353 # series_generator will swap out the underlying data 

1354 results[i] = results[i].copy(deep=False) 

1355 

1356 return results, res_index 

1357 

1358 def apply_series_numba(self): 

1359 if self.engine_kwargs.get("parallel", False): 

1360 raise NotImplementedError( 

1361 "Parallel apply is not supported when raw=False and engine='numba'" 

1362 ) 

1363 if not self.obj.index.is_unique or not self.columns.is_unique: 

1364 raise NotImplementedError( 

1365 "The index/columns must be unique when raw=False and engine='numba'" 

1366 ) 

1367 self.validate_values_for_numba() 

1368 results = self.apply_with_numba() 

1369 return results, self.result_index 

1370 

1371 def wrap_results(self, results: ResType, res_index: Index) -> DataFrame | Series: 

1372 from pandas import Series 

1373 

1374 # see if we can infer the results 

1375 if len(results) > 0 and 0 in results and is_sequence(results[0]): 

1376 return self.wrap_results_for_axis(results, res_index) 

1377 

1378 # dict of scalars 

1379 

1380 # the default dtype of an empty Series is `object`, but this 

1381 # code can be hit by df.mean() where the result should have dtype 

1382 # float64 even if it's an empty Series. 

1383 constructor_sliced = self.obj._constructor_sliced 

1384 if len(results) == 0 and constructor_sliced is Series: 

1385 result = constructor_sliced(results, dtype=np.float64) 

1386 else: 

1387 result = constructor_sliced(results) 

1388 result.index = res_index 

1389 

1390 return result 

1391 

1392 def apply_str(self) -> DataFrame | Series: 

1393 # Caller is responsible for checking isinstance(self.func, str) 

1394 # TODO: GH#39993 - Avoid special-casing by replacing with lambda 

1395 if self.func == "size": 

1396 # Special-cased because DataFrame.size returns a single scalar 

1397 obj = self.obj 

1398 value = obj.shape[self.axis] 

1399 return obj._constructor_sliced(value, index=self.agg_axis) 

1400 return super().apply_str() 

1401 

1402 

1403class FrameRowApply(FrameApply): 

1404 axis: AxisInt = 0 

1405 

1406 @property 

1407 def series_generator(self) -> Generator[Series]: 

1408 return (self.obj._ixs(i, axis=1) for i in range(len(self.columns))) 

1409 

1410 @staticmethod 

1411 @functools.cache 

1412 def generate_numba_apply_func( 

1413 func, nogil: bool = True, parallel: bool = False 

1414 ) -> Callable[[npt.NDArray, Index, Index], dict[int, Any]]: 

1415 numba = import_optional_dependency("numba") 

1416 from pandas import Series 

1417 

1418 # Import helper from extensions to cast string object -> np strings 

1419 # Note: This also has the side effect of loading our numba extensions 

1420 from pandas.core._numba.extensions import maybe_cast_str 

1421 

1422 jitted_udf = numba.extending.register_jitable(func) 

1423 

1424 # Currently the parallel argument doesn't get passed through here 

1425 # (it's disabled) since the dicts in numba aren't thread-safe. 

1426 @numba.jit(nogil=nogil, parallel=parallel) 

1427 def numba_func(values, col_names, df_index, *args): 

1428 results = {} 

1429 for j in range(values.shape[1]): 

1430 # Create the series 

1431 ser = Series( 

1432 values[:, j], index=df_index, name=maybe_cast_str(col_names[j]) 

1433 ) 

1434 results[j] = jitted_udf(ser, *args) 

1435 return results 

1436 

1437 return numba_func 

1438 

1439 def apply_with_numba(self) -> dict[int, Any]: 

1440 func = cast("Callable", self.func) 

1441 args, kwargs = prepare_function_arguments( 

1442 func, self.args, self.kwargs, num_required_args=1 

1443 ) 

1444 nb_func = self.generate_numba_apply_func( 

1445 func, **get_jit_arguments(self.engine_kwargs) 

1446 ) 

1447 from pandas.core._numba.extensions import set_numba_data 

1448 

1449 index = self.obj.index 

1450 columns = self.obj.columns 

1451 

1452 # Convert from numba dict to regular dict 

1453 # Our isinstance checks in the df constructor don't pass for numbas typed dict 

1454 with set_numba_data(index) as index, set_numba_data(columns) as columns: 

1455 res = dict(nb_func(self.values, columns, index, *args)) 

1456 return res 

1457 

1458 @property 

1459 def result_index(self) -> Index: 

1460 return self.columns 

1461 

1462 @property 

1463 def result_columns(self) -> Index: 

1464 return self.index 

1465 

1466 def wrap_results_for_axis( 

1467 self, results: ResType, res_index: Index 

1468 ) -> DataFrame | Series: 

1469 """return the results for the rows""" 

1470 

1471 if self.result_type == "reduce": 

1472 # e.g. test_apply_dict GH#8735 

1473 res = self.obj._constructor_sliced(results) 

1474 res.index = res_index 

1475 return res 

1476 

1477 elif self.result_type is None and all( 

1478 isinstance(x, dict) for x in results.values() 

1479 ): 

1480 # Our operation was a to_dict op e.g. 

1481 # test_apply_dict GH#8735, test_apply_reduce_to_dict GH#25196 #37544 

1482 res = self.obj._constructor_sliced(results) 

1483 res.index = res_index 

1484 return res 

1485 

1486 try: 

1487 result = self.obj._constructor(data=results) 

1488 except ValueError as err: 

1489 if "All arrays must be of the same length" in str(err): 

1490 # e.g. result = [[2, 3], [1.5], ['foo', 'bar']] 

1491 # see test_agg_listlike_result GH#29587 

1492 res = self.obj._constructor_sliced(results) 

1493 res.index = res_index 

1494 return res 

1495 else: 

1496 raise 

1497 

1498 if not isinstance(results[0], ABCSeries): 

1499 if len(result.index) == len(self.res_columns): 

1500 result.index = self.res_columns 

1501 

1502 if len(result.columns) == len(res_index): 

1503 result.columns = res_index 

1504 

1505 return result 

1506 

1507 

1508class FrameColumnApply(FrameApply): 

1509 axis: AxisInt = 1 

1510 

1511 def apply_broadcast(self, target: DataFrame) -> DataFrame: 

1512 result = super().apply_broadcast(target.T) 

1513 return result.T 

1514 

1515 @property 

1516 def series_generator(self) -> Generator[Series]: 

1517 assert len(self.obj) > 0 

1518 

1519 # We create one Series object, and will swap out the data inside 

1520 # of it. Kids: don't do this at home. 

1521 ser = self.obj._ixs(0, axis=0) 

1522 mgr = ser._mgr 

1523 

1524 is_view = mgr.blocks[0].refs.has_reference() 

1525 

1526 if isinstance(ser.dtype, ExtensionDtype): 

1527 # GH#61747 - Pre-extract column arrays and reuse a template 

1528 # Series to avoid the per-row block traversal and 

1529 # Series/manager construction overhead in fast_xs. 

1530 obj = self.obj 

1531 col_arrays = [obj._get_column_array(i) for i in range(len(obj.columns))] 

1532 dtype = ser.dtype 

1533 cls = dtype.construct_array_type() 

1534 mgr = ser._mgr 

1535 

1536 build_row = self._make_ea_row_builder(col_arrays, dtype, cls) 

1537 

1538 blk = mgr.blocks[0] 

1539 for row_idx in range(len(obj)): 

1540 ser._mgr = mgr 

1541 mgr.set_values(build_row(row_idx)) 

1542 # EABackedBlock.array_values is @cache_readonly; 

1543 # clear it so Series.array sees the new values. 

1544 blk._cache.pop("array_values", None) 

1545 object.__setattr__(ser, "_name", obj.index[row_idx]) 

1546 blk.refs = BlockValuesRefs(blk) 

1547 yield ser 

1548 

1549 else: 

1550 values = self.values 

1551 values = ensure_wrapped_if_datetimelike(values) 

1552 

1553 for arr, name in zip(values, self.index, strict=True): 

1554 # GH#35462 re-pin mgr in case setitem changed it 

1555 ser._mgr = mgr 

1556 mgr.set_values(arr) 

1557 object.__setattr__(ser, "_name", name) 

1558 if not is_view: 

1559 # In apply_series_generator we store a shallow copy of the 

1560 # result, which potentially increases the ref count of this reused 

1561 # `ser` object (depending on the result of the applied function) 

1562 # -> if that happened and `ser` is already a copy, then we reset 

1563 # the refs here to avoid triggering an unnecessary CoW inside the 

1564 # applied function (https://github.com/pandas-dev/pandas/pull/56212) 

1565 mgr.blocks[0].refs = BlockValuesRefs(mgr.blocks[0]) 

1566 yield ser 

1567 

1568 @staticmethod 

1569 def _make_ea_row_builder( 

1570 col_arrays: list, dtype: ExtensionDtype, cls: type[ExtensionArray] 

1571 ) -> Callable[[int], ExtensionArray]: 

1572 """Build a callable that constructs an EA row for a given row index. 

1573 

1574 Special-cases BaseMaskedArray and ArrowExtensionArray to avoid the 

1575 overhead of _from_sequence per row. The fast paths require all columns 

1576 to share ``dtype``; mixed-dtype frames fall through to the generic 

1577 ``_from_sequence`` path (GH#65097). 

1578 """ 

1579 homogeneous = all(col_arr.dtype == dtype for col_arr in col_arrays) 

1580 

1581 if homogeneous and isinstance(dtype, BaseMaskedDtype): 

1582 col_data = [col_arr._data for col_arr in col_arrays] 

1583 col_masks = [col_arr._mask for col_arr in col_arrays] 

1584 np_dtype = dtype.numpy_dtype 

1585 masked_cls = cast("type[BaseMaskedArray]", cls) 

1586 

1587 def build_row(row_idx: int) -> ExtensionArray: 

1588 data = np.array([cd[row_idx] for cd in col_data], dtype=np_dtype) 

1589 mask = np.array([cm[row_idx] for cm in col_masks], dtype=np.bool_) 

1590 return masked_cls._simple_new(data, mask) 

1591 

1592 elif homogeneous and isinstance(dtype, ArrowDtype): 

1593 import pyarrow as pa 

1594 

1595 pa_arrays = [col_arr._pa_array for col_arr in col_arrays] 

1596 pa_type = dtype.pyarrow_dtype 

1597 arrow_cls = cast("type[ArrowExtensionArray]", cls) 

1598 

1599 def build_row(row_idx: int) -> ExtensionArray: 

1600 pa_scalars = [pa_col[row_idx] for pa_col in pa_arrays] 

1601 return arrow_cls(pa.array(pa_scalars, type=pa_type)) 

1602 

1603 else: 

1604 

1605 def build_row(row_idx: int) -> ExtensionArray: 

1606 row_data = np.array( 

1607 [col_arr[row_idx] for col_arr in col_arrays], dtype=object 

1608 ) 

1609 return cls._from_sequence(row_data, dtype=dtype) 

1610 

1611 return build_row 

1612 

1613 @staticmethod 

1614 @functools.cache 

1615 def generate_numba_apply_func( 

1616 func, nogil: bool = True, parallel: bool = False 

1617 ) -> Callable[[npt.NDArray, Index, Index], dict[int, Any]]: 

1618 numba = import_optional_dependency("numba") 

1619 from pandas import Series 

1620 from pandas.core._numba.extensions import maybe_cast_str 

1621 

1622 jitted_udf = numba.extending.register_jitable(func) 

1623 

1624 @numba.jit(nogil=nogil, parallel=parallel) 

1625 def numba_func(values, col_names_index, index, *args): 

1626 results = {} 

1627 # Currently the parallel argument doesn't get passed through here 

1628 # (it's disabled) since the dicts in numba aren't thread-safe. 

1629 for i in range(values.shape[0]): 

1630 # Create the series 

1631 # TODO: values corrupted without the copy 

1632 ser = Series( 

1633 values[i].copy(), 

1634 index=col_names_index, 

1635 name=maybe_cast_str(index[i]), 

1636 ) 

1637 results[i] = jitted_udf(ser, *args) 

1638 

1639 return results 

1640 

1641 return numba_func 

1642 

1643 def apply_with_numba(self) -> dict[int, Any]: 

1644 func = cast("Callable", self.func) 

1645 args, kwargs = prepare_function_arguments( 

1646 func, self.args, self.kwargs, num_required_args=1 

1647 ) 

1648 nb_func = self.generate_numba_apply_func( 

1649 func, **get_jit_arguments(self.engine_kwargs) 

1650 ) 

1651 

1652 from pandas.core._numba.extensions import set_numba_data 

1653 

1654 # Convert from numba dict to regular dict 

1655 # Our isinstance checks in the df constructor don't pass for numbas typed dict 

1656 with ( 

1657 set_numba_data(self.obj.index) as index, 

1658 set_numba_data(self.columns) as columns, 

1659 ): 

1660 res = dict(nb_func(self.values, columns, index, *args)) 

1661 

1662 return res 

1663 

1664 @property 

1665 def result_index(self) -> Index: 

1666 return self.index 

1667 

1668 @property 

1669 def result_columns(self) -> Index: 

1670 return self.columns 

1671 

1672 def wrap_results_for_axis( 

1673 self, results: ResType, res_index: Index 

1674 ) -> DataFrame | Series: 

1675 """return the results for the columns""" 

1676 result: DataFrame | Series 

1677 

1678 # we have requested to expand 

1679 if self.result_type == "expand": 

1680 result = self.infer_to_same_shape(results, res_index) 

1681 

1682 # we have a non-series and don't want inference 

1683 elif not isinstance(results[0], ABCSeries): 

1684 result = self.obj._constructor_sliced(results) 

1685 result.index = res_index 

1686 

1687 # we may want to infer results 

1688 else: 

1689 result = self.infer_to_same_shape(results, res_index) 

1690 

1691 return result 

1692 

1693 def infer_to_same_shape(self, results: ResType, res_index: Index) -> DataFrame: 

1694 """infer the results to the same shape as the input object""" 

1695 result = self.obj._constructor(data=results) 

1696 result = result.T 

1697 

1698 # set the index 

1699 result.index = res_index 

1700 

1701 # infer dtypes 

1702 result = result.infer_objects() 

1703 

1704 return result 

1705 

1706 

1707class SeriesApply(NDFrameApply): 

1708 obj: Series 

1709 axis: AxisInt = 0 

1710 by_row: Literal[False, "compat", "_compat"] # only relevant for apply() 

1711 

1712 def __init__( 

1713 self, 

1714 obj: Series, 

1715 func: AggFuncType, 

1716 *, 

1717 by_row: Literal[False, "compat", "_compat"] = "compat", 

1718 args, 

1719 kwargs, 

1720 ) -> None: 

1721 super().__init__( 

1722 obj, 

1723 func, 

1724 raw=False, 

1725 result_type=None, 

1726 by_row=by_row, 

1727 args=args, 

1728 kwargs=kwargs, 

1729 ) 

1730 

1731 def apply(self) -> DataFrame | Series: 

1732 obj = self.obj 

1733 

1734 if len(obj) == 0: 

1735 return self.apply_empty_result() 

1736 

1737 # dispatch to handle list-like or dict-like 

1738 if is_list_like(self.func): 

1739 return self.apply_list_or_dict_like() 

1740 

1741 if isinstance(self.func, str): 

1742 # if we are a string, try to dispatch 

1743 return self.apply_str() 

1744 

1745 if self.by_row == "_compat": 

1746 return self.apply_compat() 

1747 

1748 # self.func is Callable 

1749 return self.apply_standard() 

1750 

1751 def agg(self): 

1752 result = super().agg() 

1753 if result is None: 

1754 obj = self.obj 

1755 func = self.func 

1756 # string, list-like, and dict-like are entirely handled in super 

1757 assert callable(func) 

1758 result = func(obj, *self.args, **self.kwargs) 

1759 return result 

1760 

1761 def apply_empty_result(self) -> Series: 

1762 obj = self.obj 

1763 return obj._constructor(dtype=obj.dtype, index=obj.index).__finalize__( 

1764 obj, method="apply" 

1765 ) 

1766 

1767 def apply_compat(self): 

1768 """compat apply method for funcs in listlikes and dictlikes. 

1769 

1770 Used for each callable when giving listlikes and dictlikes of callables to 

1771 apply. Needed for compatibility with Pandas < v2.1. 

1772 

1773 .. versionadded:: 2.1.0 

1774 """ 

1775 obj = self.obj 

1776 func = self.func 

1777 

1778 if callable(func): 

1779 f = com.get_cython_func(func) 

1780 if f and not self.args and not self.kwargs: 

1781 return obj.apply(func, by_row=False) 

1782 

1783 try: 

1784 result = obj.apply(func, by_row="compat") 

1785 except (ValueError, AttributeError, TypeError): 

1786 result = obj.apply(func, by_row=False) 

1787 return result 

1788 

1789 def apply_standard(self) -> DataFrame | Series: 

1790 # caller is responsible for ensuring that f is Callable 

1791 func = cast("Callable", self.func) 

1792 obj = self.obj 

1793 

1794 if isinstance(func, np.ufunc): 

1795 with np.errstate(all="ignore"): 

1796 return func(obj, *self.args, **self.kwargs) 

1797 elif not self.by_row: 

1798 return func(obj, *self.args, **self.kwargs) 

1799 

1800 if self.args or self.kwargs: 

1801 # _map_values does not support args/kwargs 

1802 def curried(x): 

1803 return func(x, *self.args, **self.kwargs) 

1804 

1805 else: 

1806 curried = func 

1807 mapped = obj._map_values(mapper=curried) 

1808 

1809 if len(mapped) and isinstance(mapped[0], ABCSeries): 

1810 # GH#43986 Need to do list(mapped) in order to get treated as nested 

1811 # See also GH#25959 regarding EA support 

1812 return obj._constructor_expanddim(list(mapped), index=obj.index) 

1813 else: 

1814 return obj._constructor(mapped, index=obj.index).__finalize__( 

1815 obj, method="apply" 

1816 ) 

1817 

1818 

1819class GroupByApply(Apply): 

1820 obj: GroupBy | Resampler | BaseWindow 

1821 

1822 def __init__( 

1823 self, 

1824 obj: GroupBy[NDFrameT], 

1825 func: AggFuncType, 

1826 *, 

1827 args, 

1828 kwargs, 

1829 ) -> None: 

1830 kwargs = kwargs.copy() 

1831 self.axis = obj.obj._get_axis_number(kwargs.get("axis", 0)) 

1832 super().__init__( 

1833 obj, 

1834 func, 

1835 raw=False, 

1836 result_type=None, 

1837 args=args, 

1838 kwargs=kwargs, 

1839 ) 

1840 

1841 def apply(self): 

1842 raise NotImplementedError 

1843 

1844 def transform(self): 

1845 raise NotImplementedError 

1846 

1847 def agg_or_apply_list_like( 

1848 self, op_name: Literal["agg", "apply"] 

1849 ) -> DataFrame | Series: 

1850 obj = self.obj 

1851 kwargs = self.kwargs 

1852 if op_name == "apply": 

1853 kwargs = {**kwargs, "by_row": False} 

1854 

1855 if getattr(obj, "axis", 0) == 1: 

1856 raise NotImplementedError("axis other than 0 is not supported") 

1857 

1858 if obj._selected_obj.ndim == 1: 

1859 # For SeriesGroupBy this matches _obj_with_exclusions 

1860 selected_obj = obj._selected_obj 

1861 else: 

1862 selected_obj = obj._obj_with_exclusions 

1863 

1864 # Only set as_index=True on groupby objects, not Window or Resample 

1865 # that inherit from this class. 

1866 with com.temp_setattr( 

1867 obj, "as_index", True, condition=hasattr(obj, "as_index") 

1868 ): 

1869 keys, results = self.compute_list_like(op_name, selected_obj, kwargs) 

1870 result = self.wrap_results_list_like(keys, results) 

1871 return result 

1872 

1873 def agg_or_apply_dict_like( 

1874 self, op_name: Literal["agg", "apply"] 

1875 ) -> DataFrame | Series: 

1876 from pandas.core.groupby.generic import ( 

1877 DataFrameGroupBy, 

1878 SeriesGroupBy, 

1879 ) 

1880 

1881 assert op_name in ["agg", "apply"] 

1882 

1883 obj = self.obj 

1884 kwargs: dict[str, Any] = {} 

1885 if op_name == "apply": 

1886 by_row = "_compat" if self.by_row else False 

1887 kwargs.update({"by_row": by_row}) 

1888 

1889 if getattr(obj, "axis", 0) == 1: 

1890 raise NotImplementedError("axis other than 0 is not supported") 

1891 

1892 selected_obj = obj._selected_obj 

1893 selection = obj._selection 

1894 

1895 is_groupby = isinstance(obj, (DataFrameGroupBy, SeriesGroupBy)) 

1896 

1897 # Numba Groupby engine/engine-kwargs passthrough 

1898 if is_groupby: 

1899 engine = self.kwargs.get("engine", None) 

1900 engine_kwargs = self.kwargs.get("engine_kwargs", None) 

1901 kwargs.update({"engine": engine, "engine_kwargs": engine_kwargs}) 

1902 

1903 with com.temp_setattr( 

1904 obj, "as_index", True, condition=hasattr(obj, "as_index") 

1905 ): 

1906 result_index, result_data = self.compute_dict_like( 

1907 op_name, selected_obj, selection, kwargs 

1908 ) 

1909 result = self.wrap_results_dict_like(selected_obj, result_index, result_data) 

1910 return result 

1911 

1912 

1913class ResamplerWindowApply(GroupByApply): 

1914 axis: AxisInt = 0 

1915 obj: Resampler | BaseWindow 

1916 

1917 def __init__( 

1918 self, 

1919 obj: Resampler | BaseWindow, 

1920 func: AggFuncType, 

1921 *, 

1922 args, 

1923 kwargs, 

1924 ) -> None: 

1925 super(GroupByApply, self).__init__( 

1926 obj, 

1927 func, 

1928 raw=False, 

1929 result_type=None, 

1930 args=args, 

1931 kwargs=kwargs, 

1932 ) 

1933 

1934 def apply(self): 

1935 raise NotImplementedError 

1936 

1937 def transform(self): 

1938 raise NotImplementedError 

1939 

1940 

1941def _groupby_result_index(obj: BaseGroupBy) -> Index: 

1942 """ 

1943 Index of a groupby aggregation result, used when there is nothing to aggregate. 

1944 """ 

1945 from pandas.core.resample import Resampler 

1946 

1947 if isinstance(obj, Resampler): 

1948 # a groupby-resample has no single grouper to take the index from 

1949 return obj.size().index 

1950 return obj._grouper.result_index 

1951 

1952 

1953def reconstruct_func( 

1954 func: AggFuncType | None, allow_skip_normalization: bool = False, /, **kwargs 

1955) -> tuple[bool, AggFuncType, tuple[str, ...] | None, npt.NDArray[np.intp] | None]: 

1956 """ 

1957 This is the internal function to reconstruct func given if there is relabeling 

1958 or not and also normalize the keyword to get new order of columns. 

1959 

1960 If named aggregation is applied, `func` will be None, and kwargs contains the 

1961 column and aggregation function information to be parsed; 

1962 If named aggregation is not applied, `func` is either string (e.g. 'min') or 

1963 Callable, or list of them (e.g. ['min', np.max]), or the dictionary of column name 

1964 and str/Callable/list of them (e.g. {'A': 'min'}, or {'A': [np.min, lambda x: x]}) 

1965 

1966 If relabeling is True, will return relabeling, reconstructed func, column 

1967 names, and the reconstructed order of columns. 

1968 If relabeling is False, the columns and order will be None. 

1969 

1970 Named aggregation is the one exception: when ``allow_skip_normalization`` is 

1971 True, every output name equals its source column name, and every aggfunc 

1972 reduces to a scalar, relabeling is reported as False (and columns/order as 

1973 None) even though named aggregation was used, because the caller can consume 

1974 the un-normalized func directly. 

1975 

1976 Parameters 

1977 ---------- 

1978 func: agg function (e.g. 'min' or Callable) or list of agg functions 

1979 (e.g. ['min', np.max]) or dictionary (e.g. {'A': ['min', np.max]}). 

1980 allow_skip_normalization: bool, default False 

1981 Whether the caller can handle the un-normalized ``{column: aggfunc}`` form 

1982 that named aggregation reduces to when every output name equals its source 

1983 column name and every aggfunc is a scalar reduction. Callers that rely on 

1984 ``columns``/``order`` being returned whenever named aggregation was used 

1985 must leave this False. 

1986 **kwargs: dict, kwargs used in is_multi_agg_with_relabel and 

1987 normalize_keyword_aggregation function for relabelling 

1988 

1989 Returns 

1990 ------- 

1991 relabelling: bool, if there is relabelling or not 

1992 func: normalized and mangled func 

1993 columns: tuple of column names 

1994 order: array of columns indices 

1995 

1996 Examples 

1997 -------- 

1998 >>> reconstruct_func(None, **{"foo": ("col", "min")}) 

1999 (True, defaultdict(<class 'list'>, {'col': ['min']}), ('foo',), array([0])) 

2000 

2001 >>> reconstruct_func("min") 

2002 (False, 'min', None, None) 

2003 """ 

2004 # deferred: pandas.core.groupby.generic imports from this module at import 

2005 # time, so a top-level import here would be circular 

2006 from pandas.core.groupby.base import reduction_kernels 

2007 from pandas.core.groupby.generic import NamedAgg 

2008 

2009 relabeling = func is None and ( 

2010 is_multi_agg_with_relabel(**kwargs) 

2011 or any(isinstance(v, NamedAgg) for v in kwargs.values()) 

2012 ) 

2013 

2014 columns: tuple[str, ...] | None = None 

2015 order: npt.NDArray[np.intp] | None = None 

2016 

2017 if not relabeling: 

2018 if isinstance(func, list) and len(func) > len(set(func)): 

2019 # GH 28426 will raise error if duplicated function names are used and 

2020 # there is no reassigned name 

2021 raise SpecificationError( 

2022 "Function names must be unique if there is no new column names assigned" 

2023 ) 

2024 if func is None: 

2025 # nicer error message 

2026 raise TypeError("Must provide 'func' or tuples of '(column, aggfunc).") 

2027 

2028 if relabeling: 

2029 normalization_needed = not allow_skip_normalization 

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

2031 # "MutableMapping[Hashable, list[Callable[..., Any] | str]]", variable has type 

2032 # "Callable[..., Any] | str | list[Callable[..., Any] | str] | 

2033 # MutableMapping[Hashable, Callable[..., Any] | str | list[Callable[..., Any] | 

2034 # str]] | None") 

2035 converted_kwargs = {} 

2036 for key, val in kwargs.items(): 

2037 if isinstance(val, NamedAgg): 

2038 column = val.column 

2039 aggfunc = val.aggfunc 

2040 if val.args or val.kwargs: 

2041 aggfunc = lambda x, func=aggfunc, a=val.args, kw=val.kwargs: func( 

2042 x, *a, **kw 

2043 ) 

2044 else: 

2045 column, aggfunc = val 

2046 

2047 # The un-normalized {column: aggfunc} form only matches the normalized 

2048 # one when the aggfunc reduces each group to a single scalar. A 

2049 # list-like aggfunc, or a string naming a non-reduction groupby method 

2050 # (e.g. "describe"/"ohlc", which give a frame per group), would widen 

2051 # the result past the one output column per keyword named aggregation 

2052 # promises. 

2053 if ( 

2054 column != key 

2055 or is_list_like(aggfunc) 

2056 or (isinstance(aggfunc, str) and aggfunc not in reduction_kernels) 

2057 ): 

2058 normalization_needed = True 

2059 converted_kwargs[key] = column, aggfunc 

2060 

2061 if normalization_needed: 

2062 func, columns, order = normalize_keyword_aggregation( # type: ignore[assignment] 

2063 converted_kwargs 

2064 ) 

2065 else: 

2066 # When names match, convert to dict format {column: aggfunc} 

2067 # and disable relabeling since no reordering is needed 

2068 func = { 

2069 column: aggfunc for key, (column, aggfunc) in converted_kwargs.items() 

2070 } 

2071 # Normalization is skipped if all kwargs keys are the same as their 

2072 # corresponding column name. 

2073 relabeling = False 

2074 

2075 assert func is not None 

2076 

2077 return relabeling, func, columns, order 

2078 

2079 

2080def is_multi_agg_with_relabel(**kwargs) -> bool: 

2081 """ 

2082 Check whether kwargs passed to .agg look like multi-agg with relabeling. 

2083 

2084 Parameters 

2085 ---------- 

2086 **kwargs : dict 

2087 

2088 Returns 

2089 ------- 

2090 bool 

2091 

2092 Examples 

2093 -------- 

2094 >>> is_multi_agg_with_relabel(a="max") 

2095 False 

2096 >>> is_multi_agg_with_relabel(a_max=("a", "max"), a_min=("a", "min")) 

2097 True 

2098 >>> is_multi_agg_with_relabel() 

2099 False 

2100 """ 

2101 return all(isinstance(v, tuple) and len(v) == 2 for v in kwargs.values()) and ( 

2102 len(kwargs) > 0 

2103 ) 

2104 

2105 

2106def normalize_keyword_aggregation( 

2107 kwargs: dict, 

2108) -> tuple[ 

2109 MutableMapping[Hashable, list[AggFuncTypeBase]], 

2110 tuple[str, ...], 

2111 npt.NDArray[np.intp], 

2112]: 

2113 """ 

2114 Normalize user-provided "named aggregation" kwargs. 

2115 Transforms from the new ``Mapping[str, NamedAgg]`` style kwargs 

2116 to the old Dict[str, List[scalar]]]. 

2117 

2118 Parameters 

2119 ---------- 

2120 kwargs : dict 

2121 

2122 Returns 

2123 ------- 

2124 aggspec : dict 

2125 The transformed kwargs. 

2126 columns : tuple[str, ...] 

2127 The user-provided keys. 

2128 col_idx_order : List[int] 

2129 List of columns indices. 

2130 

2131 Examples 

2132 -------- 

2133 >>> normalize_keyword_aggregation({"output": ("input", "sum")}) 

2134 (defaultdict(<class 'list'>, {'input': ['sum']}), ('output',), array([0])) 

2135 """ 

2136 from pandas.core.indexes.base import Index 

2137 

2138 # Normalize the aggregation functions as Mapping[column, List[func]], 

2139 # process normally, then fixup the names. 

2140 # TODO: aggspec type: typing.Dict[str, List[AggScalar]] 

2141 aggspec = defaultdict(list) 

2142 order = [] 

2143 columns = tuple(kwargs.keys()) 

2144 

2145 for column, aggfunc in kwargs.values(): 

2146 aggspec[column].append(aggfunc) 

2147 order.append((column, com.get_callable_name(aggfunc) or aggfunc)) 

2148 

2149 # uniquify aggfunc name if duplicated in order list 

2150 uniquified_order = _make_unique_kwarg_list(order) 

2151 

2152 # GH 25719, due to aggspec will change the order of assigned columns in aggregation 

2153 # uniquified_aggspec will store uniquified order list and will compare it with order 

2154 # based on index 

2155 aggspec_order = [ 

2156 (column, com.get_callable_name(aggfunc) or aggfunc) 

2157 for column, aggfuncs in aggspec.items() 

2158 for aggfunc in aggfuncs 

2159 ] 

2160 uniquified_aggspec = _make_unique_kwarg_list(aggspec_order) 

2161 

2162 # get the new index of columns by comparison 

2163 col_idx_order = Index(uniquified_aggspec).get_indexer(uniquified_order) # type: ignore[arg-type] 

2164 return aggspec, columns, col_idx_order 

2165 

2166 

2167def _make_unique_kwarg_list( 

2168 seq: Sequence[tuple[Any, Any]], 

2169) -> Sequence[tuple[Any, Any]]: 

2170 """ 

2171 Uniquify aggfunc name of the pairs in the order list 

2172 

2173 Examples: 

2174 -------- 

2175 >>> kwarg_list = [("a", "<lambda>"), ("a", "<lambda>"), ("b", "<lambda>")] 

2176 >>> _make_unique_kwarg_list(kwarg_list) 

2177 [('a', '<lambda>_0'), ('a', '<lambda>_1'), ('b', '<lambda>')] 

2178 """ 

2179 return [ 

2180 (pair[0], f"{pair[1]}_{seq[:i].count(pair)}") if seq.count(pair) > 1 else pair 

2181 for i, pair in enumerate(seq) 

2182 ] 

2183 

2184 

2185def relabel_result( 

2186 result: DataFrame | Series, 

2187 func: dict[str, list[Callable | str]], 

2188 columns: Iterable[Hashable], 

2189 order: Iterable[int], 

2190) -> dict[Hashable, Series]: 

2191 """ 

2192 Internal function to reorder result if relabelling is True for 

2193 dataframe.agg, and return the reordered result in dict. 

2194 

2195 Parameters: 

2196 ---------- 

2197 result: Result from aggregation 

2198 func: Dict of (column name, funcs) 

2199 columns: New columns name for relabelling 

2200 order: New order for relabelling 

2201 

2202 Examples 

2203 -------- 

2204 >>> from pandas.core.apply import relabel_result 

2205 >>> result = pd.DataFrame( 

2206 ... {"A": [np.nan, 2, np.nan], "C": [6, np.nan, np.nan], "B": [np.nan, 4, 2.5]}, 

2207 ... index=["max", "mean", "min"], 

2208 ... ) 

2209 >>> funcs = {"A": ["max"], "C": ["max"], "B": ["mean", "min"]} 

2210 >>> columns = ("foo", "aab", "bar", "dat") 

2211 >>> order = [0, 1, 2, 3] 

2212 >>> result_in_dict = relabel_result(result, funcs, columns, order) 

2213 >>> pd.DataFrame(result_in_dict, index=columns) 

2214 A C B 

2215 foo 2.0 NaN NaN 

2216 aab NaN 6.0 NaN 

2217 bar NaN NaN 4.0 

2218 dat NaN NaN 2.5 

2219 """ 

2220 from pandas.core.indexes.base import Index 

2221 

2222 reordered_indexes = [ 

2223 pair[0] for pair in sorted(zip(columns, order, strict=True), key=lambda t: t[1]) 

2224 ] 

2225 reordered_result_in_dict: dict[Hashable, Series] = {} 

2226 idx = 0 

2227 

2228 reorder_mask = not isinstance(result, ABCSeries) and len(result.columns) > 1 

2229 for col, fun in func.items(): 

2230 s = result[col].dropna() 

2231 

2232 # In the `_aggregate`, the callable names are obtained and used in `result`, and 

2233 # these names are ordered alphabetically. e.g. 

2234 # C2 C1 

2235 # <lambda> 1 NaN 

2236 # amax NaN 4.0 

2237 # max NaN 4.0 

2238 # sum 18.0 6.0 

2239 # Therefore, the order of functions for each column could be shuffled 

2240 # accordingly so need to get the callable name if it is not parsed names, and 

2241 # reorder the aggregated result for each column. 

2242 # e.g. if df.agg(c1=("C2", sum), c2=("C2", lambda x: min(x))), correct order is 

2243 # [sum, <lambda>], but in `result`, it will be [<lambda>, sum], and we need to 

2244 # reorder so that aggregated values map to their functions regarding the order. 

2245 

2246 # However there is only one column being used for aggregation, not need to 

2247 # reorder since the index is not sorted, and keep as is in `funcs`, e.g. 

2248 # A 

2249 # min 1.0 

2250 # mean 1.5 

2251 # mean 1.5 

2252 if reorder_mask: 

2253 fun = [ 

2254 com.get_callable_name(f) if not isinstance(f, str) else f # type: ignore[misc] 

2255 for f in fun 

2256 ] 

2257 col_idx_order = Index(s.index, copy=False).get_indexer(fun) 

2258 valid_idx = col_idx_order != -1 

2259 if valid_idx.any(): 

2260 s = s.iloc[col_idx_order[valid_idx]] 

2261 # assign the new user-provided "named aggregation" as index names, and reindex 

2262 # it based on the whole user-provided names. 

2263 if not s.empty: 

2264 s.index = reordered_indexes[idx : idx + len(fun)] 

2265 reordered_result_in_dict[col] = s.reindex(columns) 

2266 idx = idx + len(fun) 

2267 return reordered_result_in_dict 

2268 

2269 

2270def reconstruct_and_relabel_result(result, func, **kwargs) -> DataFrame | Series: 

2271 from pandas import DataFrame 

2272 

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

2274 

2275 if relabeling: 

2276 # This is to keep the order to columns occurrence unchanged, and also 

2277 # keep the order of new columns occurrence unchanged 

2278 

2279 # For the return values of reconstruct_func, if relabeling is 

2280 # False, columns and order will be None. 

2281 assert columns is not None 

2282 assert order is not None 

2283 

2284 result_in_dict = relabel_result(result, func, columns, order) 

2285 result = DataFrame(result_in_dict, index=columns) 

2286 

2287 return result 

2288 

2289 

2290# TODO: Can't use, because mypy doesn't like us setting __name__ 

2291# error: "partial[Any]" has no attribute "__name__" 

2292# the type is: 

2293# typing.Sequence[Callable[..., ScalarResult]] 

2294# -> typing.Sequence[Callable[..., ScalarResult]]: 

2295 

2296 

2297def _managle_lambda_list(aggfuncs: Sequence[Any]) -> Sequence[Any]: 

2298 """ 

2299 Possibly mangle a list of aggfuncs. 

2300 

2301 Parameters 

2302 ---------- 

2303 aggfuncs : Sequence 

2304 

2305 Returns 

2306 ------- 

2307 mangled: list-like 

2308 A new AggSpec sequence, where lambdas have been converted 

2309 to have unique names. 

2310 

2311 Notes 

2312 ----- 

2313 If just one aggfunc is passed, the name will not be mangled. 

2314 """ 

2315 if len(aggfuncs) <= 1: 

2316 # don't mangle for .agg([lambda x: .]) 

2317 return aggfuncs 

2318 i = 0 

2319 mangled_aggfuncs = [] 

2320 for aggfunc in aggfuncs: 

2321 if com.get_callable_name(aggfunc) == "<lambda>": 

2322 aggfunc = partial(aggfunc) 

2323 # error: "partial[Any]" has no attribute "__name__"; maybe "__new__"? 

2324 aggfunc.__name__ = f"<lambda_{i}>" # type: ignore[attr-defined] 

2325 i += 1 

2326 mangled_aggfuncs.append(aggfunc) 

2327 

2328 return mangled_aggfuncs 

2329 

2330 

2331def maybe_mangle_lambdas(agg_spec: Any) -> Any: 

2332 """ 

2333 Make new lambdas with unique names. 

2334 

2335 Parameters 

2336 ---------- 

2337 agg_spec : Any 

2338 An argument to GroupBy.agg. 

2339 Non-dict-like `agg_spec` are pass through as is. 

2340 For dict-like `agg_spec` a new spec is returned 

2341 with name-mangled lambdas. 

2342 

2343 Returns 

2344 ------- 

2345 mangled : Any 

2346 Same type as the input. 

2347 

2348 Examples 

2349 -------- 

2350 >>> maybe_mangle_lambdas("sum") 

2351 'sum' 

2352 >>> maybe_mangle_lambdas([lambda: 1, lambda: 2]) # doctest: +SKIP 

2353 [<function __main__.<lambda_0>, 

2354 <function pandas...._make_lambda.<locals>.f(*args, **kwargs)>] 

2355 """ 

2356 is_dict = is_dict_like(agg_spec) 

2357 if not (is_dict or is_list_like(agg_spec)): 

2358 return agg_spec 

2359 mangled_aggspec = type(agg_spec)() # dict or OrderedDict 

2360 

2361 if is_dict: 

2362 for key, aggfuncs in agg_spec.items(): 

2363 if is_list_like(aggfuncs) and not is_dict_like(aggfuncs): 

2364 mangled_aggfuncs = _managle_lambda_list(aggfuncs) 

2365 else: 

2366 mangled_aggfuncs = aggfuncs 

2367 

2368 mangled_aggspec[key] = mangled_aggfuncs 

2369 else: 

2370 mangled_aggspec = _managle_lambda_list(agg_spec) 

2371 

2372 return mangled_aggspec 

2373 

2374 

2375def validate_func_kwargs( 

2376 kwargs: dict, 

2377) -> tuple[list[str], list[str | Callable[..., Any]]]: 

2378 """ 

2379 Validates types of user-provided "named aggregation" kwargs. 

2380 `TypeError` is raised if aggfunc is not `str` or callable. 

2381 

2382 Parameters 

2383 ---------- 

2384 kwargs : dict 

2385 

2386 Returns 

2387 ------- 

2388 columns : List[str] 

2389 List of user-provided keys. 

2390 func : List[Union[str, callable[...,Any]]] 

2391 List of user-provided aggfuncs 

2392 

2393 Examples 

2394 -------- 

2395 >>> validate_func_kwargs({"one": "min", "two": "max"}) 

2396 (['one', 'two'], ['min', 'max']) 

2397 """ 

2398 tuple_given_message = "func is expected but received {} in **kwargs." 

2399 columns = list(kwargs) 

2400 func = [] 

2401 for col_func in kwargs.values(): 

2402 if not (isinstance(col_func, str) or callable(col_func)): 

2403 raise TypeError(tuple_given_message.format(type(col_func).__name__)) 

2404 func.append(col_func) 

2405 if not columns: 

2406 no_arg_message = "Must provide 'func' or named aggregation **kwargs." 

2407 raise TypeError(no_arg_message) 

2408 return columns, func 

2409 

2410 

2411def include_axis(op_name: Literal["agg", "apply"], colg: Series | DataFrame) -> bool: 

2412 return isinstance(colg, ABCDataFrame) or ( 

2413 isinstance(colg, ABCSeries) and op_name == "agg" 

2414 )