Repository: pandas-dev/pandas
Pull request #58107 (2024-04-01 18:21:54)

BUG: Remove "Mean of empty slice" warning in nanmedian
This PR supresses a warning when calculating the `median` of a `pd.Series` that is full of `NA`s. The warning is just letting you know that `numpy` got an empty array but correctly returns `np.nan`.

The warning is issued as follows:
1. When the series has `NA`s and you try to calculate the `median`, `nanops.nanmedian` is called.
2. When the array is full of NAs, the `_mask` in `nanops.nanmedian.get_median` is all `False`, so `np.nanmedian(x[_mask])` gets an empty array.
3. `np.nanmedian` calls `numpy.lib.nanfunctions._nanmedian` which has a short-circuit path when the array is empty [[ref](https://github.com/numpy/numpy/blob/1c8b03bf2c87f081eea211a5061e423285c548af/numpy/lib/_nanfunctions_impl.py#L1215-L1216)].
4. This path calls `np.nanmean`. This creates an empty mask [[ref](https://github.com/numpy/numpy/blob/1c8b03bf2c87f081eea211a5061e423285c548af/numpy/lib/_nanfunctions_impl.py#L1033)] because the array is empty.
5. Then, the sum of the negated mask is used as the denominator of the `mean`, which causes the result to be `np.nan` (correct!).
6. The method issues a warning because the array was empty.

Another way to solve this would be to change `get_median` to explicitly return `np.nan` before the call to `np.nanmedian`, but that would involve tampering with the condition in the if, which has no comments so I'm unsure if changing it makes sense.

```python
    def get_median(x, _mask=None):
        if _mask is None:
            _mask = notna(x)
        else:
            _mask = ~_mask
        all_na = _mask.all()
        if (not skipna and not all_na) or all_na:
            return np.nan
        with warnings.catch_warnings():
            # Suppress RuntimeWarning about All-NaN slice
            warnings.filterwarnings(
                        "ignore", "All-NaN slice encountered", RuntimeWarning
            )
            res = np.nanmedian(x[_mask])
        return res
```
which simplifies to `if skipna: return np.nan` I think.

I haven't added tests nor ensured all of the below passed not added comments because I want to ensure this is the preferred solution. Otherwise, I can edit `get_median`. Let me know!

---

- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 41f1c4c689..b8726e058a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,7 +19,7 @@ ci:
     skip: [pylint, pyright, mypy]
 repos:
 -   repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.3.1
+    rev: v0.3.4
     hooks:
     -   id: ruff
         args: [--exit-non-zero-on-fix]
@@ -39,7 +39,7 @@ repos:
     -   id: ruff-format
         exclude: ^scripts
 -   repo: https://github.com/jendrikseipp/vulture
-    rev: 'v2.10'
+    rev: 'v2.11'
     hooks:
       - id: vulture
         entry: python scripts/run_vulture.py
@@ -93,11 +93,11 @@ repos:
         args: [--disable=all, --enable=redefined-outer-name]
         stages: [manual]
 -   repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
     -   id: isort
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v3.15.0
+    rev: v3.15.2
     hooks:
     -   id: pyupgrade
         args: [--py39-plus]
@@ -116,7 +116,7 @@ repos:
     hooks:
     - id: sphinx-lint
 -   repo: https://github.com/pre-commit/mirrors-clang-format
-    rev: v17.0.6
+    rev: v18.1.2
     hooks:
     - id: clang-format
       files: ^pandas/_libs/src|^pandas/_libs/include
diff --git a/ci/code_checks.sh b/ci/code_checks.sh
index c3eed373c3..9c39fac13b 100755
--- a/ci/code_checks.sh
+++ b/ci/code_checks.sh
@@ -120,9 +120,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.DataFrame.to_feather SA01" \
         -i "pandas.DataFrame.to_markdown SA01" \
         -i "pandas.DataFrame.to_parquet RT03" \
-        -i "pandas.DataFrame.to_period SA01" \
-        -i "pandas.DataFrame.to_timestamp SA01" \
-        -i "pandas.DataFrame.tz_convert SA01" \
         -i "pandas.DataFrame.var PR01,RT03,SA01" \
         -i "pandas.DatetimeIndex.ceil SA01" \
         -i "pandas.DatetimeIndex.date SA01" \
@@ -467,11 +464,8 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.Series.to_frame SA01" \
         -i "pandas.Series.to_list RT03" \
         -i "pandas.Series.to_markdown SA01" \
-        -i "pandas.Series.to_period SA01" \
         -i "pandas.Series.to_string SA01" \
-        -i "pandas.Series.to_timestamp RT03,SA01" \
         -i "pandas.Series.truediv PR07" \
-        -i "pandas.Series.tz_convert SA01" \
         -i "pandas.Series.update PR07,SA01" \
         -i "pandas.Series.var PR01,RT03,SA01" \
         -i "pandas.SparseDtype SA01" \
@@ -667,60 +661,40 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.core.groupby.DataFrameGroupBy.__iter__ RT03,SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.agg RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.aggregate RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.apply RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.boxplot PR07,RT03,SA01" \
-        -i "pandas.core.groupby.DataFrameGroupBy.cummax RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.cummin RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.cumprod RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.cumsum RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.filter RT03,SA01" \
+        -i "pandas.core.groupby.DataFrameGroupBy.filter SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.get_group RT03,SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.groups SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.hist RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.indices SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.max SA01" \
-        -i "pandas.core.groupby.DataFrameGroupBy.mean RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.median SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.min SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.nth PR02" \
-        -i "pandas.core.groupby.DataFrameGroupBy.nunique RT03,SA01" \
+        -i "pandas.core.groupby.DataFrameGroupBy.nunique SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.ohlc SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.plot PR02,SA01" \
         -i "pandas.core.groupby.DataFrameGroupBy.prod SA01" \
-        -i "pandas.core.groupby.DataFrameGroupBy.rank RT03" \
-        -i "pandas.core.groupby.DataFrameGroupBy.resample RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.sem SA01" \
-        -i "pandas.core.groupby.DataFrameGroupBy.skew RT03" \
         -i "pandas.core.groupby.DataFrameGroupBy.sum SA01" \
-        -i "pandas.core.groupby.DataFrameGroupBy.transform RT03" \
         -i "pandas.core.groupby.SeriesGroupBy.__iter__ RT03,SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.agg RT03" \
         -i "pandas.core.groupby.SeriesGroupBy.aggregate RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.apply RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.cummax RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.cummin RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.cumprod RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.cumsum RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.filter PR01,RT03,SA01" \
+        -i "pandas.core.groupby.SeriesGroupBy.filter PR01,SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.get_group RT03,SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.groups SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.indices SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_decreasing SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.is_monotonic_increasing SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.max SA01" \
-        -i "pandas.core.groupby.SeriesGroupBy.mean RT03" \
         -i "pandas.core.groupby.SeriesGroupBy.median SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.min SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.nth PR02" \
         -i "pandas.core.groupby.SeriesGroupBy.ohlc SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.plot PR02,SA01" \
         -i "pandas.core.groupby.SeriesGroupBy.prod SA01" \
-        -i "pandas.core.groupby.SeriesGroupBy.rank RT03" \
-        -i "pandas.core.groupby.SeriesGroupBy.resample RT03" \
         -i "pandas.core.groupby.SeriesGroupBy.sem SA01" \
-        -i "pandas.core.groupby.SeriesGroupBy.skew RT03" \
         -i "pandas.core.groupby.SeriesGroupBy.sum SA01" \
-        -i "pandas.core.groupby.SeriesGroupBy.transform RT03" \
         -i "pandas.core.resample.Resampler.__iter__ RT03,SA01" \
         -i "pandas.core.resample.Resampler.ffill RT03" \
         -i "pandas.core.resample.Resampler.get_group RT03,SA01" \
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 15e85d0f90..4debd41de2 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -207,6 +207,7 @@ Removal of prior version deprecations/changes
 - :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
 - All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`)
 - All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`)
+- Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)
 - Disallow non-standard (``np.ndarray``, :class:`Index`, :class:`ExtensionArray`, or :class:`Series`) to :func:`isin`, :func:`unique`, :func:`factorize` (:issue:`52986`)
 - Disallow passing a pandas type to :meth:`Index.view` (:issue:`55709`)
 - Disallow units other than "s", "ms", "us", "ns" for datetime64 and timedelta64 dtypes in :func:`array` (:issue:`53817`)
diff --git a/pandas/core/arrays/_mixins.py b/pandas/core/arrays/_mixins.py
index 930ee83aea..123dc679a8 100644
--- a/pandas/core/arrays/_mixins.py
+++ b/pandas/core/arrays/_mixins.py
@@ -296,13 +296,6 @@ class NDArrayBackedExtensionArray(NDArrayBacked, ExtensionArray):  # type: ignor
         result = self._from_backing_data(result)
         return result
 
-    def _fill_mask_inplace(
-        self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
-    ) -> None:
-        # (for now) when self.ndim == 2, we assume axis=0
-        func = missing.get_fill_func(method, ndim=self.ndim)
-        func(self._ndarray.T, limit=limit, mask=mask.T)
-
     def _pad_or_backfill(
         self,
         *,
diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py
index fdc839225a..1855bd1368 100644
--- a/pandas/core/arrays/base.py
+++ b/pandas/core/arrays/base.py
@@ -2111,25 +2111,6 @@ class ExtensionArray:
         result[~mask] = val
         return result
 
-    # TODO(3.0): this can be removed once GH#33302 deprecation is enforced
-    def _fill_mask_inplace(
-        self, method: str, limit: int | None, mask: npt.NDArray[np.bool_]
-    ) -> None:
-        """
-        Replace values in locations specified by 'mask' using pad or backfill.
-
-        See also
-        --------
-        ExtensionArray.fillna
-        """
-        func = missing.get_fill_func(method)
-        npvalues = self.astype(object)
-        # NB: if we don't copy mask here, it may be altered inplace, which
-        #  would mess up the `self[mask] = ...` below.
-        func(npvalues, limit=limit, mask=mask.copy())
-        new_values = self._from_sequence(npvalues, dtype=self.dtype)
-        self[mask] = new_values[mask]
-
     def _rank(
         self,
         *,
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 4715164a42..66a68755a2 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -12482,7 +12482,9 @@ class DataFrame(NDFrame, OpsMixin):
         copy: bool | lib.NoDefault = lib.no_default,
     ) -> DataFrame:
         """
-        Cast to DatetimeIndex of timestamps, at *beginning* of period.
+        Cast PeriodIndex to DatetimeIndex of timestamps, at *beginning* of period.
+
+        This can be changed to the *end* of the period, by specifying `how="e"`.
 
         Parameters
         ----------
@@ -12512,8 +12514,13 @@ class DataFrame(NDFrame, OpsMixin):
 
         Returns
         -------
-        DataFrame
-            The DataFrame has a DatetimeIndex.
+        DataFrame with DatetimeIndex
+            DataFrame with the PeriodIndex cast to DatetimeIndex.
+
+        See Also
+        --------
+        DataFrame.to_period: Inverse method to cast DatetimeIndex to PeriodIndex.
+        Series.to_timestamp: Equivalent method for Series.
 
         Examples
         --------
@@ -12569,7 +12576,8 @@ class DataFrame(NDFrame, OpsMixin):
         Convert DataFrame from DatetimeIndex to PeriodIndex.
 
         Convert DataFrame from DatetimeIndex to PeriodIndex with desired
-        frequency (inferred from index if not passed).
+        frequency (inferred from index if not passed). Either index of columns can be
+        converted, depending on `axis` argument.
 
         Parameters
         ----------
@@ -12597,7 +12605,12 @@ class DataFrame(NDFrame, OpsMixin):
         Returns
         -------
         DataFrame
-            The DataFrame has a PeriodIndex.
+            The DataFrame with the converted PeriodIndex.
+
+        See Also
+        --------
+        Series.to_period: Equivalent method for Series.
+        Series.dt.to_period: Convert DateTime column values.
 
         Examples
         --------
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index ea618ea088..9946291759 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -7319,17 +7319,8 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         inplace: bool = False,
         regex: bool = False,
     ) -> Self | None:
-        if value is lib.no_default and not is_dict_like(to_replace) and regex is False:
-            # case that goes through _replace_single and defaults to method="pad"
-            warnings.warn(
-                # GH#33302
-                f"{type(self).__name__}.replace without 'value' and with "
-                "non-dict-like 'to_replace' is deprecated "
-                "and will raise in a future version. "
-                "Explicitly specify the new values instead.",
-                FutureWarning,
-                stacklevel=find_stack_level(),
-            )
+        if not is_bool(regex) and to_replace is not None:
+            raise ValueError("'to_replace' must be 'None' if 'regex' is not a bool")
 
         if not (
             is_scalar(to_replace)
@@ -7342,6 +7333,15 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                 f"{type(to_replace).__name__!r}"
             )
 
+        if value is lib.no_default and not (
+            is_dict_like(to_replace) or is_dict_like(regex)
+        ):
+            raise ValueError(
+                # GH#33302
+                f"{type(self).__name__}.replace must specify either 'value', "
+                "a dict-like 'to_replace', or dict-like 'regex'."
+            )
+
         inplace = validate_bool_kwarg(inplace, "inplace")
         if inplace:
             if not PYPY:
@@ -7352,41 +7352,10 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
                         stacklevel=2,
                     )
 
-        if not is_bool(regex) and to_replace is not None:
-            raise ValueError("'to_replace' must be 'None' if 'regex' is not a bool")
-
         if value is lib.no_default:
-            # GH#36984 if the user explicitly passes value=None we want to
-            #  respect that. We have the corner case where the user explicitly
-            #  passes value=None *and* a method, which we interpret as meaning
-            #  they want the (documented) default behavior.
-
-            # passing a single value that is scalar like
-            # when value is None (GH5319), for compat
-            if not is_dict_like(to_replace) and not is_dict_like(regex):
-                to_replace = [to_replace]
-
-            if isinstance(to_replace, (tuple, list)):
-                # TODO: Consider copy-on-write for non-replaced columns's here
-                if isinstance(self, ABCDataFrame):
-                    from pandas import Series
-
-                    result = self.apply(
-                        Series._replace_single,
-                        args=(to_replace, inplace),
-                    )
-                    if inplace:
-                        return None
-                    return result
-                return self._replace_single(to_replace, inplace)
-
             if not is_dict_like(to_replace):
-                if not is_dict_like(regex):
-                    raise TypeError(
-                        'If "to_replace" and "value" are both None '
-                        'and "to_replace" is not a list, then '
-                        "regex must be a mapping"
-                    )
+                # In this case we have checked above that
+                #  1) regex is dict-like and 2) to_replace is None
                 to_replace = regex
                 regex = True
 
@@ -7749,11 +7718,6 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
             raise ValueError("'method' should be a string, not None.")
 
         obj, should_transpose = (self.T, True) if axis == 1 else (self, False)
-        # GH#53631
-        if np.any(obj.dtypes == object):
-            raise TypeError(
-                f"{type(self).__name__} cannot interpolate with object dtype."
-            )
 
         if isinstance(obj.index, MultiIndex) and method != "linear":
             raise ValueError(
@@ -10425,6 +10389,11 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         TypeError
             If the axis is tz-naive.
 
+        See Also
+        --------
+        DataFrame.tz_localize: Localize tz-naive index of DataFrame to target time zone.
+        Series.tz_localize: Localize tz-naive index of Series to target time zone.
+
         Examples
         --------
         Change to another time zone:
diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py
index 361e9e87fa..0a048d11d0 100644
--- a/pandas/core/groupby/generic.py
+++ b/pandas/core/groupby/generic.py
@@ -240,6 +240,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Returns
         -------
         Series or DataFrame
+            A pandas object with the result of applying ``func`` to each group.
 
         See Also
         --------
@@ -600,6 +601,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Returns
         -------
         Series
+            The filtered subset of the original Series.
 
         Notes
         -----
@@ -1078,6 +1080,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Returns
         -------
         Series
+            Unbiased skew within groups.
 
         See Also
         --------
@@ -1941,6 +1944,7 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         Returns
         -------
         DataFrame
+            The filtered subset of the original DataFrame.
 
         Notes
         -----
@@ -2108,6 +2112,7 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         Returns
         -------
         nunique: DataFrame
+            Counts of unique elements in each position.
 
         Examples
         --------
@@ -2506,6 +2511,7 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         Returns
         -------
         DataFrame
+            Unbiased skew within groups.
 
         See Also
         --------
diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py
index bd8e222831..b313657d33 100644
--- a/pandas/core/groupby/groupby.py
+++ b/pandas/core/groupby/groupby.py
@@ -333,6 +333,8 @@ engine_kwargs : dict, default None
 Returns
 -------
 %(klass)s
+    %(klass)s with the same indexes as the original object filled
+    with transformed values.
 
 See Also
 --------
@@ -1551,6 +1553,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         Series or DataFrame
+            A pandas object with the result of applying ``func`` to each group.
 
         See Also
         --------
@@ -2245,6 +2248,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         pandas.Series or pandas.DataFrame
+            Mean of values within each group. Same object type as the caller.
         %(see_also)s
         Examples
         --------
@@ -3512,11 +3516,8 @@ class GroupBy(BaseGroupBy[NDFrameT]):
 
         Returns
         -------
-        pandas.api.typing.DatetimeIndexResamplerGroupby,
-        pandas.api.typing.PeriodIndexResamplerGroupby, or
-        pandas.api.typing.TimedeltaIndexResamplerGroupby
-            Return a new groupby object, with type depending on the data
-            being resampled.
+        DatetimeIndexResampler, PeriodIndexResampler or TimdeltaResampler
+            Resampler object for the type of the index.
 
         See Also
         --------
@@ -4591,7 +4592,8 @@ class GroupBy(BaseGroupBy[NDFrameT]):
 
         Returns
         -------
-        DataFrame with ranking of values within each group
+        DataFrame
+            The ranking of values within each group.
         %(see_also)s
         Examples
         --------
@@ -4663,6 +4665,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         Series or DataFrame
+            Cumulative product for each group. Same object type as the caller.
         %(see_also)s
         Examples
         --------
@@ -4721,6 +4724,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         Series or DataFrame
+            Cumulative sum for each group. Same object type as the caller.
         %(see_also)s
         Examples
         --------
@@ -4783,6 +4787,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         Series or DataFrame
+            Cumulative min for each group. Same object type as the caller.
         %(see_also)s
         Examples
         --------
@@ -4853,6 +4858,7 @@ class GroupBy(BaseGroupBy[NDFrameT]):
         Returns
         -------
         Series or DataFrame
+            Cumulative max for each group. Same object type as the caller.
         %(see_also)s
         Examples
         --------
diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py
index 468ec32ce7..7be1d5d95f 100644
--- a/pandas/core/internals/blocks.py
+++ b/pandas/core/internals/blocks.py
@@ -1388,12 +1388,10 @@ class Block(PandasObject, libinternals.Block):
             # If there are no NAs, then interpolate is a no-op
             return [self.copy(deep=False)]
 
-        # TODO(3.0): this case will not be reachable once GH#53638 is enforced
         if self.dtype == _dtype_obj:
-            # only deal with floats
-            # bc we already checked that can_hold_na, we don't have int dtype here
-            # test_interp_basic checks that we make a copy here
-            return [self.copy(deep=False)]
+            # GH#53631
+            name = {1: "Series", 2: "DataFrame"}[self.ndim]
+            raise TypeError(f"{name} cannot interpolate with object dtype.")
 
         copy, refs = self._get_refs_and_copy(inplace)
 
diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py
index 51cc9a18c2..9abe3f3564 100644
--- a/pandas/core/nanops.py
+++ b/pandas/core/nanops.py
@@ -745,6 +745,10 @@ def nanmedian(values, *, axis: AxisInt | None = None, skipna: bool = True, mask=
     >>> s = pd.Series([1, np.nan, 2, 2])
     >>> nanops.nanmedian(s.values)
     2.0
+
+    >>> s = pd.Series([np.nan, np.nan, np.nan])
+    >>> nanops.nanmedian(s.values)
+    nan
     """
     # for floats without mask, the data already uses NaN as missing value
     # indicator, and `mask` will be calculated from that below -> in those
@@ -763,6 +767,7 @@ def nanmedian(values, *, axis: AxisInt | None = None, skipna: bool = True, mask=
             warnings.filterwarnings(
                 "ignore", "All-NaN slice encountered", RuntimeWarning
             )
+            warnings.filterwarnings("ignore", "Mean of empty slice", RuntimeWarning)
             res = np.nanmedian(x[_mask])
         return res
 
diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py
index 0a2f7fe43b..574e683907 100644
--- a/pandas/core/reshape/reshape.py
+++ b/pandas/core/reshape/reshape.py
@@ -35,6 +35,7 @@ from pandas.core.algorithms import (
     factorize,
     unique,
 )
+from pandas.core.arrays._mixins import NDArrayBackedExtensionArray
 from pandas.core.arrays.categorical import factorize_from_iterable
 from pandas.core.construction import ensure_wrapped_if_datetimelike
 from pandas.core.frame import DataFrame
@@ -231,20 +232,31 @@ class _Unstacker:
         return new_values, mask.any(0)
         # TODO: in all tests we have mask.any(0).all(); can we rely on that?
 
-    def get_result(self, values, value_columns, fill_value) -> DataFrame:
+    def get_result(self, obj, value_columns, fill_value) -> DataFrame:
+        values = obj._values
         if values.ndim == 1:
             values = values[:, np.newaxis]
 
         if value_columns is None and values.shape[1] != 1:  # pragma: no cover
             raise ValueError("must pass column labels for multi-column data")
 
-        values, _ = self.get_new_values(values, fill_value)
+        new_values, _ = self.get_new_values(values, fill_value)
         columns = self.get_new_columns(value_columns)
         index = self.new_index
 
-        return self.constructor(
-            values, index=index, columns=columns, dtype=values.dtype
+        result = self.constructor(
+            new_values, index=index, columns=columns, dtype=new_values.dtype, copy=False
         )
+        if isinstance(values, np.ndarray):
+            base, new_base = values.base, new_values.base
+        elif isinstance(values, NDArrayBackedExtensionArray):
+            base, new_base = values._ndarray.base, new_values._ndarray.base
+        else:
+            base, new_base = 1, 2  # type: ignore[assignment]
+        if base is new_base:
+            # We can only get here if one of the dimensions is size 1
+            result._mgr.add_references(obj._mgr)
+        return result
 
     def get_new_values(self, values, fill_value=None):
         if values.ndim == 1:
@@ -532,9 +544,7 @@ def unstack(
         unstacker = _Unstacker(
             obj.index, level=level, constructor=obj._constructor_expanddim, sort=sort
         )
-        return unstacker.get_result(
-            obj._values, value_columns=None, fill_value=fill_value
-        )
+        return unstacker.get_result(obj, value_columns=None, fill_value=fill_value)
 
 
 def _unstack_frame(
@@ -550,7 +560,7 @@ def _unstack_frame(
         return obj._constructor_from_mgr(mgr, axes=mgr.axes)
     else:
         return unstacker.get_result(
-            obj._values, value_columns=obj.columns, fill_value=fill_value
+            obj, value_columns=obj.columns, fill_value=fill_value
         )
 
 
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 843788273a..ee496a355f 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -97,7 +97,6 @@ from pandas.core import (
     algorithms,
     base,
     common as com,
-    missing,
     nanops,
     ops,
     roperator,
@@ -5116,40 +5115,6 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
             show_counts=show_counts,
         )
 
-    @overload
-    def _replace_single(self, to_replace, inplace: Literal[False]) -> Self: ...
-
-    @overload
-    def _replace_single(self, to_replace, inplace: Literal[True]) -> None: ...
-
-    @overload
-    def _replace_single(self, to_replace, inplace: bool) -> Self | None: ...
-
-    # TODO(3.0): this can be removed once GH#33302 deprecation is enforced
-    def _replace_single(self, to_replace, inplace: bool) -> Self | None:
-        """
-        Replaces values in a Series using the fill method specified when no
-        replacement value is given in the replace method
-        """
-        limit = None
-        method = "pad"
-
-        result = self if inplace else self.copy()
-
-        values = result._values
-        mask = missing.mask_missing(values, to_replace)
-
-        if isinstance(values, ExtensionArray):
-            # dispatch to the EA's _pad_mask_inplace method
-            values._fill_mask_inplace(method, limit, mask)
-        else:
-            fill_f = missing.get_fill_func(method)
-            fill_f(values, limit=limit, mask=mask)
-
-        if inplace:
-            return None
-        return result
-
     def memory_usage(self, index: bool = True, deep: bool = False) -> int:
         """
         Return the memory usage of the Series.
@@ -5648,6 +5613,8 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         """
         Cast to DatetimeIndex of Timestamps, at *beginning* of period.
 
+        This can be changed to the *end* of the period, by specifying `how="e"`.
+
         Parameters
         ----------
         freq : str, default frequency of PeriodIndex
@@ -5675,6 +5642,12 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         Returns
         -------
         Series with DatetimeIndex
+            Series with the PeriodIndex cast to DatetimeIndex.
+
+        See Also
+        --------
+        Series.to_period: Inverse method to cast DatetimeIndex to PeriodIndex.
+        DataFrame.to_timestamp: Equivalent method for DataFrame.
 
         Examples
         --------
@@ -5748,6 +5721,11 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
         Series
             Series with index converted to PeriodIndex.
 
+        See Also
+        --------
+        DataFrame.to_period: Equivalent method for DataFrame.
+        Series.dt.to_period: Convert DateTime column values.
+
         Examples
         --------
         >>> idx = pd.DatetimeIndex(["2023", "2024", "2025"])
diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py
index 2d8517693a..38a443b56e 100644
--- a/pandas/core/shared_docs.py
+++ b/pandas/core/shared_docs.py
@@ -608,24 +608,7 @@ _shared_docs["replace"] = """
     4    None
     dtype: object
 
-    When ``value`` is not explicitly passed and `to_replace` is a scalar, list
-    or tuple, `replace` uses the method parameter (default 'pad') to do the
-    replacement. So this is why the 'a' values are being replaced by 10
-    in rows 1 and 2 and 'b' in row 4 in this case.
-
-    >>> s.replace('a')
-    0    10
-    1    10
-    2    10
-    3     b
-    4     b
-    dtype: object
-
-        .. deprecated:: 2.1.0
-            The 'method' parameter and padding behavior are deprecated.
-
-    On the other hand, if ``None`` is explicitly passed for ``value``, it will
-    be respected:
+    If ``None`` is explicitly passed for ``value``, it will be respected:
 
     >>> s.replace('a', None)
     0      10
diff --git a/pandas/io/common.py b/pandas/io/common.py
index 35c3a24d8e..4507a7d08c 100644
--- a/pandas/io/common.py
+++ b/pandas/io/common.py
@@ -361,6 +361,16 @@ def _get_filepath_or_buffer(
             stacklevel=find_stack_level(),
         )
 
+    if "a" in mode and compression_method in ["zip", "tar"]:
+        # GH56778
+        warnings.warn(
+            "zip and tar do not support mode 'a' properly. "
+            "This combination will result in multiple files with same name "
+            "being added to the archive.",
+            RuntimeWarning,
+            stacklevel=find_stack_level(),
+        )
+
     # Use binary mode when converting path-like objects to file-like objects (fsspec)
     # except when text mode is explicitly requested. The original mode is returned if
     # fsspec is not used.
diff --git a/pandas/io/parsers/readers.py b/pandas/io/parsers/readers.py
index 7ecd8cd6d5..70f9a68244 100644
--- a/pandas/io/parsers/readers.py
+++ b/pandas/io/parsers/readers.py
@@ -194,6 +194,12 @@ header : int, Sequence of int, 'infer' or None, default 'infer'
     parameter ignores commented lines and empty lines if
     ``skip_blank_lines=True``, so ``header=0`` denotes the first line of
     data rather than the first line of the file.
+
+    When inferred from the file contents, headers are kept distinct from
+    each other by renaming duplicate names with a numeric suffix of the form
+    ``".{{count}}"`` starting from 1, e.g. ``"foo"`` and ``"foo.1"``.
+    Empty headers are named ``"Unnamed: {{i}}"`` or ``"Unnamed: {{i}}_level_{{level}}"``
+    in the case of MultiIndex columns.
 names : Sequence of Hashable, optional
     Sequence of column labels to apply. If the file contains a header row,
     then you should explicitly pass ``header=0`` to override the column names.
diff --git a/web/pandas/pdeps/0001-purpose-and-guidelines.md b/web/pandas/pdeps/0001-purpose-and-guidelines.md
index 24c91fbab0..49a3bc4c87 100644
--- a/web/pandas/pdeps/0001-purpose-and-guidelines.md
+++ b/web/pandas/pdeps/0001-purpose-and-guidelines.md
@@ -6,7 +6,7 @@
   [#51417](https://github.com/pandas-dev/pandas/pull/51417)
 - Author: [Marc Garcia](https://github.com/datapythonista),
   [Noa Tamir](https://github.com/noatamir)
-- Revision: 2
+- Revision: 3
 
 ## PDEP definition, purpose and scope
 
@@ -56,12 +56,24 @@ advisor on the PDEP when it is submitted to the PDEP repository.
 
 ### Workflow
 
+#### Rationale
+
+Our workflow was created to support and enable a consensus seeking process, and to provide clarity,
+for current and future authors, as well as voting members. It is not a strict policy, and we
+discourage any interpretation which seeks to take advantage of it in a way that could "force" or
+"sneak" decisions in one way or another. We expect and encourage transparency, active discussion,
+feedback, and compromise from all our community members.
+
+#### PDEP States
+
 The possible states of a PDEP are:
 
+- Draft
 - Under discussion
 - Accepted
 - Implemented
 - Rejected
+- Withdrawn
 
 Next is described the workflow that PDEPs can follow.
 
@@ -71,16 +83,75 @@ Proposing a PDEP is done by creating a PR adding a new file to `web/pdeps/`.
 The file is a markdown file, you can use `web/pdeps/0001.md` as a reference
 for the expected format.
 
-The initial status of a PDEP will be `Status: Under discussion`. This will be changed to
-`Status: Accepted` when the PDEP is ready and has the approval of the core team.
+The initial status of a PDEP will be `Status: Draft`. This will be changed to
+`Status: Under discussion` by the author(s), when they are ready to proceed with the decision
+making process.
 
-#### Accepted PDEP
+#### PDEP Discussion Timeline
+
+A PDEP discussion will remain open for up to 60 days. This period aims to enable participation
+from volunteers, who might not always be available to respond quickly, as well as provide ample
+time to make changes based on suggestions and considerations offered by the participants.
+Similarly, the following voting period will remain open for 15 days.
+
+To enable and encourage discussions on PDEPs, we follow a notification schedule. At each of the
+following steps, the pandas team, and the pandas-dev mailing list are notified via GitHub and
+E-mail:
+
+- Once a PDEP is ready for discussion.
+- After 30 days, with a note that there is at most 30 days remaining for discussion,
+  and that a vote will be called for if no discussion occurs in the next 15 days.
+- After 45 days, with a note that there is at most 15 days remaining for discussion,
+  and that a vote will be called for in 15 days.
+- Once the voting period starts, after 60 days or in case of an earlier vote, with 15 days
+  remaining for voting.
+- After 10 voting days, with 5 days remaining for voting.
+
+After 30 discussion days, in case 15 days passed without any new unaddressed comments,
+the authors may close the discussion period preemptively, by sending an early reminder
+of 15 days remaining until the voting period starts.
+
+#### Casting Votes
+
+As the voting period starts, a VOTE issue is created which links to the PDEP discussion pull request.
+Each voting member, including author(s) with voting rights, may cast a vote by adding one of the following comments:
+
+- +1: approve.
+- 0: abstain.
+  - Reason: A one sentence reason is required.
+- -1: disapprove
+  - Reason: A one sentence reason is required.
+
+A disapprove vote requires prior participation in the PDEP discussion issue.
 
-A PDEP can only be accepted by the core development team, if the proposal is considered
-worth implementing. Decisions will be made based on the process detailed in the
-[pandas governance document](https://github.com/pandas-dev/pandas-governance/blob/master/governance.md).
-In general, more than one approval will be needed before the PR is merged. And
-there should not be any `Request changes` review at the time of merging.
+Comments made on the public VOTE issue by non-voting members will be deleted.
+
+Once the voting period ends, any voter may tally the votes in a comment, using the format: w-x-y-z,
+where w stands for the total of approving, x of abstaining, y of disapproving votes cast, and z
+of number of voting members who did not respond to the VOTE issue. The tally of the votes will state
+if a quorum has been reached or not.
+
+#### Quorum and Majority
+
+For a PDEP vote to result in accepting the proposal, a quorum is required. All votes (including
+abstentions) are counted towards the quorum. The quorum is computed as the lower of these two
+values:
+
+- 11 voting members.
+- 50% of voting members.
+
+Given a quorum, a majority of 70% of the non-abstaining votes is required as well, i.e. 70% of
+the approving and disapproving votes must be in favor.
+
+Thus, abstaining votes count towards a quorum, but not towards a majority. A voting member might
+choose to abstain when they have participated in the discussion, have some objections to the
+proposal, but do not wish to stop the proposal from moving forward, nor indicate their full
+support.
+
+If a quorum was not reached by the end of the voting period, the PDEP is not accepted. Its status
+will change to rejected.
+
+#### Accepted PDEP
 
 Once a PDEP is accepted, any contributions can be made toward the implementation of the PDEP,
 with an open-ended completion timeline. Development of pandas is difficult to understand and
@@ -109,6 +180,17 @@ discussion. A PDEP can be rejected for different reasons, for example good ideas
 that are not backward-compatible, and the breaking changes are not considered worth
 implementing.
 
+The PDEP author(s) can also decide to withdraw the PDEP before a final decision
+is made (`Status: Withdrawn`), when the PDEP authors themselves have decided
+that the PDEP is actually a bad idea, or have accepted that it is not broadly
+supported or a competing proposal is a better alternative.
+
+The author(s) may choose to resubmit a rejected or withdrawn PDEP. We expect
+authors to use their judgement in that case, as to whether they believe more
+discussion, or an amended proposal has the potential to lead to a different
+result. A new PDEP is then created, which includes a link to the previously
+rejected PDEP.
+
 #### Invalid PDEP
 
 For submitted PDEPs that do not contain proper documentation, are out of scope, or
@@ -184,6 +266,7 @@ hope can help clarify our meaning here:
 
 - 3 August 2022: Initial version ([GH-47938][47938])
 - 15 February 2023: Version 2 ([GH-51417][51417]) clarifies the scope of PDEPs and adds examples
+- 09 June 2023: Version 3 ([GH-53576][53576]) defines a structured decision making process for PDEPs
 
 [7217]: https://github.com/pandas-dev/pandas/pull/7217
 [8074]: https://github.com/pandas-dev/pandas/issues/8074


Test patch:
diff --git a/pandas/tests/frame/methods/test_replace.py b/pandas/tests/frame/methods/test_replace.py
index 3b9c342f35..fb7ba2b7af 100644
--- a/pandas/tests/frame/methods/test_replace.py
+++ b/pandas/tests/frame/methods/test_replace.py
@@ -1264,13 +1264,8 @@ class TestDataFrameReplace:
             r"Expecting 'to_replace' to be either a scalar, array-like, "
             r"dict or None, got invalid type.*"
         )
-        msg2 = (
-            "DataFrame.replace without 'value' and with non-dict-like "
-            "'to_replace' is deprecated"
-        )
         with pytest.raises(TypeError, match=msg):
-            with tm.assert_produces_warning(FutureWarning, match=msg2):
-                df.replace(lambda x: x.strip())
+            df.replace(lambda x: x.strip())
 
     @pytest.mark.parametrize("dtype", ["float", "float64", "int64", "Int64", "boolean"])
     @pytest.mark.parametrize("value", [np.nan, pd.NA])
diff --git a/pandas/tests/frame/methods/test_to_csv.py b/pandas/tests/frame/methods/test_to_csv.py
index 5b9ced8d47..f87fa4137d 100644
--- a/pandas/tests/frame/methods/test_to_csv.py
+++ b/pandas/tests/frame/methods/test_to_csv.py
@@ -1405,3 +1405,20 @@ class TestDataFrameToCSV:
         expected_rows = [",a", '0,"[2020-01-01 00:00:00, 2020-01-02 00:00:00]"']
         expected = tm.convert_rows_list_to_csv_str(expected_rows)
         assert result == expected
+
+    def test_to_csv_warn_when_zip_tar_and_append_mode(self):
+        # GH57875
+        df = DataFrame({"a": [1, 2, 3]})
+        msg = (
+            "zip and tar do not support mode 'a' properly. This combination will "
+            "result in multiple files with same name being added to the archive"
+        )
+        with tm.assert_produces_warning(
+            RuntimeWarning, match=msg, raise_on_extra_warnings=False
+        ):
+            df.to_csv("test.zip", mode="a")
+
+        with tm.assert_produces_warning(
+            RuntimeWarning, match=msg, raise_on_extra_warnings=False
+        ):
+            df.to_csv("test.tar", mode="a")
diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py
index f750d5e7fa..2ccb622c7a 100644
--- a/pandas/tests/reshape/test_pivot.py
+++ b/pandas/tests/reshape/test_pivot.py
@@ -2703,3 +2703,16 @@ class TestPivot:
             index=Index(["a", "b", "All"], name=0),
         )
         tm.assert_frame_equal(result, expected)
+
+    @pytest.mark.parametrize("m", [1, 10])
+    def test_unstack_shares_memory(self, m):
+        # GH#56633
+        levels = np.arange(m)
+        index = MultiIndex.from_product([levels] * 2)
+        values = np.arange(m * m * 100).reshape(m * m, 100)
+        df = DataFrame(values, index, np.arange(100))
+        df_orig = df.copy()
+        result = df.unstack(sort=False)
+        assert np.shares_memory(df._values, result._values) is (m == 1)
+        result.iloc[0, 0] = -1
+        tm.assert_frame_equal(df, df_orig)
diff --git a/pandas/tests/series/methods/test_interpolate.py b/pandas/tests/series/methods/test_interpolate.py
index c5df1fd498..1008c2c87d 100644
--- a/pandas/tests/series/methods/test_interpolate.py
+++ b/pandas/tests/series/methods/test_interpolate.py
@@ -790,11 +790,9 @@ class TestSeriesInterpolateData:
 
     def test_interpolate_asfreq_raises(self):
         ser = Series(["a", None, "b"], dtype=object)
-        msg2 = "Series cannot interpolate with object dtype"
-        msg = "Invalid fill method"
-        with pytest.raises(TypeError, match=msg2):
-            with pytest.raises(ValueError, match=msg):
-                ser.interpolate(method="asfreq")
+        msg = "Can not interpolate with method=asfreq"
+        with pytest.raises(ValueError, match=msg):
+            ser.interpolate(method="asfreq")
 
     def test_interpolate_fill_value(self):
         # GH#54920
diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py
index 09a3469e73..0a79bcea67 100644
--- a/pandas/tests/series/methods/test_replace.py
+++ b/pandas/tests/series/methods/test_replace.py
@@ -137,20 +137,15 @@ class TestSeriesReplace:
         # API change from 0.12?
         # GH 5319
         ser = pd.Series([0, np.nan, 2, 3, 4])
-        expected = ser.ffill()
         msg = (
-            "Series.replace without 'value' and with non-dict-like "
-            "'to_replace' is deprecated"
+            "Series.replace must specify either 'value', "
+            "a dict-like 'to_replace', or dict-like 'regex'"
         )
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = ser.replace([np.nan])
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(ValueError, match=msg):
+            ser.replace([np.nan])
 
-        ser = pd.Series([0, np.nan, 2, 3, 4])
-        expected = ser.ffill()
-        with tm.assert_produces_warning(FutureWarning, match=msg):
-            result = ser.replace(np.nan)
-        tm.assert_series_equal(result, expected)
+        with pytest.raises(ValueError, match=msg):
+            ser.replace(np.nan)
 
     def test_replace_datetime64(self):
         # GH 5797
@@ -182,19 +177,16 @@ class TestSeriesReplace:
 
     def test_replace_with_single_list(self):
         ser = pd.Series([0, 1, 2, 3, 4])
-        msg2 = (
-            "Series.replace without 'value' and with non-dict-like "
-            "'to_replace' is deprecated"
+        msg = (
+            "Series.replace must specify either 'value', "
+            "a dict-like 'to_replace', or dict-like 'regex'"
         )
-        with tm.assert_produces_warning(FutureWarning, match=msg2):
-            result = ser.replace([1, 2, 3])
-        tm.assert_series_equal(result, pd.Series([0, 0, 0, 0, 4]))
+        with pytest.raises(ValueError, match=msg):
+            ser.replace([1, 2, 3])
 
         s = ser.copy()
-        with tm.assert_produces_warning(FutureWarning, match=msg2):
-            return_value = s.replace([1, 2, 3], inplace=True)
-        assert return_value is None
-        tm.assert_series_equal(s, pd.Series([0, 0, 0, 0, 4]))
+        with pytest.raises(ValueError, match=msg):
+            s.replace([1, 2, 3], inplace=True)
 
     def test_replace_mixed_types(self):
         ser = pd.Series(np.arange(5), dtype="int64")
@@ -483,13 +475,8 @@ class TestSeriesReplace:
             r"Expecting 'to_replace' to be either a scalar, array-like, "
             r"dict or None, got invalid type.*"
         )
-        msg2 = (
-            "Series.replace without 'value' and with non-dict-like "
-            "'to_replace' is deprecated"
-        )
         with pytest.raises(TypeError, match=msg):
-            with tm.assert_produces_warning(FutureWarning, match=msg2):
-                series.replace(lambda x: x.strip())
+            series.replace(lambda x: x.strip())
 
     @pytest.mark.parametrize("frame", [False, True])
     def test_replace_nonbool_regex(self, frame):