PERF/BUG: use masked algo in groupby cummin and cummax
State: closed

Potential first step towards #37493

ASV's (on `N = 5_000_000` instead of `N = 1_000_000` on the added benchmark to get more stable results):
<details>

```
       before           after         ratio
     [3a973b6d]       [2fa80ad0]
     <master>         <perf/masked_cummin/max>
-        297±10ms          241±9ms     0.81  groupby.CumminMax.time_frame_transform('Float64', 'cummax')
-         284±6ms          240±7ms     0.85  groupby.CumminMax.time_frame_transform('Float64', 'cummin')
-        761±60ms          393±4ms     0.52  groupby.CumminMax.time_frame_transform('Int64', 'cummax')
-        753±50ms          390±3ms     0.52  groupby.CumminMax.time_frame_transform('Int64', 'cummin')
         481±20ms         474±10ms     0.99  groupby.CumminMax.time_frame_transform('float64', 'cummax')
         461±10ms         459±10ms     1.00  groupby.CumminMax.time_frame_transform('float64', 'cummin')
         706±40ms         732±50ms     1.04  groupby.CumminMax.time_frame_transform('int64', 'cummax')
         730±20ms         674±20ms     0.92  groupby.CumminMax.time_frame_transform('int64', 'cummin')
-        685±30ms          214±5ms     0.31  groupby.CumminMax.time_frame_transform_many_nulls('Float64', 'cummax')
-        698±20ms          225±4ms     0.32  groupby.CumminMax.time_frame_transform_many_nulls('Float64', 'cummin')
-        811±50ms          204±6ms     0.25  groupby.CumminMax.time_frame_transform_many_nulls('Int64', 'cummax')
-        804±50ms          207±6ms     0.26  groupby.CumminMax.time_frame_transform_many_nulls('Int64', 'cummin')
         444±20ms          447±4ms     1.01  groupby.CumminMax.time_frame_transform_many_nulls('float64', 'cummax')
         447±20ms         459±20ms     1.03  groupby.CumminMax.time_frame_transform_many_nulls('float64', 'cummin')
        1.41±0.1s       1.33±0.01s     0.94  groupby.CumminMax.time_frame_transform_many_nulls('int64', 'cummax')
       1.41±0.09s          1.33±0s     0.94  groupby.CumminMax.time_frame_transform_many_nulls('int64', 'cummin')
```

</details>


Labels:
name: Groupby
name: Performance
description: Memory or execution speed performance
name: NA - MaskedArrays
description: Related to pd.NA and nullable extension arrays

Comments:
> on a 5x greater N than the benchmark below to get more stable results

im unclear on what this means.  can you rephrase?
> > on a 5x greater N than the benchmark below to get more stable results
> 
> im unclear on what this means. can you rephrase?

Sorry about that definitely confusing, updated comment 
Not directly related to this PR, but something that I noticed in the implementation while reviewing this: the cummin/cummax "propagate" NAs:

```
In [13]: s = pd.Series([1, 3, np.nan, 2, 4])

In [14]: s
Out[14]: 
0    1.0
1    3.0
2    NaN
3    2.0
4    4.0
dtype: float64

In [15]: s.groupby([1] * 5).cummax()
Out[15]: 
0    1.0
1    3.0
2    NaN
3    3.0
4    4.0
dtype: float64
```

But since this is a *cumulative* maximum, that doesn't make much sense to me (the third value is the max of [1.0, 3.0, NaN], which should be 3.0 in our skipna=True logic). 
So I would rather expect:

```
Out[15]: 
0    1.0
1    3.0
2    3.0
3    3.0
4    4.0
dtype: float64
```

Does somebody know if this is an explicit design? Or whether this was discussed before?
> Does somebody know if this is an explicit design? Or whether this was discussed before?

Also seems like for consistency with other transforms like `cumsum`, `cummin/max` should take a `skipna` argument. Right now `skipna` would be ignored: 
```
>>> s.groupby([1] * 5).cummax(skipna=False)
0    1.0
1    3.0
2    NaN
3    3.0
4    4.0
```
but should be able to match the `numpy` behavior of 
```
>>> np.maximum.accumulate([1, 3, np.nan, 2, 4])
array([ 1.,  3., nan, nan, nan])
```
assume this depends on https://github.com/pandas-dev/pandas/pull/40767
> assume this depends on #40767

To some extent. The perf improvement for the case of not many NULL's will be captured by #40767 instead because of the avoidance of checks like `x == iNaT`. But the issue of nullable ints losing precision by casting to float will still be present, and this implementation will also improve performance for the nullable case.
@jreback can you take another look at this?
> > if the goal is for nullable types to become the default in pandas
>
> This decision has not been made.

@jbrockmendel we have the *intention*, though. Our roadmap says on that topic: *"We want to eventually make the new semantics the default."* (at the level of abstraction of the "semantics", but currently for numeric dtypes that in practice means using masks)
> @jreback can you take another look at this?

Don't want to take up any more reviewer time just yet - want to take a quick crack at seeing what https://github.com/pandas-dev/pandas/pull/40651#discussion_r613607848 might look like first.  Wouldn't want everyone to spend a bunch of time reviewing this, then have it end up being made obsolete by a more general solution.
(I personally think it makes more sense to merge this first, and then have a follow-up PR to refactor how things are called/passed in groupb/ops.py for masked arrays, focusing specifically on that without the cummin/cummax changes here. But leave it to @jbrockmendel and @jreback to state their preference)
yep onboard here to merge this (needs a rebase for some conflicts).
ditto
have rebased
can you merge master once more (small conflict), but want to have CI run.
thanks @mzeitlin11 

happy to take followups on cleaning