Repository: pandas-dev/pandas
Pull request: Deprecate `center` on `df.expanding`
`df.expanding(center=True)` currently returns non-sensical results and it is unclear what results would be expected. It was previously removed in #7934 for that same reason,but was reintroduced for unclear reasons in a later refactoring

- [x] closes #20647
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
API/BUG: center=True in expanding operations returns non-sensical results
What is `center=True` supposed to do in `df.expanding()...` operations?

Using the example from the docstring of `.expanding()`:

```
In [103]: df = DataFrame({'B': [0, 1, 2, np.nan, 4]})

In [105]: df.expanding(2).sum()
Out[105]: 
     B
0  NaN
1  1.0
2  3.0
3  3.0
4  7.0
```

Now adding `center=True`:

```
In [106]: df.expanding(2, center=True).sum()
Out[106]: 
     B
0  3.0
1  3.0
2  7.0
3  7.0
4  6.0
```

Two observations: it seems it just shifted the first rows (which is not the same as centering) and I don't know where the last values are coming from (and they should certainly not decrease again in an expanding sum with only positive values). 

Further, I also cannot think of what it actually *should* do. If we really want to center it, that would mean returning with a new index (as the real center would only increase with 0.5 and be something like [0, 0.5, 1, 1.5, 2] for those data).

So maybe we should rather remove this keyword for `expanding` ?

Hints from issue discussion:
Hmm, I see that we in the past removed the `center` keyword in the `pd.expanding_` functions, exactly because it did not make sense (https://github.com/pandas-dev/pandas/pull/7934). But it seems with the refactor to the `expanding()` method, it was introduced again
yes center doesn’t make sense in context of expanding
I'd like to work on this 
Go for it!  No need to ask for permission unless someone else is working on it.
Is anyone still working on this?
I don't think so (unless @stillmatic did work on it? If you do, best to say so on this issue), so feel free to take a look at it!
i started working on this issue but don't think my solution is correct/finished. Haven't had time to finish (there are many dependencies). If you have a good idea go ahead, otherwise I might be able to find time and get this done
@stillmatic in the commit you removed `center` parameter from `rolling` method as well, which I believe introduced most broken dependencies. But I believe in that one, centering makes sense.

@jorisvandenbossche I believe in this example center should do nothing if it's supposed to be consistent with functionality of `rolling`. 
As far as I understand, when center=False, the result of the operation applied on the window is placed at the index of last element in window. e.g. `df = DataFrame({'B': [0, 1, 2, 3, 4, 5]})`
`df.rolling(3).sum()` gives [NaN, NaN, 3.0, 6.0, 9.0, 12.0]

When setting center=True, the result is placed at the index of element in center of the window (index in window / 2), effectively shifting the results back.
`df.rolling(3, center=True).sum()` gives [NaN, 3.0, 6.0, 9.0, 12.0, NaN]

`df.rolling(2).sum()` gives same result as df.rolling(2, center=True).sum()`, because the center of window of len=2, is index=1 -> second element in window, which is also the last.
Thus, `df.expanding(2).sum()` should give same result as `df.expanding(2, center=True).sum()`, so there must be a bug somewhere.
Please let me know if you think I got it wrong.

In terms of whether it's actually useful for `expanding` I'm not sure as I am not really familiar, but it should have the same effect as in `rolling` e.g.
`df.expanding(3).sum()` gives [NaN, NaN, 3.0, 6.0, 10.0, 15.0]
And `df.expanding(3, center=True).sum()` should give [NaN, 3.0, 6.0, 10.0, 15.0, NaN] right?
take
It seems like `center` is indeed a weird option for an `expanding` window. I just ran the test suite and there are no tests breaking when I remove the `center` option from expanding (except for one that checks whether there's validation on the center parameter). Also, a code search on GitHub doesn't reveal any instances where `center` is used in conjuction with `expanding`.

Combined with the current output simply being wrong, I think that means we can move forward with deprecating `center`. I'll submit a PR.

Commit message: Deprecate `center` on `df.expanding`

`df.expanding(center=True)` currently returns non-sensical results and it
is unclear what results would be expected. It was previously removed in
https://github.com/pandas-dev/pandas/pull/7934 for that same reason,
but was reintroduced for unclear reasons in a later refactoring

Patch to review:
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index 9bd4ddbb624d9..e7f417cbe52c3 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -820,6 +820,9 @@ Deprecations
   precision through the ``rtol``, and ``atol`` parameters, thus deprecating the
   ``check_less_precise`` parameter. (:issue:`13357`).
 - :func:`DataFrame.melt` accepting a value_name that already exists is deprecated, and will be removed in a future version (:issue:`34731`)
+- the ``center`` keyword in the :meth:`DataFrame.expanding`  function is deprecated and will be removed in a future version (:issue:`20647`)
+
+
 
 .. ---------------------------------------------------------------------------
 
diff --git a/pandas/core/base.py b/pandas/core/base.py
index b62ef668df5e1..2297f8337ca0b 100644
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -5,6 +5,7 @@
 import builtins
 import textwrap
 from typing import Any, Dict, FrozenSet, List, Optional, Union
+import warnings
 
 import numpy as np
 
@@ -581,12 +582,17 @@ def _shallow_copy(self, obj, **kwargs):
         """
         return a new object with the replacement attributes
         """
-        if isinstance(obj, self._constructor):
-            obj = obj.obj
-        for attr in self._attributes:
-            if attr not in kwargs:
-                kwargs[attr] = getattr(self, attr)
-        return self._constructor(obj, **kwargs)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                "The `center` argument on `expanding` will be removed in the future",
+            )
+            if isinstance(obj, self._constructor):
+                obj = obj.obj
+            for attr in self._attributes:
+                if attr not in kwargs:
+                    kwargs[attr] = getattr(self, attr)
+            return self._constructor(obj, **kwargs)
 
 
 class IndexOpsMixin:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index d892e2487b31c..7e7a5ae92e2a2 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -10501,7 +10501,7 @@ def rolling(
         cls.rolling = rolling
 
         @doc(Expanding)
-        def expanding(self, min_periods=1, center=False, axis=0):
+        def expanding(self, min_periods=1, center=None, axis=0):
             axis = self._get_axis_number(axis)
             return Expanding(self, min_periods=min_periods, center=center, axis=axis)
 
diff --git a/pandas/core/groupby/base.py b/pandas/core/groupby/base.py
index e71b2f94c8014..7dded2ba258e3 100644
--- a/pandas/core/groupby/base.py
+++ b/pandas/core/groupby/base.py
@@ -4,6 +4,7 @@
 SeriesGroupBy and the DataFrameGroupBy objects.
 """
 import collections
+import warnings
 
 from pandas.core.dtypes.common import is_list_like, is_scalar
 
@@ -40,8 +41,12 @@ def _gotitem(self, key, ndim, subset=None):
             groupby = self._groupby[key]
         except IndexError:
             groupby = self._groupby
-
-        self = type(self)(subset, groupby=groupby, parent=self, **kwargs)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                "The `center` argument on `expanding` will be removed in the future",
+            )
+            self = type(self)(subset, groupby=groupby, parent=self, **kwargs)
         self._reset_cache()
         if subset.ndim == 2:
             if is_scalar(key) and key in subset or is_list_like(key):
diff --git a/pandas/core/window/expanding.py b/pandas/core/window/expanding.py
index bbc19fad8b799..6fcb3b5dcbc2b 100644
--- a/pandas/core/window/expanding.py
+++ b/pandas/core/window/expanding.py
@@ -1,5 +1,6 @@
 from textwrap import dedent
 from typing import Dict, Optional
+import warnings
 
 from pandas.compat.numpy import function as nv
 from pandas.util._decorators import Appender, Substitution, doc
@@ -57,7 +58,15 @@ class Expanding(_Rolling_and_Expanding):
 
     _attributes = ["min_periods", "center", "axis"]
 
-    def __init__(self, obj, min_periods=1, center=False, axis=0, **kwargs):
+    def __init__(self, obj, min_periods=1, center=None, axis=0, **kwargs):
+        if center is not None:
+            warnings.warn(
+                "The `center` argument on `expanding` will be removed in the future",
+                FutureWarning,
+                stacklevel=3,
+            )
+        else:
+            center = False
         super().__init__(obj=obj, min_periods=min_periods, center=center, axis=axis)
 
     @property
@@ -129,7 +138,12 @@ def aggregate(self, func, *args, **kwargs):
     @Substitution(name="expanding")
     @Appender(_shared_docs["count"])
     def count(self, **kwargs):
-        return super().count(**kwargs)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                "The `center` argument on `expanding` will be removed in the future",
+            )
+            return super().count(**kwargs)
 
     @Substitution(name="expanding")
     @Appender(_shared_docs["apply"])
diff --git a/pandas/tests/window/moments/test_moments_consistency_expanding.py b/pandas/tests/window/moments/test_moments_consistency_expanding.py
index ee3579d76d1db..3ec91dcb60610 100644
--- a/pandas/tests/window/moments/test_moments_consistency_expanding.py
+++ b/pandas/tests/window/moments/test_moments_consistency_expanding.py
@@ -119,8 +119,8 @@ def test_expanding_corr_pairwise(frame):
     ids=["sum", "mean", "max", "min"],
 )
 def test_expanding_func(func, static_comp, has_min_periods, series, frame, nan_locs):
-    def expanding_func(x, min_periods=1, center=False, axis=0):
-        exp = x.expanding(min_periods=min_periods, center=center, axis=axis)
+    def expanding_func(x, min_periods=1, axis=0):
+        exp = x.expanding(min_periods=min_periods, axis=axis)
         return getattr(exp, func)()
 
     _check_expanding(
@@ -166,7 +166,7 @@ def test_expanding_apply_consistency(
 
     with warnings.catch_warnings():
         warnings.filterwarnings(
-            "ignore", message=".*(empty slice|0 for slice).*", category=RuntimeWarning,
+            "ignore", message=".*(empty slice|0 for slice).*", category=RuntimeWarning
         )
         # test consistency between expanding_xyz() and either (a)
         # expanding_apply of Series.xyz(), or (b) expanding_apply of
@@ -267,7 +267,7 @@ def test_expanding_consistency(consistency_data, min_periods):
     # with empty/0-length Series/DataFrames
     with warnings.catch_warnings():
         warnings.filterwarnings(
-            "ignore", message=".*(empty slice|0 for slice).*", category=RuntimeWarning,
+            "ignore", message=".*(empty slice|0 for slice).*", category=RuntimeWarning
         )
 
         # test consistency between different expanding_* moments
@@ -454,7 +454,7 @@ def test_expanding_cov_pairwise_diff_length():
 def test_expanding_corr_pairwise_diff_length():
     # GH 7512
     df1 = DataFrame(
-        [[1, 2], [3, 2], [3, 4]], columns=["A", "B"], index=Index(range(3), name="bar"),
+        [[1, 2], [3, 2], [3, 4]], columns=["A", "B"], index=Index(range(3), name="bar")
     )
     df1a = DataFrame(
         [[1, 2], [3, 4]], index=Index([0, 2], name="bar"), columns=["A", "B"]
diff --git a/pandas/tests/window/test_api.py b/pandas/tests/window/test_api.py
index 33fb79d98a324..2c3d8b4608806 100644
--- a/pandas/tests/window/test_api.py
+++ b/pandas/tests/window/test_api.py
@@ -107,10 +107,7 @@ def test_agg():
 
     with pytest.raises(SpecificationError, match=msg):
         r.aggregate(
-            {
-                "A": {"mean": "mean", "sum": "sum"},
-                "B": {"mean2": "mean", "sum2": "sum"},
-            }
+            {"A": {"mean": "mean", "sum": "sum"}, "B": {"mean2": "mean", "sum2": "sum"}}
         )
 
     result = r.aggregate({"A": ["mean", "std"], "B": ["mean", "std"]})
@@ -191,11 +188,7 @@ def test_count_nonnumeric_types():
         "dt_nat",
         "periods_nat",
     ]
-    dt_nat_col = [
-        Timestamp("20170101"),
-        Timestamp("20170203"),
-        Timestamp(None),
-    ]
+    dt_nat_col = [Timestamp("20170101"), Timestamp("20170203"), Timestamp(None)]
 
     df = DataFrame(
         {
diff --git a/pandas/tests/window/test_expanding.py b/pandas/tests/window/test_expanding.py
index 30d65ebe84a1f..1c53d72ec719f 100644
--- a/pandas/tests/window/test_expanding.py
+++ b/pandas/tests/window/test_expanding.py
@@ -18,13 +18,10 @@ def test_doc_string():
 
 def test_constructor(which):
     # GH 12669
-
     c = which.expanding
 
     # valid
     c(min_periods=1)
-    c(min_periods=1, center=True)
-    c(min_periods=1, center=False)
 
     # not valid
     for w in [2.0, "foo", np.array([2])]:
@@ -32,10 +29,6 @@ def test_constructor(which):
         with pytest.raises(ValueError, match=msg):
             c(min_periods=w)
 
-        msg = "center must be a boolean"
-        with pytest.raises(ValueError, match=msg):
-            c(min_periods=1, center=w)
-
 
 @pytest.mark.parametrize("method", ["std", "mean", "sum", "max", "min", "var"])
 def test_numpy_compat(method):
@@ -213,3 +206,16 @@ def test_iter_expanding_series(ser, expected, min_periods):
 
     for (expected, actual) in zip(expected, ser.expanding(min_periods)):
         tm.assert_series_equal(actual, expected)
+
+
+def test_center_deprecate_warning():
+    # GH 20647
+    df = pd.DataFrame()
+    with tm.assert_produces_warning(FutureWarning):
+        df.expanding(center=True)
+
+    with tm.assert_produces_warning(FutureWarning):
+        df.expanding(center=False)
+
+    with tm.assert_produces_warning(None):
+        df.expanding()


Code review comments:
diff_hunk: @@ -581,12 +582,17 @@ def _shallow_copy(self, obj, **kwargs):
         """
         return a new object with the replacement attributes
         """
-        if isinstance(obj, self._constructor):
-            obj = obj.obj
-        for attr in self._attributes:
-            if attr not in kwargs:
-                kwargs[attr] = getattr(self, attr)
-        return self._constructor(obj, **kwargs)
+        with warnings.catch_warnings():
path: pandas/core/base.py
text: @user1:
Do you know where this is getting called? Seems strange to have to filter the warning here

@user2:
oh no, we DO not want to do this here at all