ENH: Allow users to definite their own window bound calculations in rolling
State: closed

Currently `rolling` allows integer and offset as `window` arguments. This PR allows users to define a custom method for calculating window boundaries by passing a subclass of a new `BaseIndexer`

```
from pandas.api.indexers import BaseIndexer

class MyWindowIndexer(BaseIndexer):

    def get_window_bounds(self, ...):
          ....
          return (start, end)

indexer = MyWindowIndexer(arg1, arg2, ...)
result = df.rolling(indexer).mean()
```
Could help address more custom rolling window operations as described in #26959,  #13969, #25510, #24295

- [x] documented new feature
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Labels:
name: Enhancement
name: Window
description: rolling, ewma, expanding

Comments:
so far the only thing in indexers.pyx for which being cython really matters is VariableWindowIndexer.build, which is a static method, so could just as easily be a module-level function.  Do these classes need to be in the cython code?
>  Do these classes need to be in the cython code?

Good point @jbrockmendel. I can move these indexers to `pandas/core/window/indexers.py` and just import "VariableWindowIndexer.build" as a function from `pandas/_libs/window/indexers.py`
Hello @mroeschke! Thanks for updating this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:













There are currently no PEP 8 issues detected in this Pull Request. Cheers! :beers: 

##### Comment last updated at 2019-12-04 21:56:57 UTC
Note: I will need to write another form of the cython aggregation functions that is different from the "fixed" and "variable" versions we have now as this simple example doesn't work:

```
In [20]: df
Out[20]:
   values
0       0
1       1
2       2
3       3
4       4

In [21]:     class CustomIndexer(BaseIndexer):
    ...:         def get_window_bounds(self, num_values, min_periods, center, closed, win_type):
    ...:             return np.array([0, 1, 0, 3, 0]), np.array([1, 2, 5, 4, 5])
    ...:

In [22]: indexer = CustomIndexer()

In [23]: result = df.rolling(indexer).sum()

In [24]: result
Out[24]:
   values
0     0.0
1     1.0
2    10.0
3     7.0   # should be 3
4    11.0  # should be 10
```
> not using ExpandingIndexer right now, correct?

@jreback. Correct, just exists for testing purposes.
All green @jreback 

Collecting followups:
https://github.com/pandas-dev/pandas/pull/29878#discussion_r353772843
thanks @mroeschke very nice
Apologies for the very late PR comment, but shouldn't `rolling()` default to the `VariableWindowIndexer` when the passed-in offset is a variable-frequency offset, rather than throwing as it does now?
> Apologies for the very late PR comment, but shouldn't `rolling()` default to the `VariableWindowIndexer` when the passed-in offset is a variable-frequency offset, rather than throwing as it does now?

Could you show an example?
```
Python 3.8.13 (default, Mar 28 2022, 11:38:47) 
Type 'copyright', 'credits' or 'license' for more information
IPython 8.3.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import pandas as pd
   ...: from pandas.tseries.holiday import USFederalHolidayCalendar
   ...: 
   ...: s = pd.Series(range(10), pd.date_range("7/1/2022", "7/10/2022"))
   ...: bday = pd.tseries.offsets.CustomBusinessDay(n=1, calendar=USFederalHolidayCalendar())
   ...: 

In [2]: s.rolling(bday).median()
ValueError: <CustomBusinessDay> is a non-fixed frequency

In [3]: s.rolling(pd.api.indexers.VariableOffsetWindowIndexer(index=s.index, offset=bday)).median()
Out[3]: 
2022-07-01    0.0
2022-07-02    1.0
2022-07-03    1.5
2022-07-04    2.0
2022-07-05    2.5
2022-07-06    5.0
2022-07-07    6.0
2022-07-08    7.0
2022-07-09    8.0
2022-07-10    8.5
Freq: D, dtype: float64
```
`VariableOffsetWindowIndexer` is not as performant as the prior implementation for fixed offsets, so I don't think the default could be switched. `VariableOffsetWindowIndexer` was an enhancement to support non-fixed offsets
I'm not suggesting changing the default. My suggestion is that if the "X is a non-fixed frequency" exception is being thrown, it has detected that the passed offset is non-fixed, and thus could intelligently fall back to the variable indexer automatically. I'd think a typical user who is passing in a non-fixed offset is doing so deliberately and isn't wanting an exception.
I think a pull request to improve exception message to suggest using `VariableOffsetWindowIndexer` would be appreciated. Generally fallback & non-explicit behaviors are preferred to avoid surprises.
The code located here just raises:

https://github.com/pandas-dev/pandas/blob/0a8b45f5a2e6f886d7d5d7ad6956750e21e29978/pandas/core/window/rolling.py#L1831-L1838

But this could just be another clause right here that sets `self.window` to a `VariableOffsetWindowIndexer` that wraps the original `self.window`, then from then on it's pretty much the same thing as passing in a `BaseIndexer`, except now the onus isn't on the user to have to do this:

https://github.com/pandas-dev/pandas/blob/0a8b45f5a2e6f886d7d5d7ad6956750e21e29978/pandas/core/window/rolling.py#L1859-L1861