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

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

847 statements  

1from __future__ import annotations 

2 

3import abc 

4from collections import defaultdict 

5from collections.abc import Callable 

6import functools 

7from functools import partial 

8import inspect 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 Literal, 

13 TypeAlias, 

14 cast, 

15) 

16 

17import numpy as np 

18 

19from pandas._libs.internals import BlockValuesRefs 

20from pandas._typing import ( 

21 AggFuncType, 

22 AggFuncTypeBase, 

23 AggFuncTypeDict, 

24 AggObjType, 

25 Axis, 

26 AxisInt, 

27 NDFrameT, 

28 npt, 

29) 

30from pandas.compat._optional import import_optional_dependency 

31from pandas.errors import SpecificationError 

32from pandas.util._decorators import ( 

33 cache_readonly, 

34 set_module, 

35) 

36 

37from pandas.core.dtypes.cast import is_nested_object 

38from pandas.core.dtypes.common import ( 

39 is_dict_like, 

40 is_extension_array_dtype, 

41 is_list_like, 

42 is_numeric_dtype, 

43 is_sequence, 

44) 

45from pandas.core.dtypes.dtypes import ExtensionDtype 

46from pandas.core.dtypes.generic import ( 

47 ABCDataFrame, 

48 ABCNDFrame, 

49 ABCSeries, 

50) 

51 

52from pandas.core._numba.executor import generate_apply_looper 

53import pandas.core.common as com 

54from pandas.core.construction import ensure_wrapped_if_datetimelike 

55from pandas.core.util.numba_ import ( 

56 get_jit_arguments, 

57 prepare_function_arguments, 

58) 

59 

60if TYPE_CHECKING: 

61 from collections.abc import ( 

62 Generator, 

63 Hashable, 

64 Iterable, 

65 MutableMapping, 

66 Sequence, 

67 ) 

68 

69 from pandas import ( 

70 DataFrame, 

71 Index, 

72 Series, 

73 ) 

74 from pandas.core.groupby import GroupBy 

75 from pandas.core.resample import Resampler 

76 from pandas.core.window.rolling import BaseWindow 

77 

78ResType: TypeAlias = dict[int, Any] 

79 

80 

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

82class BaseExecutionEngine(abc.ABC): 

83 """ 

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

85 

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

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

88 etc. and takes care of running the execution. 

89 

90 Supporting different engines allows functions to be JIT compiled, 

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

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

93 """ 

94 

95 @staticmethod 

96 @abc.abstractmethod 

97 def map( 

98 data: Series | DataFrame | np.ndarray, 

99 func: AggFuncType, 

100 args: tuple, 

101 kwargs: dict[str, Any], 

102 decorator: Callable | None, 

103 skip_na: bool, 

104 ): 

105 """ 

106 Executor method to run functions elementwise. 

107 

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

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

110 call this executor function. 

111 

112 Parameters 

113 ---------- 

114 data : Series, DataFrame or NumPy ndarray 

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

116 parameter which will convert the original pandas object to a 

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

118 func : function or NumPy ufunc 

119 The function to execute. 

120 args : tuple 

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

122 kwargs : dict 

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

124 decorator : function, optional 

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

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

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

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

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

130 decorator parameter will contain the exact decorator from the 

131 executor the user wants to use. 

132 skip_na : bool 

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

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

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

136 """ 

137 

138 @staticmethod 

139 @abc.abstractmethod 

140 def apply( 

141 data: Series | DataFrame | np.ndarray, 

142 func: AggFuncType, 

143 args: tuple, 

144 kwargs: dict[str, Any], 

145 decorator: Callable, 

146 axis: Axis, 

147 ): 

148 """ 

149 Executor method to run functions by an axis. 

150 

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

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

153 function for each column (or row). 

154 

155 Parameters 

156 ---------- 

157 data : Series, DataFrame or NumPy ndarray 

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

159 parameter which will convert the original pandas object to a 

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

161 func : function or NumPy ufunc 

162 The function to execute. 

163 args : tuple 

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

165 kwargs : dict 

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

167 decorator : function, optional 

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

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

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

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

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

173 decorator parameter will contain the exact decorator from the 

174 executor the user wants to use. 

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

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

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

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

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

180 expect functions to be implemented this way for compatibility. 

181 But passing rows as other data structures is technically possible 

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

183 """ 

184 

185 

186def frame_apply( 

187 obj: DataFrame, 

188 func: AggFuncType, 

189 axis: Axis = 0, 

190 raw: bool = False, 

191 result_type: str | None = None, 

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

193 engine: str = "python", 

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

195 args=None, 

196 kwargs=None, 

197) -> FrameApply: 

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

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

200 

201 axis = obj._get_axis_number(axis) 

202 klass: type[FrameApply] 

203 if axis == 0: 

204 klass = FrameRowApply 

205 elif axis == 1: 

206 if columns: 

207 raise NotImplementedError( 

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

209 ) 

210 klass = FrameColumnApply 

211 

212 return klass( 

213 obj, 

214 func, 

215 raw=raw, 

216 result_type=result_type, 

217 by_row=by_row, 

218 engine=engine, 

219 engine_kwargs=engine_kwargs, 

220 args=args, 

221 kwargs=kwargs, 

222 ) 

223 

224 

225class Apply(metaclass=abc.ABCMeta): 

226 axis: AxisInt 

227 

228 def __init__( 

229 self, 

230 obj: AggObjType, 

231 func: AggFuncType, 

232 raw: bool, 

233 result_type: str | None, 

234 *, 

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

236 engine: str = "python", 

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

238 args, 

239 kwargs, 

240 ) -> None: 

241 self.obj = obj 

242 self.raw = raw 

243 

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

245 self.by_row = by_row 

246 

247 self.args = args or () 

248 self.kwargs = kwargs or {} 

249 

250 self.engine = engine 

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

252 

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

254 raise ValueError( 

255 "invalid value for result_type, must be one " 

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

257 ) 

258 

259 self.result_type = result_type 

260 

261 self.func = func 

262 

263 @abc.abstractmethod 

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

265 pass 

266 

267 @abc.abstractmethod 

268 def agg_or_apply_list_like( 

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

270 ) -> DataFrame | Series: 

271 pass 

272 

273 @abc.abstractmethod 

274 def agg_or_apply_dict_like( 

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

276 ) -> DataFrame | Series: 

277 pass 

278 

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

280 """ 

281 Provide an implementation for the aggregators. 

282 

283 Returns 

284 ------- 

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

286 this method. 

287 """ 

288 func = self.func 

289 

290 if isinstance(func, str): 

291 return self.apply_str() 

292 

293 if is_dict_like(func): 

294 return self.agg_dict_like() 

295 elif is_list_like(func): 

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

297 return self.agg_list_like() 

298 

299 # caller can react 

300 return None 

301 

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

303 """ 

304 Transform a DataFrame or Series. 

305 

306 Returns 

307 ------- 

308 DataFrame or Series 

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

310 Series or DataFrame. 

311 

312 Raises 

313 ------ 

314 ValueError 

315 If the transform function fails or does not transform. 

316 """ 

317 obj = self.obj 

318 func = self.func 

319 axis = self.axis 

320 args = self.args 

321 kwargs = self.kwargs 

322 

323 is_series = obj.ndim == 1 

324 

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

326 assert not is_series 

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

328 

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

330 func = cast(list[AggFuncTypeBase], func) 

331 # Convert func equivalent dict 

332 if is_series: 

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

334 else: 

335 func = dict.fromkeys(obj, func) 

336 

337 if is_dict_like(func): 

338 func = cast(AggFuncTypeDict, func) 

339 return self.transform_dict_like(func) 

340 

341 # func is either str or callable 

342 func = cast(AggFuncTypeBase, func) 

343 try: 

344 result = self.transform_str_or_callable(func) 

345 except TypeError: 

346 raise 

347 except Exception as err: 

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

349 

350 # Functions that transform may return empty Series/DataFrame 

351 # when the dtype is not appropriate 

352 if ( 

353 isinstance(result, (ABCSeries, ABCDataFrame)) 

354 and result.empty 

355 and not obj.empty 

356 ): 

357 raise ValueError("Transform function failed") 

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

359 obj.index 

360 ): 

361 raise ValueError("Function did not transform") 

362 

363 return result 

364 

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

366 """ 

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

368 """ 

369 from pandas.core.reshape.concat import concat 

370 

371 obj = self.obj 

372 args = self.args 

373 kwargs = self.kwargs 

374 

375 # transform is currently only for Series/DataFrame 

376 assert isinstance(obj, ABCNDFrame) 

377 

378 if len(func) == 0: 

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

380 

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

382 

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

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

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

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

387 return concat(results, axis=1) 

388 

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

390 """ 

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

392 """ 

393 obj = self.obj 

394 args = self.args 

395 kwargs = self.kwargs 

396 

397 if isinstance(func, str): 

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

399 

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

401 try: 

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

403 except Exception: 

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

405 

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

407 """ 

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

409 

410 Returns 

411 ------- 

412 Result of aggregation. 

413 """ 

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

415 

416 def compute_list_like( 

417 self, 

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

419 selected_obj: Series | DataFrame, 

420 kwargs: dict[str, Any], 

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

422 """ 

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

424 

425 Parameters 

426 ---------- 

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

428 Operation being performed. 

429 selected_obj : Series or DataFrame 

430 Data to perform operation on. 

431 kwargs : dict 

432 Keyword arguments to pass to the functions. 

433 

434 Returns 

435 ------- 

436 keys : list[Hashable] or Index 

437 Index labels for result. 

438 results : list 

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

440 Python objects. 

441 """ 

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

443 obj = self.obj 

444 

445 results = [] 

446 keys = [] 

447 

448 # degenerate case 

449 if selected_obj.ndim == 1: 

450 for a in func: 

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

452 args = ( 

453 [self.axis, *self.args] 

454 if include_axis(op_name, colg) 

455 else self.args 

456 ) 

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

458 results.append(new_res) 

459 

460 # make sure we find a good name 

461 name = com.get_callable_name(a) or a 

462 keys.append(name) 

463 

464 else: 

465 indices = [] 

466 for index, col in enumerate(selected_obj): 

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

468 args = ( 

469 [self.axis, *self.args] 

470 if include_axis(op_name, colg) 

471 else self.args 

472 ) 

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

474 results.append(new_res) 

475 indices.append(index) 

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

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

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

479 

480 return keys, results 

481 

482 def wrap_results_list_like( 

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

484 ): 

485 from pandas.core.reshape.concat import concat 

486 

487 obj = self.obj 

488 

489 try: 

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

491 except TypeError as err: 

492 # we are concatting non-NDFrame objects, 

493 # e.g. a list of scalars 

494 from pandas import Series 

495 

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

497 if is_nested_object(result): 

498 raise ValueError( 

499 "cannot combine transform and aggregation operations" 

500 ) from err 

501 return result 

502 

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

504 """ 

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

506 

507 Returns 

508 ------- 

509 Result of aggregation. 

510 """ 

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

512 

513 def compute_dict_like( 

514 self, 

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

516 selected_obj: Series | DataFrame, 

517 selection: Hashable | Sequence[Hashable], 

518 kwargs: dict[str, Any], 

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

520 """ 

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

522 

523 Parameters 

524 ---------- 

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

526 Operation being performed. 

527 selected_obj : Series or DataFrame 

528 Data to perform operation on. 

529 selection : hashable or sequence of hashables 

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

531 kwargs : dict 

532 Keyword arguments to pass to the functions. 

533 

534 Returns 

535 ------- 

536 keys : list[hashable] 

537 Index labels for result. 

538 results : list 

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

540 Python object. 

541 """ 

542 from pandas.core.groupby.generic import ( 

543 DataFrameGroupBy, 

544 SeriesGroupBy, 

545 ) 

546 

547 obj = self.obj 

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

549 func = cast(AggFuncTypeDict, self.func) 

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

551 

552 is_non_unique_col = ( 

553 selected_obj.ndim == 2 

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

555 ) 

556 

557 if selected_obj.ndim == 1: 

558 # key only used for output 

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

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

561 keys = list(func.keys()) 

562 elif not is_groupby and is_non_unique_col: 

563 # key used for column selection and output 

564 # GH#51099 

565 results = [] 

566 keys = [] 

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

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

569 labels = selected_obj.columns.take(indices) 

570 label_to_indices = defaultdict(list) 

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

572 label_to_indices[label].append(index) 

573 

574 key_data = [ 

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

576 for label, indices in label_to_indices.items() 

577 for indice in indices 

578 ] 

579 

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

581 results += key_data 

582 elif is_groupby: 

583 # key used for column selection and output 

584 

585 df = selected_obj 

586 results, keys = [], [] 

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

588 cols = df[key] 

589 

590 if cols.ndim == 1: 

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

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

593 keys.append(key) 

594 else: 

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

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

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

598 keys.append(key) 

599 else: 

600 results = [ 

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

602 for key, how in func.items() 

603 ] 

604 keys = list(func.keys()) 

605 

606 return keys, results 

607 

608 def wrap_results_dict_like( 

609 self, 

610 selected_obj: Series | DataFrame, 

611 result_index: list[Hashable], 

612 result_data: list, 

613 ): 

614 from pandas import Index 

615 from pandas.core.reshape.concat import concat 

616 

617 obj = self.obj 

618 

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

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

621 

622 if all(is_ndframe): 

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

624 keys_to_use: Iterable[Hashable] 

625 keys_to_use = [ 

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

627 ] 

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

629 if keys_to_use == []: 

630 keys_to_use = result_index 

631 results = result_data 

632 

633 if selected_obj.ndim == 2: 

634 # keys are columns, so we can preserve names 

635 ktu = Index(keys_to_use) 

636 ktu._set_names(selected_obj.columns.names) 

637 keys_to_use = ktu 

638 

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

640 result = concat( 

641 results, 

642 axis=axis, 

643 keys=keys_to_use, 

644 sort=False, 

645 ) 

646 elif any(is_ndframe): 

647 # There is a mix of NDFrames and scalars 

648 raise ValueError( 

649 "cannot perform both aggregation " 

650 "and transformation operations " 

651 "simultaneously" 

652 ) 

653 else: 

654 from pandas import Series 

655 

656 # we have a list of scalars 

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

658 if obj.ndim == 1: 

659 obj = cast("Series", obj) 

660 name = obj.name 

661 else: 

662 name = None 

663 

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

665 

666 return result 

667 

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

669 """ 

670 Compute apply in case of a string. 

671 

672 Returns 

673 ------- 

674 result: Series or DataFrame 

675 """ 

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

677 func = cast(str, self.func) 

678 

679 obj = self.obj 

680 

681 from pandas.core.groupby.generic import ( 

682 DataFrameGroupBy, 

683 SeriesGroupBy, 

684 ) 

685 

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

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

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

689 method = getattr(obj, func, None) 

690 if callable(method): 

691 sig = inspect.getfullargspec(method) 

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

693 if self.axis != 0 and ( 

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

695 ): 

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

697 if "axis" in arg_names and not isinstance( 

698 obj, (SeriesGroupBy, DataFrameGroupBy) 

699 ): 

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

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

702 

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

704 """ 

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

706 

707 Returns 

708 ------- 

709 result: Series, DataFrame, or None 

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

711 """ 

712 

713 if self.engine == "numba": 

714 raise NotImplementedError( 

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

716 "dict likes of callables yet." 

717 ) 

718 

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

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

721 

722 func = self.func 

723 kwargs = self.kwargs 

724 

725 if is_dict_like(func): 

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

727 else: 

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

729 

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

731 

732 return result 

733 

734 def normalize_dictlike_arg( 

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

736 ) -> AggFuncTypeDict: 

737 """ 

738 Handler for dict-like argument. 

739 

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

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

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

743 """ 

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

745 

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

747 if ( 

748 how == "agg" 

749 and isinstance(obj, ABCSeries) 

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

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

752 # GH 15931 - deprecation of renaming keys 

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

754 

755 if obj.ndim != 1: 

756 # Check for missing columns on a frame 

757 from pandas import Index 

758 

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

760 if len(cols) > 0: 

761 # GH 58474 

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

763 

764 aggregator_types = (list, tuple, dict) 

765 

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

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

768 # be list-likes 

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

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

771 new_func: AggFuncTypeDict = {} 

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

773 if not isinstance(v, aggregator_types): 

774 new_func[k] = [v] 

775 else: 

776 new_func[k] = v 

777 func = new_func 

778 return func 

779 

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

781 """ 

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

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

784 - try to find a numpy function 

785 - raise 

786 """ 

787 assert isinstance(func, str) 

788 

789 if hasattr(obj, func): 

790 f = getattr(obj, func) 

791 if callable(f): 

792 return f(*args, **kwargs) 

793 

794 # people may aggregate on a non-callable attribute 

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

796 assert len(args) == 0 

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

798 return f 

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

800 # in particular exclude Window 

801 f = getattr(np, func) 

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

803 else: 

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

805 raise AttributeError(msg) 

806 

807 

808class NDFrameApply(Apply): 

809 """ 

810 Methods shared by FrameApply and SeriesApply but 

811 not GroupByApply or ResamplerWindowApply 

812 """ 

813 

814 obj: DataFrame | Series 

815 

816 @property 

817 def index(self) -> Index: 

818 return self.obj.index 

819 

820 @property 

821 def agg_axis(self) -> Index: 

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

823 

824 def agg_or_apply_list_like( 

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

826 ) -> DataFrame | Series: 

827 obj = self.obj 

828 kwargs = self.kwargs 

829 

830 if op_name == "apply": 

831 if isinstance(self, FrameApply): 

832 by_row = self.by_row 

833 

834 elif isinstance(self, SeriesApply): 

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

836 else: 

837 by_row = False 

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

839 

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

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

842 

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

844 result = self.wrap_results_list_like(keys, results) 

845 return result 

846 

847 def agg_or_apply_dict_like( 

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

849 ) -> DataFrame | Series: 

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

851 obj = self.obj 

852 

853 kwargs = {} 

854 if op_name == "apply": 

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

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

857 

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

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

860 

861 selection = None 

862 result_index, result_data = self.compute_dict_like( 

863 op_name, obj, selection, kwargs 

864 ) 

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

866 return result 

867 

868 

869class FrameApply(NDFrameApply): 

870 obj: DataFrame 

871 

872 def __init__( 

873 self, 

874 obj: AggObjType, 

875 func: AggFuncType, 

876 raw: bool, 

877 result_type: str | None, 

878 *, 

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

880 engine: str = "python", 

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

882 args, 

883 kwargs, 

884 ) -> None: 

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

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

887 super().__init__( 

888 obj, 

889 func, 

890 raw, 

891 result_type, 

892 by_row=by_row, 

893 engine=engine, 

894 engine_kwargs=engine_kwargs, 

895 args=args, 

896 kwargs=kwargs, 

897 ) 

898 

899 # --------------------------------------------------------------- 

900 # Abstract Methods 

901 

902 @property 

903 @abc.abstractmethod 

904 def result_index(self) -> Index: 

905 pass 

906 

907 @property 

908 @abc.abstractmethod 

909 def result_columns(self) -> Index: 

910 pass 

911 

912 @property 

913 @abc.abstractmethod 

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

915 pass 

916 

917 @staticmethod 

918 @functools.cache 

919 @abc.abstractmethod 

920 def generate_numba_apply_func( 

921 func, nogil=True, nopython=True, parallel=False 

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

923 pass 

924 

925 @abc.abstractmethod 

926 def apply_with_numba(self): 

927 pass 

928 

929 def validate_values_for_numba(self) -> None: 

930 # Validate column dtyps all OK 

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

932 if not is_numeric_dtype(dtype): 

933 raise ValueError( 

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

935 f"Found '{dtype}' instead" 

936 ) 

937 if is_extension_array_dtype(dtype): 

938 raise ValueError( 

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

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

941 ) 

942 

943 @abc.abstractmethod 

944 def wrap_results_for_axis( 

945 self, results: ResType, res_index: Index 

946 ) -> DataFrame | Series: 

947 pass 

948 

949 # --------------------------------------------------------------- 

950 

951 @property 

952 def res_columns(self) -> Index: 

953 return self.result_columns 

954 

955 @property 

956 def columns(self) -> Index: 

957 return self.obj.columns 

958 

959 @cache_readonly 

960 def values(self): 

961 return self.obj.values 

962 

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

964 """compute the results""" 

965 

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

967 if is_list_like(self.func): 

968 if self.engine == "numba": 

969 raise NotImplementedError( 

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

971 ) 

972 return self.apply_list_or_dict_like() 

973 

974 # all empty 

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

976 return self.apply_empty_result() 

977 

978 # string dispatch 

979 if isinstance(self.func, str): 

980 if self.engine == "numba": 

981 raise NotImplementedError( 

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

983 "a string as the callable function" 

984 ) 

985 return self.apply_str() 

986 

987 # ufunc 

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

989 if self.engine == "numba": 

990 raise NotImplementedError( 

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

992 "using a numpy ufunc as the callable function" 

993 ) 

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

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

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

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

998 

999 # broadcasting 

1000 if self.result_type == "broadcast": 

1001 if self.engine == "numba": 

1002 raise NotImplementedError( 

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

1004 ) 

1005 return self.apply_broadcast(self.obj) 

1006 

1007 # one axis empty 

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

1009 return self.apply_empty_result() 

1010 

1011 # raw 

1012 elif self.raw: 

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

1014 

1015 return self.apply_standard() 

1016 

1017 def agg(self): 

1018 obj = self.obj 

1019 axis = self.axis 

1020 

1021 # TODO: Avoid having to change state 

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

1023 self.axis = 0 

1024 

1025 result = None 

1026 try: 

1027 result = super().agg() 

1028 finally: 

1029 self.obj = obj 

1030 self.axis = axis 

1031 

1032 if axis == 1: 

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

1034 

1035 if result is None: 

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

1037 

1038 return result 

1039 

1040 def apply_empty_result(self): 

1041 """ 

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

1043 

1044 we will try to apply the function to an empty 

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

1046 """ 

1047 assert callable(self.func) 

1048 

1049 # we are not asked to reduce or infer reduction 

1050 # so just return a copy of the existing object 

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

1052 return self.obj.copy() 

1053 

1054 # we may need to infer 

1055 should_reduce = self.result_type == "reduce" 

1056 

1057 from pandas import Series 

1058 

1059 if not should_reduce: 

1060 try: 

1061 if self.axis == 0: 

1062 r = self.func( 

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

1064 ) 

1065 else: 

1066 r = self.func( 

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

1068 *self.args, 

1069 **self.kwargs, 

1070 ) 

1071 except Exception: 

1072 pass 

1073 else: 

1074 should_reduce = not isinstance(r, Series) 

1075 

1076 if should_reduce: 

1077 if len(self.agg_axis): 

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

1079 else: 

1080 r = np.nan 

1081 

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

1083 else: 

1084 return self.obj.copy() 

1085 

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

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

1088 

1089 def wrap_function(func): 

1090 """ 

1091 Wrap user supplied function to work around numpy issue. 

1092 

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

1094 """ 

1095 

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

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

1098 if isinstance(result, str): 

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

1100 return result 

1101 

1102 return wrapper 

1103 

1104 if engine == "numba": 

1105 args, kwargs = prepare_function_arguments( 

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

1107 self.args, 

1108 self.kwargs, 

1109 num_required_args=1, 

1110 ) 

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

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

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

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

1115 nb_looper = generate_apply_looper( 

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

1117 **get_jit_arguments(engine_kwargs), 

1118 ) 

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

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

1121 result = np.squeeze(result) 

1122 else: 

1123 result = np.apply_along_axis( 

1124 wrap_function(self.func), 

1125 self.axis, 

1126 self.values, 

1127 *self.args, 

1128 **self.kwargs, 

1129 ) 

1130 

1131 # TODO: mixed type case 

1132 if result.ndim == 2: 

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

1134 else: 

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

1136 

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

1138 assert callable(self.func) 

1139 

1140 result_values = np.empty_like(target.values) 

1141 

1142 # axis which we want to compare compliance 

1143 result_compare = target.shape[0] 

1144 

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

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

1147 ares = np.asarray(res).ndim 

1148 

1149 # must be a scalar or 1d 

1150 if ares > 1: 

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

1152 if ares == 1: 

1153 # must match return dim 

1154 if result_compare != len(res): 

1155 raise ValueError("cannot broadcast result") 

1156 

1157 result_values[:, i] = res 

1158 

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

1160 result = self.obj._constructor( 

1161 result_values, index=target.index, columns=target.columns 

1162 ) 

1163 return result 

1164 

1165 def apply_standard(self): 

1166 if self.engine == "python": 

1167 results, res_index = self.apply_series_generator() 

1168 else: 

1169 results, res_index = self.apply_series_numba() 

1170 

1171 # wrap results 

1172 return self.wrap_results(results, res_index) 

1173 

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

1175 assert callable(self.func) 

1176 

1177 series_gen = self.series_generator 

1178 res_index = self.result_index 

1179 

1180 results = {} 

1181 

1182 for i, v in enumerate(series_gen): 

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

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

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

1186 # series_generator will swap out the underlying data 

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

1188 

1189 return results, res_index 

1190 

1191 def apply_series_numba(self): 

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

1193 raise NotImplementedError( 

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

1195 ) 

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

1197 raise NotImplementedError( 

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

1199 ) 

1200 self.validate_values_for_numba() 

1201 results = self.apply_with_numba() 

1202 return results, self.result_index 

1203 

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

1205 from pandas import Series 

1206 

1207 # see if we can infer the results 

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

1209 return self.wrap_results_for_axis(results, res_index) 

1210 

1211 # dict of scalars 

1212 

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

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

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

1216 constructor_sliced = self.obj._constructor_sliced 

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

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

1219 else: 

1220 result = constructor_sliced(results) 

1221 result.index = res_index 

1222 

1223 return result 

1224 

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

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

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

1228 if self.func == "size": 

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

1230 obj = self.obj 

1231 value = obj.shape[self.axis] 

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

1233 return super().apply_str() 

1234 

1235 

1236class FrameRowApply(FrameApply): 

1237 axis: AxisInt = 0 

1238 

1239 @property 

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

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

1242 

1243 @staticmethod 

1244 @functools.cache 

1245 def generate_numba_apply_func( 

1246 func, nogil=True, nopython=True, parallel=False 

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

1248 numba = import_optional_dependency("numba") 

1249 from pandas import Series 

1250 

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

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

1253 from pandas.core._numba.extensions import maybe_cast_str 

1254 

1255 jitted_udf = numba.extending.register_jitable(func) 

1256 

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

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

1259 @numba.jit(nogil=nogil, nopython=nopython, parallel=parallel) 

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

1261 results = {} 

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

1263 # Create the series 

1264 ser = Series( 

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

1266 ) 

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

1268 return results 

1269 

1270 return numba_func 

1271 

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

1273 func = cast(Callable, self.func) 

1274 args, kwargs = prepare_function_arguments( 

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

1276 ) 

1277 nb_func = self.generate_numba_apply_func( 

1278 func, **get_jit_arguments(self.engine_kwargs) 

1279 ) 

1280 from pandas.core._numba.extensions import set_numba_data 

1281 

1282 index = self.obj.index 

1283 columns = self.obj.columns 

1284 

1285 # Convert from numba dict to regular dict 

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

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

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

1289 return res 

1290 

1291 @property 

1292 def result_index(self) -> Index: 

1293 return self.columns 

1294 

1295 @property 

1296 def result_columns(self) -> Index: 

1297 return self.index 

1298 

1299 def wrap_results_for_axis( 

1300 self, results: ResType, res_index: Index 

1301 ) -> DataFrame | Series: 

1302 """return the results for the rows""" 

1303 

1304 if self.result_type == "reduce": 

1305 # e.g. test_apply_dict GH#8735 

1306 res = self.obj._constructor_sliced(results) 

1307 res.index = res_index 

1308 return res 

1309 

1310 elif self.result_type is None and all( 

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

1312 ): 

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

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

1315 res = self.obj._constructor_sliced(results) 

1316 res.index = res_index 

1317 return res 

1318 

1319 try: 

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

1321 except ValueError as err: 

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

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

1324 # see test_agg_listlike_result GH#29587 

1325 res = self.obj._constructor_sliced(results) 

1326 res.index = res_index 

1327 return res 

1328 else: 

1329 raise 

1330 

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

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

1333 result.index = self.res_columns 

1334 

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

1336 result.columns = res_index 

1337 

1338 return result 

1339 

1340 

1341class FrameColumnApply(FrameApply): 

1342 axis: AxisInt = 1 

1343 

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

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

1346 return result.T 

1347 

1348 @property 

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

1350 values = self.values 

1351 values = ensure_wrapped_if_datetimelike(values) 

1352 assert len(values) > 0 

1353 

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

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

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

1357 mgr = ser._mgr 

1358 

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

1360 

1361 if isinstance(ser.dtype, ExtensionDtype): 

1362 # values will be incorrect for this block 

1363 # TODO(EA2D): special case would be unnecessary with 2D EAs 

1364 obj = self.obj 

1365 for i in range(len(obj)): 

1366 yield obj._ixs(i, axis=0) 

1367 

1368 else: 

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

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

1371 ser._mgr = mgr 

1372 mgr.set_values(arr) 

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

1374 if not is_view: 

1375 # In apply_series_generator we store the a shallow copy of the 

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

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

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

1379 # the refs here to avoid triggering a unnecessary CoW inside the 

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

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

1382 yield ser 

1383 

1384 @staticmethod 

1385 @functools.cache 

1386 def generate_numba_apply_func( 

1387 func, nogil=True, nopython=True, parallel=False 

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

1389 numba = import_optional_dependency("numba") 

1390 from pandas import Series 

1391 from pandas.core._numba.extensions import maybe_cast_str 

1392 

1393 jitted_udf = numba.extending.register_jitable(func) 

1394 

1395 @numba.jit(nogil=nogil, nopython=nopython, parallel=parallel) 

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

1397 results = {} 

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

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

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

1401 # Create the series 

1402 # TODO: values corrupted without the copy 

1403 ser = Series( 

1404 values[i].copy(), 

1405 index=col_names_index, 

1406 name=maybe_cast_str(index[i]), 

1407 ) 

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

1409 

1410 return results 

1411 

1412 return numba_func 

1413 

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

1415 func = cast(Callable, self.func) 

1416 args, kwargs = prepare_function_arguments( 

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

1418 ) 

1419 nb_func = self.generate_numba_apply_func( 

1420 func, **get_jit_arguments(self.engine_kwargs) 

1421 ) 

1422 

1423 from pandas.core._numba.extensions import set_numba_data 

1424 

1425 # Convert from numba dict to regular dict 

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

1427 with ( 

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

1429 set_numba_data(self.columns) as columns, 

1430 ): 

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

1432 

1433 return res 

1434 

1435 @property 

1436 def result_index(self) -> Index: 

1437 return self.index 

1438 

1439 @property 

1440 def result_columns(self) -> Index: 

1441 return self.columns 

1442 

1443 def wrap_results_for_axis( 

1444 self, results: ResType, res_index: Index 

1445 ) -> DataFrame | Series: 

1446 """return the results for the columns""" 

1447 result: DataFrame | Series 

1448 

1449 # we have requested to expand 

1450 if self.result_type == "expand": 

1451 result = self.infer_to_same_shape(results, res_index) 

1452 

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

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

1455 result = self.obj._constructor_sliced(results) 

1456 result.index = res_index 

1457 

1458 # we may want to infer results 

1459 else: 

1460 result = self.infer_to_same_shape(results, res_index) 

1461 

1462 return result 

1463 

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

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

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

1467 result = result.T 

1468 

1469 # set the index 

1470 result.index = res_index 

1471 

1472 # infer dtypes 

1473 result = result.infer_objects() 

1474 

1475 return result 

1476 

1477 

1478class SeriesApply(NDFrameApply): 

1479 obj: Series 

1480 axis: AxisInt = 0 

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

1482 

1483 def __init__( 

1484 self, 

1485 obj: Series, 

1486 func: AggFuncType, 

1487 *, 

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

1489 args, 

1490 kwargs, 

1491 ) -> None: 

1492 super().__init__( 

1493 obj, 

1494 func, 

1495 raw=False, 

1496 result_type=None, 

1497 by_row=by_row, 

1498 args=args, 

1499 kwargs=kwargs, 

1500 ) 

1501 

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

1503 obj = self.obj 

1504 

1505 if len(obj) == 0: 

1506 return self.apply_empty_result() 

1507 

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

1509 if is_list_like(self.func): 

1510 return self.apply_list_or_dict_like() 

1511 

1512 if isinstance(self.func, str): 

1513 # if we are a string, try to dispatch 

1514 return self.apply_str() 

1515 

1516 if self.by_row == "_compat": 

1517 return self.apply_compat() 

1518 

1519 # self.func is Callable 

1520 return self.apply_standard() 

1521 

1522 def agg(self): 

1523 result = super().agg() 

1524 if result is None: 

1525 obj = self.obj 

1526 func = self.func 

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

1528 assert callable(func) 

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

1530 return result 

1531 

1532 def apply_empty_result(self) -> Series: 

1533 obj = self.obj 

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

1535 obj, method="apply" 

1536 ) 

1537 

1538 def apply_compat(self): 

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

1540 

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

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

1543 

1544 .. versionadded:: 2.1.0 

1545 """ 

1546 obj = self.obj 

1547 func = self.func 

1548 

1549 if callable(func): 

1550 f = com.get_cython_func(func) 

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

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

1553 

1554 try: 

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

1556 except (ValueError, AttributeError, TypeError): 

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

1558 return result 

1559 

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

1561 # caller is responsible for ensuring that f is Callable 

1562 func = cast(Callable, self.func) 

1563 obj = self.obj 

1564 

1565 if isinstance(func, np.ufunc): 

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

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

1568 elif not self.by_row: 

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

1570 

1571 if self.args or self.kwargs: 

1572 # _map_values does not support args/kwargs 

1573 def curried(x): 

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

1575 

1576 else: 

1577 curried = func 

1578 mapped = obj._map_values(mapper=curried) 

1579 

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

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

1582 # See also GH#25959 regarding EA support 

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

1584 else: 

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

1586 obj, method="apply" 

1587 ) 

1588 

1589 

1590class GroupByApply(Apply): 

1591 obj: GroupBy | Resampler | BaseWindow 

1592 

1593 def __init__( 

1594 self, 

1595 obj: GroupBy[NDFrameT], 

1596 func: AggFuncType, 

1597 *, 

1598 args, 

1599 kwargs, 

1600 ) -> None: 

1601 kwargs = kwargs.copy() 

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

1603 super().__init__( 

1604 obj, 

1605 func, 

1606 raw=False, 

1607 result_type=None, 

1608 args=args, 

1609 kwargs=kwargs, 

1610 ) 

1611 

1612 def apply(self): 

1613 raise NotImplementedError 

1614 

1615 def transform(self): 

1616 raise NotImplementedError 

1617 

1618 def agg_or_apply_list_like( 

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

1620 ) -> DataFrame | Series: 

1621 obj = self.obj 

1622 kwargs = self.kwargs 

1623 if op_name == "apply": 

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

1625 

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

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

1628 

1629 if obj._selected_obj.ndim == 1: 

1630 # For SeriesGroupBy this matches _obj_with_exclusions 

1631 selected_obj = obj._selected_obj 

1632 else: 

1633 selected_obj = obj._obj_with_exclusions 

1634 

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

1636 # that inherit from this class. 

1637 with com.temp_setattr( 

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

1639 ): 

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

1641 result = self.wrap_results_list_like(keys, results) 

1642 return result 

1643 

1644 def agg_or_apply_dict_like( 

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

1646 ) -> DataFrame | Series: 

1647 from pandas.core.groupby.generic import ( 

1648 DataFrameGroupBy, 

1649 SeriesGroupBy, 

1650 ) 

1651 

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

1653 

1654 obj = self.obj 

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

1656 if op_name == "apply": 

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

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

1659 

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

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

1662 

1663 selected_obj = obj._selected_obj 

1664 selection = obj._selection 

1665 

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

1667 

1668 # Numba Groupby engine/engine-kwargs passthrough 

1669 if is_groupby: 

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

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

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

1673 

1674 with com.temp_setattr( 

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

1676 ): 

1677 result_index, result_data = self.compute_dict_like( 

1678 op_name, selected_obj, selection, kwargs 

1679 ) 

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

1681 return result 

1682 

1683 

1684class ResamplerWindowApply(GroupByApply): 

1685 axis: AxisInt = 0 

1686 obj: Resampler | BaseWindow 

1687 

1688 def __init__( 

1689 self, 

1690 obj: Resampler | BaseWindow, 

1691 func: AggFuncType, 

1692 *, 

1693 args, 

1694 kwargs, 

1695 ) -> None: 

1696 super(GroupByApply, self).__init__( 

1697 obj, 

1698 func, 

1699 raw=False, 

1700 result_type=None, 

1701 args=args, 

1702 kwargs=kwargs, 

1703 ) 

1704 

1705 def apply(self): 

1706 raise NotImplementedError 

1707 

1708 def transform(self): 

1709 raise NotImplementedError 

1710 

1711 

1712def reconstruct_func( 

1713 func: AggFuncType | None, **kwargs 

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

1715 """ 

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

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

1718 

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

1720 column and aggregation function information to be parsed; 

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

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

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

1724 

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

1726 names, and the reconstructed order of columns. 

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

1728 

1729 Parameters 

1730 ---------- 

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

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

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

1734 normalize_keyword_aggregation function for relabelling 

1735 

1736 Returns 

1737 ------- 

1738 relabelling: bool, if there is relabelling or not 

1739 func: normalized and mangled func 

1740 columns: tuple of column names 

1741 order: array of columns indices 

1742 

1743 Examples 

1744 -------- 

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

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

1747 

1748 >>> reconstruct_func("min") 

1749 (False, 'min', None, None) 

1750 """ 

1751 from pandas.core.groupby.generic import NamedAgg 

1752 

1753 relabeling = func is None and ( 

1754 is_multi_agg_with_relabel(**kwargs) 

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

1756 ) 

1757 

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

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

1760 

1761 if not relabeling: 

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

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

1764 # there is no reassigned name 

1765 raise SpecificationError( 

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

1767 ) 

1768 if func is None: 

1769 # nicer error message 

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

1771 

1772 if relabeling: 

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

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

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

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

1777 # str]] | None") 

1778 converted_kwargs = {} 

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

1780 if isinstance(val, NamedAgg): 

1781 aggfunc = val.aggfunc 

1782 if val.args or val.kwargs: 

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

1784 x, *a, **kw 

1785 ) 

1786 converted_kwargs[key] = (val.column, aggfunc) 

1787 else: 

1788 converted_kwargs[key] = val 

1789 

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

1791 converted_kwargs 

1792 ) 

1793 

1794 assert func is not None 

1795 

1796 return relabeling, func, columns, order 

1797 

1798 

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

1800 """ 

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

1802 

1803 Parameters 

1804 ---------- 

1805 **kwargs : dict 

1806 

1807 Returns 

1808 ------- 

1809 bool 

1810 

1811 Examples 

1812 -------- 

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

1814 False 

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

1816 True 

1817 >>> is_multi_agg_with_relabel() 

1818 False 

1819 """ 

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

1821 len(kwargs) > 0 

1822 ) 

1823 

1824 

1825def normalize_keyword_aggregation( 

1826 kwargs: dict, 

1827) -> tuple[ 

1828 MutableMapping[Hashable, list[AggFuncTypeBase]], 

1829 tuple[str, ...], 

1830 npt.NDArray[np.intp], 

1831]: 

1832 """ 

1833 Normalize user-provided "named aggregation" kwargs. 

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

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

1836 

1837 Parameters 

1838 ---------- 

1839 kwargs : dict 

1840 

1841 Returns 

1842 ------- 

1843 aggspec : dict 

1844 The transformed kwargs. 

1845 columns : tuple[str, ...] 

1846 The user-provided keys. 

1847 col_idx_order : List[int] 

1848 List of columns indices. 

1849 

1850 Examples 

1851 -------- 

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

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

1854 """ 

1855 from pandas.core.indexes.base import Index 

1856 

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

1858 # process normally, then fixup the names. 

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

1860 aggspec = defaultdict(list) 

1861 order = [] 

1862 columns = tuple(kwargs.keys()) 

1863 

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

1865 aggspec[column].append(aggfunc) 

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

1867 

1868 # uniquify aggfunc name if duplicated in order list 

1869 uniquified_order = _make_unique_kwarg_list(order) 

1870 

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

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

1873 # based on index 

1874 aggspec_order = [ 

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

1876 for column, aggfuncs in aggspec.items() 

1877 for aggfunc in aggfuncs 

1878 ] 

1879 uniquified_aggspec = _make_unique_kwarg_list(aggspec_order) 

1880 

1881 # get the new index of columns by comparison 

1882 col_idx_order = Index(uniquified_aggspec).get_indexer(uniquified_order) 

1883 return aggspec, columns, col_idx_order 

1884 

1885 

1886def _make_unique_kwarg_list( 

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

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

1889 """ 

1890 Uniquify aggfunc name of the pairs in the order list 

1891 

1892 Examples: 

1893 -------- 

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

1895 >>> _make_unique_kwarg_list(kwarg_list) 

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

1897 """ 

1898 return [ 

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

1900 for i, pair in enumerate(seq) 

1901 ] 

1902 

1903 

1904def relabel_result( 

1905 result: DataFrame | Series, 

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

1907 columns: Iterable[Hashable], 

1908 order: Iterable[int], 

1909) -> dict[Hashable, Series]: 

1910 """ 

1911 Internal function to reorder result if relabelling is True for 

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

1913 

1914 Parameters: 

1915 ---------- 

1916 result: Result from aggregation 

1917 func: Dict of (column name, funcs) 

1918 columns: New columns name for relabelling 

1919 order: New order for relabelling 

1920 

1921 Examples 

1922 -------- 

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

1924 >>> result = pd.DataFrame( 

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

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

1927 ... ) 

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

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

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

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

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

1933 A C B 

1934 foo 2.0 NaN NaN 

1935 aab NaN 6.0 NaN 

1936 bar NaN NaN 4.0 

1937 dat NaN NaN 2.5 

1938 """ 

1939 from pandas.core.indexes.base import Index 

1940 

1941 reordered_indexes = [ 

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

1943 ] 

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

1945 idx = 0 

1946 

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

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

1949 s = result[col].dropna() 

1950 

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

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

1953 # C2 C1 

1954 # <lambda> 1 NaN 

1955 # amax NaN 4.0 

1956 # max NaN 4.0 

1957 # sum 18.0 6.0 

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

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

1960 # reorder the aggregated result for each column. 

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

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

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

1964 

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

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

1967 # A 

1968 # min 1.0 

1969 # mean 1.5 

1970 # mean 1.5 

1971 if reorder_mask: 

1972 fun = [ 

1973 com.get_callable_name(f) if not isinstance(f, str) else f for f in fun 

1974 ] 

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

1976 valid_idx = col_idx_order != -1 

1977 if valid_idx.any(): 

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

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

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

1981 if not s.empty: 

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

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

1984 idx = idx + len(fun) 

1985 return reordered_result_in_dict 

1986 

1987 

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

1989 from pandas import DataFrame 

1990 

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

1992 

1993 if relabeling: 

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

1995 # keep the order of new columns occurrence unchanged 

1996 

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

1998 # False, columns and order will be None. 

1999 assert columns is not None 

2000 assert order is not None 

2001 

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

2003 result = DataFrame(result_in_dict, index=columns) 

2004 

2005 return result 

2006 

2007 

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

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

2010# the type is: 

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

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

2013 

2014 

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

2016 """ 

2017 Possibly mangle a list of aggfuncs. 

2018 

2019 Parameters 

2020 ---------- 

2021 aggfuncs : Sequence 

2022 

2023 Returns 

2024 ------- 

2025 mangled: list-like 

2026 A new AggSpec sequence, where lambdas have been converted 

2027 to have unique names. 

2028 

2029 Notes 

2030 ----- 

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

2032 """ 

2033 if len(aggfuncs) <= 1: 

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

2035 return aggfuncs 

2036 i = 0 

2037 mangled_aggfuncs = [] 

2038 for aggfunc in aggfuncs: 

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

2040 aggfunc = partial(aggfunc) 

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

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

2043 i += 1 

2044 mangled_aggfuncs.append(aggfunc) 

2045 

2046 return mangled_aggfuncs 

2047 

2048 

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

2050 """ 

2051 Make new lambdas with unique names. 

2052 

2053 Parameters 

2054 ---------- 

2055 agg_spec : Any 

2056 An argument to GroupBy.agg. 

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

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

2059 with name-mangled lambdas. 

2060 

2061 Returns 

2062 ------- 

2063 mangled : Any 

2064 Same type as the input. 

2065 

2066 Examples 

2067 -------- 

2068 >>> maybe_mangle_lambdas("sum") 

2069 'sum' 

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

2071 [<function __main__.<lambda_0>, 

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

2073 """ 

2074 is_dict = is_dict_like(agg_spec) 

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

2076 return agg_spec 

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

2078 

2079 if is_dict: 

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

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

2082 mangled_aggfuncs = _managle_lambda_list(aggfuncs) 

2083 else: 

2084 mangled_aggfuncs = aggfuncs 

2085 

2086 mangled_aggspec[key] = mangled_aggfuncs 

2087 else: 

2088 mangled_aggspec = _managle_lambda_list(agg_spec) 

2089 

2090 return mangled_aggspec 

2091 

2092 

2093def validate_func_kwargs( 

2094 kwargs: dict, 

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

2096 """ 

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

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

2099 

2100 Parameters 

2101 ---------- 

2102 kwargs : dict 

2103 

2104 Returns 

2105 ------- 

2106 columns : List[str] 

2107 List of user-provided keys. 

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

2109 List of user-provided aggfuncs 

2110 

2111 Examples 

2112 -------- 

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

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

2115 """ 

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

2117 columns = list(kwargs) 

2118 func = [] 

2119 for col_func in kwargs.values(): 

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

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

2122 func.append(col_func) 

2123 if not columns: 

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

2125 raise TypeError(no_arg_message) 

2126 return columns, func 

2127 

2128 

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

2130 return isinstance(colg, ABCDataFrame) or ( 

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

2132 )