Repository: pandas-dev/pandas
Pull request: ENH: Implement weighted rolling var and std
- [x] closes #26597
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Used West's online [algorithm](https://dl.acm.org/citation.cfm?id=359153). [Here](http://www.nowozin.net/sebastian/blog/streaming-mean-and-variance-computation.html) is an explanation of the algorithm and link to pdf of the paper. Tested implementation with `win_type="boxcar"` comparing it with the result of `Rolling.std`.

Additionaly, `_get_window` function is split into two. `_get_kwargs` function is used to split kwargs for window function and rolling function (since std and var takes an additional `ddof` argument).

Shared docs for `std` and `var` are moved to be used with `Window` functions


Problem statement:
Rolling standard deviation fails when used with win_type
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
df = pd.DataFrame({'a': range(6)})
df['a'].rolling(3, win_type='blackman').agg(['mean', 'std'])
```
#### Problem description

When calculating rolling aggregations with a window function, sometimes there is an error which is quite hard to understand. I think it might be that the std() aggregation is not compatible with certain window types, or something like that- but it's not clear from the error messages or documentation what is going wrong.

Here is the stack trace that I see when I run the code sample above:
<details><pre><code>
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-48-41cc0256ede1> in <module>
      3 df = pd.DataFrame({'a': range(6)})
      4 
----> 5 print(df['a'].rolling(3, win_type='blackman').agg(['mean', 'std']))

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\window.py in aggregate(self, arg, *args, **kwargs)
    741     @Appender(_shared_docs['aggregate'])
    742     def aggregate(self, arg, *args, **kwargs):
--> 743         result, how = self._aggregate(arg, *args, **kwargs)
    744         if result is None:
    745 

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\base.py in _aggregate(self, arg, *args, **kwargs)
    557             return self._aggregate_multiple_funcs(arg,
    558                                                   _level=_level,
--> 559                                                   _axis=_axis), None
    560         else:
    561             result = None

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\base.py in _aggregate_multiple_funcs(self, arg, _level, _axis)
    587                 try:
    588                     colg = self._gotitem(obj.name, ndim=1, subset=obj)
--> 589                     results.append(colg.aggregate(a))
    590 
    591                     # make sure we find a good name

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\window.py in aggregate(self, arg, *args, **kwargs)
    741     @Appender(_shared_docs['aggregate'])
    742     def aggregate(self, arg, *args, **kwargs):
--> 743         result, how = self._aggregate(arg, *args, **kwargs)
    744         if result is None:
    745 

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\base.py in _aggregate(self, arg, *args, **kwargs)
    354         if isinstance(arg, compat.string_types):
    355             return self._try_aggregate_string_function(arg, *args,
--> 356                                                        **kwargs), None
    357 
    358         if isinstance(arg, dict):

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\base.py in _try_aggregate_string_function(self, arg, *args, **kwargs)
    321         f = getattr(np, arg, None)
    322         if f is not None:
--> 323             return f(self, *args, **kwargs)
    324 
    325         raise ValueError("{arg} is an unknown string function".format(arg=arg))

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\numpy\core\fromnumeric.py in std(a, axis, dtype, out, ddof, keepdims)
   3240 
   3241     return _methods._std(a, axis=axis, dtype=dtype, out=out, ddof=ddof,
-> 3242                          **kwargs)
   3243 
   3244 

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\numpy\core\_methods.py in _std(a, axis, dtype, out, ddof, keepdims)
    138 def _std(a, axis=None, dtype=None, out=None, ddof=0, keepdims=False):
    139     ret = _var(a, axis=axis, dtype=dtype, out=out, ddof=ddof,
--> 140                keepdims=keepdims)
    141 
    142     if isinstance(ret, mu.ndarray):

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\numpy\core\_methods.py in _var(a, axis, dtype, out, ddof, keepdims)
    110                 arrmean, rcount, out=arrmean, casting='unsafe', subok=False)
    111     else:
--> 112         arrmean = arrmean.dtype.type(arrmean / rcount)
    113 
    114     # Compute sum of squared deviations from mean

~\AppData\Local\Continuum\anaconda3\envs\petropy\lib\site-packages\pandas\core\window.py in __getattr__(self, attr)
    148 
    149         raise AttributeError("%r object has no attribute %r" %
--> 150                              (type(self).__name__, attr))
    151 
    152     def _dir_additions(self):

AttributeError: 'Window' object has no attribute 'dtype'


</details></pre></code>


#### Expected Output

When I run the code above without the win_type paramater, everything works fine:

```python
import pandas as pd
df = pd.DataFrame({'a': range(6)})
df['a'].rolling(3, win_type=None).agg(['mean', 'std'])
```
Result:
<pre><code>
   mean  std
0   NaN  NaN
1   NaN  NaN
2   1.0  1.0
3   2.0  1.0
4   3.0  1.0
5   4.0  1.0
</pre></code>

Thanks in advance for any tips!

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]
INSTALLED VERSIONS
------------------
commit: None
python: 3.7.3.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 158 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.24.2
pytest: 4.3.1
pip: 19.0.3
setuptools: 40.8.0
Cython: 0.29.6
numpy: 1.16.2
scipy: 1.2.1
pyarrow: None
xarray: None
IPython: 7.4.0
sphinx: 1.8.5
patsy: 0.5.1
dateutil: 2.8.0
pytz: 2018.9
blosc: None
bottleneck: 1.2.1
tables: None
numexpr: 2.6.9
feather: None
matplotlib: 3.0.3
openpyxl: 2.6.1
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.5
lxml.etree: 4.3.2
bs4: 4.7.1
html5lib: 1.0.1
sqlalchemy: 1.3.1
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None
</details>

Hints from issue discussion:
> think it might be that the std() aggregation is not compatible with certain window types


That seems to be the case.

```python
In [23]: df.rolling(3, win_type='blackman').agg('std')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-23-0df3ddf74018> in <module>
----> 1 df.rolling(3, win_type='blackman').agg('std')

~/sandbox/pandas/pandas/core/window.py in aggregate(self, arg, *args, **kwargs)
    748     @Appender(_shared_docs['aggregate'])
    749     def aggregate(self, arg, *args, **kwargs):
--> 750         result, how = self._aggregate(arg, *args, **kwargs)
    751         if result is None:
    752

~/sandbox/pandas/pandas/core/base.py in _aggregate(self, arg, *args, **kwargs)
    328         if isinstance(arg, str):
    329             return self._try_aggregate_string_function(arg, *args,
--> 330                                                        **kwargs), None
    331
    332         if isinstance(arg, dict):

~/sandbox/pandas/pandas/core/base.py in _try_aggregate_string_function(self, arg, *args, **kwargs)
    295         f = getattr(np, arg, None)
    296         if f is not None:
--> 297             return f(self, *args, **kwargs)
    298
    299         raise ValueError("{arg} is an unknown string function".format(arg=arg))

<__array_function__ internals> in std(*args, **kwargs)

~/sandbox/numpy/numpy/core/fromnumeric.py in std(a, axis, dtype, out, ddof, keepdims)
   3354
   3355     return _methods._std(a, axis=axis, dtype=dtype, out=out, ddof=ddof,
-> 3356                          **kwargs)
   3357
   3358

~/sandbox/numpy/numpy/core/_methods.py in _std(a, axis, dtype, out, ddof, keepdims)
    214 def _std(a, axis=None, dtype=None, out=None, ddof=0, keepdims=False):
    215     ret = _var(a, axis=axis, dtype=dtype, out=out, ddof=ddof,
--> 216                keepdims=keepdims)
    217
    218     if isinstance(ret, mu.ndarray):

~/sandbox/numpy/numpy/core/_methods.py in _var(a, axis, dtype, out, ddof, keepdims)
    185                 arrmean, rcount, out=arrmean, casting='unsafe', subok=False)
    186     else:
--> 187         arrmean = arrmean.dtype.type(arrmean / rcount)
    188
    189     # Compute sum of squared deviations from mean

~/sandbox/pandas/pandas/core/window.py in __getattr__(self, attr)
    146
    147         raise AttributeError("%r object has no attribute %r" %
--> 148                              (type(self).__name__, attr))
    149
    150     def _dir_additions(self):

AttributeError: 'Window' object has no attribute 'dtype'

```

it'd be good to verify if that's intentional, or whether it's an implementation detail. If it's intentional, then we should be able to verify which aggfuncs are compatible with which window types, and raise an informative error message before attempting the aggregation.

@Connossor are you interested in doing that investigation?
Hi @TomAugspurger,

Thanks for the response, and yep agree with your plan.

I did my best to look through the codebase, and it's not clear to me which aggfuncs are supposed to be compatible with which window types. It seems from here that if a string aggregation such as "std", then if it exists as a function in numpy it gets used:

https://github.com/pandas-dev/pandas/blob/cb00deb94500205fcb27a33cc1d0df79a9727f8b/pandas/core/base.py#L299-L323

From some research I think a std() ought to be useable with any window type. A formula for a weighted standard deviation is here: 
https://www.itl.nist.gov/div898/software/dataplot/refman2/ch2/weightsd.pdf . 
This issue may be related to issue #26462, where the mean() aggregation seems to have different behaviour for a  rolling window versus a grouby-rolling window.
@TomAugspurger I've investigated as you suggested, here is what I think is happening:

This function seems to govern what class is actually used: we get a `pandas.core.window.Window` object if the win_type parameter is set, otherwise a  `pandas.core.window.Rolling` object which seems to a be effectively a Window with uniform weights.

https://github.com/pandas-dev/pandas/blob/addc5fcd95064b765d4ee4260304d44822fdee3b/pandas/core/window.py#L2626-L2633


Then, when the `"std"` aggregation is called the `_try_aggregate_string_function()` method linked above looks to see of the object has a `.std()` method, and if none is present falls back to a numpy function.

The Window class has no `.std()` method. It falls back to the numpy implementation of std() which fails, as per the example above.

On the other hand, the `Rolling` class has a std() method which works just fine.

On a related note: the `pandas.core.window.RollingGroupby` class seems to inherit the mean() method from the `Rolling` class, and hence completely ignores the win_type paramater. 

Overall, it looks like we need two fixes:

1.  Raise an informative error if the "std" aggregation is used on a Window object, or alternatively implement a weighted standard deviation as described above.
2.  Ensure consistent behaviour with groupby. `Rolling` is to `Window` as `RollingGroupby` is to `WindowGroupby`, so perhaps we need a `WindowGroupby` class?

What do you think?

Your number 1. sounds reasonable.

Not sure about 2. I'm not that familiar with this code.
I want to implement weighted variance function (using proposed algo [here](https://people.xiph.org/~tterribe/tmp/homs/West79-_Updating_Mean_and_Variance_Estimates-_An_Improved_Method.pdf)) but `Window` uses single [function](https://github.com/pandas-dev/pandas/blob/master/pandas/_libs/window.pyx#L1678-L1740) with an argument `avg` (if it is True mean else sum) for calculating both `mean` and `sum`. I think the function should be splitted into two at first. What do you think? @jreback @WillAyd
sure this could be changed

Commit message: Pass updated kwargs to scipy's get_window

Patch to review:
diff --git a/doc/source/whatsnew/v0.25.1.rst b/doc/source/whatsnew/v0.25.1.rst
index fb67decb46b64..c6a8c5e67ea3b 100644
--- a/doc/source/whatsnew/v0.25.1.rst
+++ b/doc/source/whatsnew/v0.25.1.rst
@@ -16,7 +16,7 @@ Enhancements
 Other enhancements
 ^^^^^^^^^^^^^^^^^^
 
--
+- Implemented :meth:`pandas.core.window.Window.var` and :meth:`pandas.core.window.Window.std` functions (:issue:`26597`)
 -
 -
 
diff --git a/pandas/_libs/window.pyx b/pandas/_libs/window.pyx
index 8de593ce36c86..c8dd90845c64e 100644
--- a/pandas/_libs/window.pyx
+++ b/pandas/_libs/window.pyx
@@ -1754,6 +1754,135 @@ def _roll_weighted_sum_mean(float64_t[:] values, float64_t[:] weights,
     return output
 
 
+# ----------------------------------------------------------------------
+# Rolling var for weighted window
+
+
+cdef inline float64_t calc_weighted_var(float64_t t,
+                                        float64_t sum_w,
+                                        Py_ssize_t win,
+                                        int ddof,
+                                        float64_t nobs,
+                                        int64_t minp) nogil:
+    cdef:
+        float64_t result
+
+    # Variance is unchanged if no observation is added or removed
+    if (nobs >= minp) and (nobs > ddof):
+
+        # pathological case
+        if nobs == 1:
+            result = 0
+        else:
+            result = t * win / ((win - <float64_t>ddof) * sum_w)
+            if result < 0:
+                result = 0
+    else:
+        result = NaN
+
+    return result
+
+
+cdef inline void add_weighted_var(float64_t val,
+                                  float64_t w,
+                                  float64_t *t,
+                                  float64_t *sum_w,
+                                  float64_t *mean,
+                                  float64_t *nobs) nogil:
+    cdef:
+        float64_t temp, q, r
+
+    if isnan(val):
+        return
+
+    nobs[0] = nobs[0] + 1
+
+    q = val - mean[0]
+    temp = sum_w[0] + w
+    r = q * w / temp
+
+    mean[0] = mean[0] + r
+    t[0] = t[0] + r * sum_w[0] * q
+    sum_w[0] = temp
+
+
+cdef inline void remove_weighted_var(float64_t val,
+                                     float64_t w,
+                                     float64_t *t,
+                                     float64_t *sum_w,
+                                     float64_t *mean,
+                                     float64_t *nobs) nogil:
+    cdef:
+        float64_t temp, q, r
+
+    if notnan(val):
+        nobs[0] = nobs[0] - 1
+
+        if nobs[0]:
+            q = val - mean[0]
+            temp = sum_w[0] - w
+            r = q * w / temp
+
+            mean[0] = mean[0] - r
+            t[0] = t[0] - r * sum_w[0] * q
+            sum_w[0] = temp
+
+        else:
+            t[0] = 0
+            sum_w[0] = 0
+            mean[0] = 0
+
+
+def roll_weighted_var(float64_t[:] values, float64_t[:] weights,
+                      int64_t minp, int ddof):
+    """
+    Calculates weighted rolling variance using West's online algorithm
+
+    Paper: https://dl.acm.org/citation.cfm?id=359153
+    """
+    cdef:
+        float64_t t = 0, sum_w = 0, mean = 0, nobs = 0
+        float64_t val, pre_val, w, pre_w
+        Py_ssize_t i, n, win_n
+        float64_t[:] output
+
+    n = len(values)
+    win_n = len(weights)
+    output = np.empty(n, dtype=float)
+
+    with nogil:
+
+        for i in range(win_n):
+            add_weighted_var(values[i], weights[i], &t,
+                             &sum_w, &mean, &nobs)
+
+            output[i] = calc_weighted_var(t, sum_w, win_n,
+                                          ddof, nobs, minp)
+
+        for i in range(win_n, n):
+            val = values[i]
+            pre_val = values[i - win_n]
+
+            w = weights[i % win_n]
+            pre_w = weights[(i - win_n) % win_n]
+
+            if notnan(val):
+                if pre_val == pre_val:
+                    remove_weighted_var(pre_val, pre_w, &t,
+                                        &sum_w, &mean, &nobs)
+
+                add_weighted_var(val, w, &t, &sum_w, &mean, &nobs)
+
+            elif pre_val == pre_val:
+                remove_weighted_var(pre_val, pre_w, &t,
+                                    &sum_w, &mean, &nobs)
+
+            output[i] = calc_weighted_var(t, sum_w, win_n,
+                                          ddof, nobs, minp)
+
+    return output
+
+
 # ----------------------------------------------------------------------
 # Exponentially weighted moving average
 
diff --git a/pandas/core/window.py b/pandas/core/window.py
index 2199daa743655..551ac1a229cfa 100644
--- a/pandas/core/window.py
+++ b/pandas/core/window.py
@@ -5,7 +5,7 @@
 from collections import defaultdict
 from datetime import timedelta
 from textwrap import dedent
-from typing import Callable, List, Optional, Set, Union
+from typing import Callable, List, Optional, Set, Tuple, Union
 import warnings
 
 import numpy as np
@@ -173,6 +173,17 @@ def __getattr__(self, attr):
     def _dir_additions(self):
         return self.obj._dir_additions()
 
+    def _get_kwargs(self, **kwargs) -> Tuple[dict, dict]:
+        """
+        Separate kwargs for rolling and window functions.
+
+        Returns
+        -------
+        tuple of (roll_kwargs : dict, window_kwargs : dict)
+            kwargs for rolling and window function
+        """
+        return kwargs, dict()
+
     def _get_window(self, other=None, **kwargs) -> int:
         """
         Returns window lenght
@@ -405,6 +416,8 @@ def _apply(
         -------
         y : type of input
         """
+        roll_kwargs, window_kwargs = self._get_kwargs(**kwargs)
+
         if center is None:
             center = self.center
 
@@ -412,7 +425,7 @@ def _apply(
             check_minp = _use_window
 
         if window is None:
-            window = self._get_window(**kwargs)
+            window = self._get_window(**window_kwargs)
 
         blocks, obj = self._create_blocks()
         block_list = list(blocks)
@@ -445,7 +458,9 @@ def _apply(
                         "in libwindow.{func}".format(func=func)
                     )
 
-                func = self._get_roll_func(cfunc, check_minp, index_as_array, **kwargs)
+                func = self._get_roll_func(
+                    cfunc, check_minp, index_as_array, **roll_kwargs
+                )
 
             # calculation function
             if center:
@@ -612,6 +627,126 @@ def aggregate(self, func, *args, **kwargs):
     """
     )
 
+    _shared_docs["var"] = dedent(
+        """
+    Calculate unbiased %(name)s variance.
+
+    Normalized by N-1 by default. This can be changed using the `ddof`
+    argument.
+
+    Parameters
+    ----------
+    ddof : int, default 1
+        Delta Degrees of Freedom.  The divisor used in calculations
+        is ``N - ddof``, where ``N`` represents the number of elements.
+    *args, **kwargs
+        For NumPy compatibility. No additional arguments are used.
+
+    Returns
+    -------
+    Series or DataFrame
+        Returns the same object type as the caller of the %(name)s calculation.
+
+    See Also
+    --------
+    Series.%(name)s : Calling object with Series data.
+    DataFrame.%(name)s : Calling object with DataFrames.
+    Series.var : Equivalent method for Series.
+    DataFrame.var : Equivalent method for DataFrame.
+    numpy.var : Equivalent method for Numpy array.
+
+    Notes
+    -----
+    The default `ddof` of 1 used in :meth:`Series.var` is different than the
+    default `ddof` of 0 in :func:`numpy.var`.
+
+    A minimum of 1 period is required for the rolling calculation.
+
+    Examples
+    --------
+    >>> s = pd.Series([5, 5, 6, 7, 5, 5, 5])
+    >>> s.rolling(3).var()
+    0         NaN
+    1         NaN
+    2    0.333333
+    3    1.000000
+    4    1.000000
+    5    1.333333
+    6    0.000000
+    dtype: float64
+
+    >>> s.expanding(3).var()
+    0         NaN
+    1         NaN
+    2    0.333333
+    3    0.916667
+    4    0.800000
+    5    0.700000
+    6    0.619048
+    dtype: float64
+    """
+    )
+
+    _shared_docs["std"] = dedent(
+        """
+    Calculate %(name)s standard deviation.
+
+    Normalized by N-1 by default. This can be changed using the `ddof`
+    argument.
+
+    Parameters
+    ----------
+    ddof : int, default 1
+        Delta Degrees of Freedom.  The divisor used in calculations
+        is ``N - ddof``, where ``N`` represents the number of elements.
+    *args, **kwargs
+        For NumPy compatibility. No additional arguments are used.
+
+    Returns
+    -------
+    Series or DataFrame
+        Returns the same object type as the caller of the %(name)s calculation.
+
+    See Also
+    --------
+    Series.%(name)s : Calling object with Series data.
+    DataFrame.%(name)s : Calling object with DataFrames.
+    Series.std : Equivalent method for Series.
+    DataFrame.std : Equivalent method for DataFrame.
+    numpy.std : Equivalent method for Numpy array.
+
+    Notes
+    -----
+    The default `ddof` of 1 used in Series.std is different than the default
+    `ddof` of 0 in numpy.std.
+
+    A minimum of one period is required for the rolling calculation.
+
+    Examples
+    --------
+    >>> s = pd.Series([5, 5, 6, 7, 5, 5, 5])
+    >>> s.rolling(3).std()
+    0         NaN
+    1         NaN
+    2    0.577350
+    3    1.000000
+    4    1.000000
+    5    1.154701
+    6    0.000000
+    dtype: float64
+
+    >>> s.expanding(3).std()
+    0         NaN
+    1         NaN
+    2    0.577350
+    3    0.957427
+    4    0.894427
+    5    0.836660
+    6    0.786796
+    dtype: float64
+    """
+    )
+
 
 class Window(_Window):
     """
@@ -785,10 +920,52 @@ def validate(self):
         else:
             raise ValueError("Invalid window {0}".format(window))
 
+    def _get_kwargs(self, **kwargs) -> Tuple[dict, dict]:
+        """
+        Validate arguments for the window function, then separate
+        them from arguments of rolling function.
+
+        Returns
+        -------
+        tuple of (roll_kwargs : dict, window_kwargs : dict)
+            kwargs for rolling and window function
+        """
+        # the below may pop from kwargs
+        def _validate_win_type(win_type, kwargs):
+            arg_map = {
+                "kaiser": ["beta"],
+                "gaussian": ["std"],
+                "general_gaussian": ["power", "width"],
+                "slepian": ["width"],
+                "exponential": ["tau"],
+            }
+
+            if win_type in arg_map:
+                win_args = _pop_args(win_type, arg_map[win_type], kwargs)
+                if win_type == "exponential":
+                    # exponential window requires the first arg (center)
+                    # to be set to None (necessary for symmetric window)
+                    win_args.insert(0, None)
+
+                return tuple([win_type] + win_args)
+
+            return win_type
+
+        def _pop_args(win_type, arg_names, kwargs):
+            msg = "%s window requires %%s" % win_type
+            all_args = []
+            for n in arg_names:
+                if n not in kwargs:
+                    raise ValueError(msg % n)
+                all_args.append(kwargs.pop(n))
+            return all_args
+
+        win_type = _validate_win_type(self.win_type, kwargs)
+        return kwargs, dict(window=win_type)
+
     def _get_window(self, other=None, **kwargs) -> np.ndarray:
         """
-        Provide validation for the window type, return the window
-        which has already been validated.
+        Get the window, weights.
 
         Parameters
         ----------
@@ -807,46 +984,16 @@ def _get_window(self, other=None, **kwargs) -> np.ndarray:
         elif is_integer(window):
             import scipy.signal as sig
 
-            # the below may pop from kwargs
-            def _validate_win_type(win_type, kwargs):
-                arg_map = {
-                    "kaiser": ["beta"],
-                    "gaussian": ["std"],
-                    "general_gaussian": ["power", "width"],
-                    "slepian": ["width"],
-                    "exponential": ["tau"],
-                }
-
-                if win_type in arg_map:
-                    win_args = _pop_args(win_type, arg_map[win_type], kwargs)
-                    if win_type == "exponential":
-                        # exponential window requires the first arg (center)
-                        # to be set to None (necessary for symmetric window)
-                        win_args.insert(0, None)
-
-                    return tuple([win_type] + win_args)
-
-                return win_type
-
-            def _pop_args(win_type, arg_names, kwargs):
-                msg = "%s window requires %%s" % win_type
-                all_args = []
-                for n in arg_names:
-                    if n not in kwargs:
-                        raise ValueError(msg % n)
-                    all_args.append(kwargs.pop(n))
-                return all_args
-
-            win_type = _validate_win_type(self.win_type, kwargs)
             # GH #15662. `False` makes symmetric window, rather than periodic.
-            return sig.get_window(win_type, window, False).astype(float)
+            kwargs.update(dict(Nx=window, fftbins=False))
+            return sig.get_window(**kwargs).astype(float)
 
     def _get_roll_func(
         self, cfunc: Callable, check_minp: Callable, index: np.ndarray, **kwargs
     ) -> Callable:
         def func(arg, window, min_periods=None, closed=None):
             minp = check_minp(min_periods, len(window))
-            return cfunc(arg, window, minp)
+            return cfunc(arg, window, minp, **kwargs)
 
         return func
 
@@ -924,6 +1071,18 @@ def mean(self, *args, **kwargs):
         nv.validate_window_func("mean", args, kwargs)
         return self._apply("roll_weighted_mean", **kwargs)
 
+    @Substitution(name="window")
+    @Appender(_shared_docs["var"])
+    def var(self, ddof=1, *args, **kwargs):
+        nv.validate_window_func("var", args, kwargs)
+        return self._apply("roll_weighted_var", ddof=ddof, **kwargs)
+
+    @Substitution(name="window")
+    @Appender(_shared_docs["std"])
+    def std(self, ddof=1, *args, **kwargs):
+        nv.validate_window_func("std", args, kwargs)
+        return _zsqrt(self.var(ddof=ddof, **kwargs))
+
 
 class _GroupByMixin(GroupByMixin):
     """
@@ -1216,66 +1375,6 @@ def mean(self, *args, **kwargs):
     def median(self, **kwargs):
         return self._apply("roll_median_c", "median", **kwargs)
 
-    _shared_docs["std"] = dedent(
-        """
-    Calculate %(name)s standard deviation.
-
-    Normalized by N-1 by default. This can be changed using the `ddof`
-    argument.
-
-    Parameters
-    ----------
-    ddof : int, default 1
-        Delta Degrees of Freedom.  The divisor used in calculations
-        is ``N - ddof``, where ``N`` represents the number of elements.
-    *args, **kwargs
-        For NumPy compatibility. No additional arguments are used.
-
-    Returns
-    -------
-    Series or DataFrame
-        Returns the same object type as the caller of the %(name)s calculation.
-
-    See Also
-    --------
-    Series.%(name)s : Calling object with Series data.
-    DataFrame.%(name)s : Calling object with DataFrames.
-    Series.std : Equivalent method for Series.
-    DataFrame.std : Equivalent method for DataFrame.
-    numpy.std : Equivalent method for Numpy array.
-
-    Notes
-    -----
-    The default `ddof` of 1 used in Series.std is different than the default
-    `ddof` of 0 in numpy.std.
-
-    A minimum of one period is required for the rolling calculation.
-
-    Examples
-    --------
-    >>> s = pd.Series([5, 5, 6, 7, 5, 5, 5])
-    >>> s.rolling(3).std()
-    0         NaN
-    1         NaN
-    2    0.577350
-    3    1.000000
-    4    1.000000
-    5    1.154701
-    6    0.000000
-    dtype: float64
-
-    >>> s.expanding(3).std()
-    0         NaN
-    1         NaN
-    2    0.577350
-    3    0.957427
-    4    0.894427
-    5    0.836660
-    6    0.786796
-    dtype: float64
-    """
-    )
-
     def std(self, ddof=1, *args, **kwargs):
         nv.validate_window_func("std", args, kwargs)
         window = self._get_window()
@@ -1291,66 +1390,6 @@ def f(arg, *args, **kwargs):
             f, "std", check_minp=_require_min_periods(1), ddof=ddof, **kwargs
         )
 
-    _shared_docs["var"] = dedent(
-        """
-    Calculate unbiased %(name)s variance.
-
-    Normalized by N-1 by default. This can be changed using the `ddof`
-    argument.
-
-    Parameters
-    ----------
-    ddof : int, default 1
-        Delta Degrees of Freedom.  The divisor used in calculations
-        is ``N - ddof``, where ``N`` represents the number of elements.
-    *args, **kwargs
-        For NumPy compatibility. No additional arguments are used.
-
-    Returns
-    -------
-    Series or DataFrame
-        Returns the same object type as the caller of the %(name)s calculation.
-
-    See Also
-    --------
-    Series.%(name)s : Calling object with Series data.
-    DataFrame.%(name)s : Calling object with DataFrames.
-    Series.var : Equivalent method for Series.
-    DataFrame.var : Equivalent method for DataFrame.
-    numpy.var : Equivalent method for Numpy array.
-
-    Notes
-    -----
-    The default `ddof` of 1 used in :meth:`Series.var` is different than the
-    default `ddof` of 0 in :func:`numpy.var`.
-
-    A minimum of 1 period is required for the rolling calculation.
-
-    Examples
-    --------
-    >>> s = pd.Series([5, 5, 6, 7, 5, 5, 5])
-    >>> s.rolling(3).var()
-    0         NaN
-    1         NaN
-    2    0.333333
-    3    1.000000
-    4    1.000000
-    5    1.333333
-    6    0.000000
-    dtype: float64
-
-    >>> s.expanding(3).var()
-    0         NaN
-    1         NaN
-    2    0.333333
-    3    0.916667
-    4    0.800000
-    5    0.700000
-    6    0.619048
-    dtype: float64
-    """
-    )
-
     def var(self, ddof=1, *args, **kwargs):
         nv.validate_window_func("var", args, kwargs)
         return self._apply(
diff --git a/pandas/tests/window/test_moments.py b/pandas/tests/window/test_moments.py
index d860859958254..b42464a71981b 100644
--- a/pandas/tests/window/test_moments.py
+++ b/pandas/tests/window/test_moments.py
@@ -155,9 +155,23 @@ def test_cmov_window_frame(self):
         rs = DataFrame(vals).rolling(5, win_type="boxcar", center=True).mean()
         tm.assert_frame_equal(DataFrame(xp), rs)
 
-        # invalid method
-        with pytest.raises(AttributeError):
-            (DataFrame(vals).rolling(5, win_type="boxcar", center=True).std())
+        # std
+        xp = np.array(
+            [
+                [np.nan, np.nan],
+                [np.nan, np.nan],
+                [3.789706, 4.068313],
+                [3.429232, 3.237411],
+                [3.589269, 3.220810],
+                [3.405195, 2.380655],
+                [3.281839, 2.369869],
+                [3.676846, 1.801799],
+                [np.nan, np.nan],
+                [np.nan, np.nan],
+            ]
+        )
+        rs = DataFrame(vals).rolling(5, win_type="boxcar", center=True).std()
+        tm.assert_frame_equal(DataFrame(xp), rs)
 
         # sum
         xp = np.array(
diff --git a/pandas/tests/window/test_window.py b/pandas/tests/window/test_window.py
index a6a56c98a9377..d2237f25cce6c 100644
--- a/pandas/tests/window/test_window.py
+++ b/pandas/tests/window/test_window.py
@@ -60,7 +60,7 @@ def test_numpy_compat(self, method):
             getattr(w, method)(dtype=np.float64)
 
     @td.skip_if_no_scipy
-    @pytest.mark.parametrize("arg", ["median", "var", "std", "kurt", "skew"])
+    @pytest.mark.parametrize("arg", ["median", "kurt", "skew"])
     def test_agg_function_support(self, arg):
         df = pd.DataFrame({"A": np.arange(5)})
         roll = df.rolling(2, win_type="triang")


Code review comments:
diff_hunk: @@ -1754,6 +1754,135 @@ def _roll_weighted_sum_mean(float64_t[:] values, float64_t[:] weights,
     return output
 
 
+# ----------------------------------------------------------------------
+# Rolling var for weighted window
+
+
+cdef inline float64_t calc_weighted_var(float64_t t,
+                                        float64_t sum_w,
+                                        Py_ssize_t win,
+                                        int ddof,
+                                        float64_t nobs,
+                                        int64_t minp) nogil:
+    cdef:
+        float64_t result
+
+    # Variance is unchanged if no observation is added or removed
+    if (nobs >= minp) and (nobs > ddof):
+
+        # pathological case
+        if nobs == 1:
+            result = 0
+        else:
+            result = t * win / ((win - <float64_t>ddof) * sum_w)
+            if result < 0:
path: pandas/_libs/window.pyx
text: @user1:
Would this only be negative if the weight was negative which we should validate much earlier?

@author:
I guess it might be if one weight is negative and another is positive but not necessarily

diff_hunk: @@ -1754,6 +1754,135 @@ def _roll_weighted_sum_mean(float64_t[:] values, float64_t[:] weights,
     return output
 
 
+# ----------------------------------------------------------------------
+# Rolling var for weighted window
+
+
+cdef inline float64_t calc_weighted_var(float64_t t,
+                                        float64_t sum_w,
+                                        Py_ssize_t win,
+                                        int ddof,
+                                        float64_t nobs,
+                                        int64_t minp) nogil:
+    cdef:
+        float64_t result
+
+    # Variance is unchanged if no observation is added or removed
+    if (nobs >= minp) and (nobs > ddof):
+
+        # pathological case
+        if nobs == 1:
+            result = 0
+        else:
+            result = t * win / ((win - <float64_t>ddof) * sum_w)
path: pandas/_libs/window.pyx
text: @user1:
Is the cast necessary here?

diff_hunk: @@ -173,6 +173,17 @@ def __getattr__(self, attr):
     def _dir_additions(self):
         return self.obj._dir_additions()
 
+    def _get_kwargs(self, **kwargs) -> Tuple[dict, dict]:
path: pandas/core/window.py
text: @user1:
Can you import `Dict` from typing and add subscription if possible?

@author:
Only annotated separated [kwargs of window](https://github.com/pandas-dev/pandas/pull/27682/files#diff-813747abc5dedd9f437f30f692c84f48R923). The other one is remaining of kwargs from apply function which seems intractable. I guess we don't need to annotate empty dict nor it is possible in python

diff_hunk: @@ -1754,6 +1754,135 @@ def _roll_weighted_sum_mean(float64_t[:] values, float64_t[:] weights,
     return output
 
 
+# ----------------------------------------------------------------------
+# Rolling var for weighted window
+
+
+cdef inline float64_t calc_weighted_var(float64_t t,
+                                        float64_t sum_w,
+                                        Py_ssize_t win,
+                                        int ddof,
path: pandas/_libs/window.pyx
text: @user1:
Should `ddof` be unsigned?