@@ -2015,10 +2015,14 @@ def _transform(self, func, *args, engine=None, engine_kwargs=None, **kwargs):
2015
2015
with com .temp_setattr (self , "as_index" , True ):
2016
2016
# GH#49834 - result needs groups in the index for
2017
2017
# _wrap_transform_fast_result
2018
- if engine is not None :
2019
- kwargs ["engine" ] = engine
2020
- kwargs ["engine_kwargs" ] = engine_kwargs
2021
- result = getattr (self , func )(* args , ** kwargs )
2018
+ if func in ["idxmin" , "idxmax" ]:
2019
+ func = cast (Literal ["idxmin" , "idxmax" ], func )
2020
+ result = self ._idxmax_idxmin (func , True , * args , ** kwargs )
2021
+ else :
2022
+ if engine is not None :
2023
+ kwargs ["engine" ] = engine
2024
+ kwargs ["engine_kwargs" ] = engine_kwargs
2025
+ result = getattr (self , func )(* args , ** kwargs )
2022
2026
2023
2027
return self ._wrap_transform_fast_result (result )
2024
2028
@@ -5720,6 +5724,113 @@ def sample(
5720
5724
sampled_indices = np .concatenate (sampled_indices )
5721
5725
return self ._selected_obj .take (sampled_indices , axis = self .axis )
5722
5726
5727
+ def _idxmax_idxmin (
5728
+ self ,
5729
+ how : Literal ["idxmax" , "idxmin" ],
5730
+ ignore_unobserved : bool = False ,
5731
+ axis : Axis | None | lib .NoDefault = lib .no_default ,
5732
+ skipna : bool = True ,
5733
+ numeric_only : bool = False ,
5734
+ ):
5735
+ """Compute idxmax/idxmin.
5736
+
5737
+ Parameters
5738
+ ----------
5739
+ how: {"idxmin", "idxmax"}
5740
+ Whether to compute idxmin or idxmax.
5741
+ axis : {{0 or 'index', 1 or 'columns'}}, default None
5742
+ The axis to use. 0 or 'index' for row-wise, 1 or 'columns' for column-wise.
5743
+ If axis is not provided, grouper's axis is used.
5744
+ numeric_only : bool, default False
5745
+ Include only float, int, boolean columns.
5746
+ skipna : bool, default True
5747
+ Exclude NA/null values. If an entire row/column is NA, the result
5748
+ will be NA.
5749
+ ignore_unobserved : bool, default False
5750
+ When True and an unobserved group is encountered, do not raise. This used
5751
+ for transform where unobserved groups do not play an impact on the result.
5752
+
5753
+ Returns
5754
+ -------
5755
+ Series or DataFrame
5756
+ idxmax or idxmin for the groupby operation.
5757
+ """
5758
+ if axis is not lib .no_default :
5759
+ if axis is None :
5760
+ axis = self .axis
5761
+ axis = self .obj ._get_axis_number (axis )
5762
+ self ._deprecate_axis (axis , how )
5763
+ else :
5764
+ axis = self .axis
5765
+
5766
+ if not self .observed and any (
5767
+ ping ._passed_categorical for ping in self .grouper .groupings
5768
+ ):
5769
+ expected_len = np .prod (
5770
+ [len (ping .group_index ) for ping in self .grouper .groupings ]
5771
+ )
5772
+ if len (self .grouper .groupings ) == 1 :
5773
+ result_len = len (self .grouper .groupings [0 ].grouping_vector .unique ())
5774
+ else :
5775
+ # result_index only contains observed groups in this case
5776
+ result_len = len (self .grouper .result_index )
5777
+ assert result_len <= expected_len
5778
+ has_unobserved = result_len < expected_len
5779
+
5780
+ raise_err : bool | np .bool_ = not ignore_unobserved and has_unobserved
5781
+ # Only raise an error if there are columns to compute; otherwise we return
5782
+ # an empty DataFrame with an index (possibly including unobserved) but no
5783
+ # columns
5784
+ data = self ._obj_with_exclusions
5785
+ if raise_err and isinstance (data , DataFrame ):
5786
+ if numeric_only :
5787
+ data = data ._get_numeric_data ()
5788
+ raise_err = len (data .columns ) > 0
5789
+ else :
5790
+ raise_err = False
5791
+ if raise_err :
5792
+ raise ValueError (
5793
+ f"Can't get { how } of an empty group due to unobserved categories. "
5794
+ "Specify observed=True in groupby instead."
5795
+ )
5796
+
5797
+ try :
5798
+ if self .obj .ndim == 1 :
5799
+ result = self ._op_via_apply (how , skipna = skipna )
5800
+ else :
5801
+
5802
+ def func (df ):
5803
+ method = getattr (df , how )
5804
+ return method (axis = axis , skipna = skipna , numeric_only = numeric_only )
5805
+
5806
+ func .__name__ = how
5807
+ result = self ._python_apply_general (
5808
+ func , self ._obj_with_exclusions , not_indexed_same = True
5809
+ )
5810
+ except ValueError as err :
5811
+ name = "argmax" if how == "idxmax" else "argmin"
5812
+ if f"attempt to get { name } of an empty sequence" in str (err ):
5813
+ raise ValueError (
5814
+ f"Can't get { how } of an empty group due to unobserved categories. "
5815
+ "Specify observed=True in groupby instead."
5816
+ ) from None
5817
+ raise
5818
+
5819
+ result = result .astype (self .obj .index .dtype ) if result .empty else result
5820
+
5821
+ if not skipna :
5822
+ has_na_value = result .isnull ().any (axis = None )
5823
+ if has_na_value :
5824
+ warnings .warn (
5825
+ f"The behavior of { type (self ).__name__ } .{ how } with all-NA "
5826
+ "values, or any-NA and skipna=False, is deprecated. In a future "
5827
+ "version this will raise ValueError" ,
5828
+ FutureWarning ,
5829
+ stacklevel = find_stack_level (),
5830
+ )
5831
+
5832
+ return result
5833
+
5723
5834
5724
5835
@doc (GroupBy )
5725
5836
def get_groupby (
0 commit comments