pandas-dev/pandas · #71492
ENH: add axis keyword to ExtensionArray._concat_same_type (GH#47514)
doc/source/whatsnew/v3.2.0.rst1 + / 0 −
@@ -97,6 +97,7 @@ Other API changes - :class:`.Styler` now displays missing float values as ``NaN`` rather than ``nan`` by default, matching :class:`DataFrame` (:issue:`12710`) - :class:`ExtensionArray` now implements ``__array_function__``. Code expecting :func:`numpy.concatenate` or :func:`numpy.delete` to return a NumPy array for :class:`ExtensionArray` input, including ``_concat_same_type`` and ``delete`` implementations in :class:`ExtensionArray` subclasses, may need updating, as may code mixing an :class:`ExtensionArray` with another library's ``__array_function__`` arrays (:issue:`26380`) - :class:`Index` now displays missing float values as ``NaN`` rather than ``nan`` in its repr, matching :class:`Series` and :class:`DataFrame` (:issue:`64733`)+- :meth:`.ExtensionArray._concat_same_type` now has an ``axis`` keyword (default ``0``); pandas only passes it for dtypes that support 2D, so existing implementations without it keep working (:issue:`47514`) - :meth:`.Styler.to_excel` now omits the rows, columns, index and header levels, and names hidden with :meth:`.Styler.hide`, matching the other :class:`.Styler` output methods; hiding the whole index with :class:`MultiIndex` columns now raises ``NotImplementedError``, as ``index=False`` does (:issue:`42276`) - :meth:`DataFrame.copy` and :meth:`Series.copy` with ``deep=True`` (the default), and :meth:`.ArrowExtensionArray.copy`, now copy pyarrow-backed data instead of sharing its memory, so a copy no longer keeps the original's memory alive (:issue:`61930`) - :meth:`DataFrame.query` now raises a ``ValueError`` when the expression does not evaluate to a boolean array, instead of silently using the result as row labels; use :attr:`DataFrame.loc` for label selection (:issue:`8560`)pandas/core/arrays/arrow/array.py5 + / 1 −
@@ -2647,18 +2647,22 @@ def value_counts(self, dropna: bool = True) -> Series: return Series(counts, index=index, name="count", copy=False) @classmethod- def _concat_same_type(cls, to_concat) -> Self:+ def _concat_same_type(cls, to_concat, axis: AxisInt = 0) -> Self: """ Concatenate multiple ArrowExtensionArrays. Parameters ---------- to_concat : sequence of ArrowExtensionArrays+ axis : int, default 0+ Must be 0 or -1; ArrowExtensionArray is 1D-only. Returns ------- ArrowExtensionArray """+ if axis not in (0, -1):+ raise ValueError(f"axis {axis} is out of bounds for array of dimension 1") chunks = [array for ea in to_concat for array in ea._pa_array.iterchunks()] if to_concat[0].dtype == "string": # StringDtype has no attribute pyarrow_dtypepandas/core/arrays/base.py4 + / 1 −
@@ -2396,7 +2396,7 @@ def ravel(self, order: Literal["C", "F", "A", "K"] | None = "C") -> Self: return self @classmethod- def _concat_same_type(cls, to_concat: Sequence[Self]) -> Self:+ def _concat_same_type(cls, to_concat: Sequence[Self], axis: AxisInt = 0) -> Self: """ Concatenate multiple arrays of this dtype. @@ -2408,6 +2408,9 @@ def _concat_same_type(cls, to_concat: Sequence[Self]) -> Self: ---------- to_concat : sequence of this type An array of the same dtype to concatenate.+ axis : int, default 0+ Axis along which to concatenate. pandas only passes the axis+ keyword for dtypes that support 2D (``dtype._supports_2d``). Returns -------pandas/core/arrays/interval.py7 + / 1 −
@@ -1008,18 +1008,24 @@ def equals(self, other) -> bool: ) @classmethod- def _concat_same_type(cls, to_concat: Sequence[IntervalArray]) -> Self:+ def _concat_same_type(+ cls, to_concat: Sequence[IntervalArray], axis: AxisInt = 0+ ) -> Self: """ Concatenate multiple IntervalArray Parameters ---------- to_concat : sequence of IntervalArray+ axis : int, default 0+ Must be 0 or -1; IntervalArray is 1D-only. Returns ------- IntervalArray """+ if axis not in (0, -1):+ raise ValueError(f"axis {axis} is out of bounds for array of dimension 1") closed_set = {interval.closed for interval in to_concat} if len(closed_set) != 1: raise ValueError("Intervals must all be closed on the same side.")pandas/core/arrays/sparse/array.py3 + / 1 −
@@ -1698,7 +1698,9 @@ def copy(self) -> Self: return self._simple_new(values, self.sp_index, self.dtype) @classmethod- def _concat_same_type(cls, to_concat: Sequence[Self]) -> Self:+ def _concat_same_type(cls, to_concat: Sequence[Self], axis: AxisInt = 0) -> Self:+ if axis not in (0, -1):+ raise ValueError(f"axis {axis} is out of bounds for array of dimension 1") fill_value = to_concat[0].fill_value values = []pandas/core/dtypes/concat.py2 + / 10 −
@@ -104,12 +104,7 @@ def concat_compat( # e.g. DatetimeArray # NB: We are assuming here that ensure_wrapped_if_arraylike has # been called where relevant.- return obj._concat_same_type(- # error: Unexpected keyword argument "axis" for "_concat_same_type"- # of "ExtensionArray"- to_concat_eas,- axis=axis, # type: ignore[call-arg]- )+ return obj._concat_same_type(to_concat_eas, axis=axis) # If all arrays are empty, there's nothing to convert, just short-cut to # the concatenation, #3121.@@ -134,10 +129,7 @@ def concat_compat( if ea_compat_axis or axis == 0: return cls._concat_same_type(to_concat_eas) else:- return cls._concat_same_type(- to_concat_eas,- axis=axis, # type: ignore[call-arg]- )+ return cls._concat_same_type(to_concat_eas, axis=axis) else: to_concat_arrs = cast("Sequence[np.ndarray]", to_concat) result = np.concatenate(to_concat_arrs, axis=axis)pandas/tests/arrays/test_array.py34 + / 0 −
@@ -761,3 +761,37 @@ def test_numpy_datetime_like_to_string(data, dtype, expected_data): ) tm.assert_equal(result, expected)+++@pytest.mark.parametrize(+ "data, dtype",+ [+ ([1, 2, None], "Int64"),+ ([1.5, None], "Float64"),+ ([True, None], "boolean"),+ (["a", "b"], "category"),+ (["a", None], pd.StringDtype("python")),+ ([1, 0, 0], pd.SparseDtype("int64", 0)),+ ([pd.Interval(0, 1), pd.Interval(1, 2)], "interval"),+ (["2020-01-01", None], "M8[ns]"),+ (["2020-01-01", None], "M8[ns, UTC]"),+ (["1D", None], "m8[ns]"),+ (["2020-01", None], "period[M]"),+ ([1, 2], "object"),+ pytest.param([1, None], "int64[pyarrow]", marks=td.skip_if_no("pyarrow")),+ pytest.param(["a", None], "string[pyarrow]", marks=td.skip_if_no("pyarrow")),+ ],+)+def test_concat_same_type_axis_keyword(data, dtype):+ # GH#47514 every internal EA accepts the base class's axis keyword+ arr = pd.array(data, dtype=dtype)+ result = type(arr)._concat_same_type([arr, arr], axis=0)+ expected = type(arr)._concat_same_type([arr, arr])+ tm.assert_extension_array_equal(result, expected)+ assert len(result) == 2 * len(arr)+ result = type(arr)._concat_same_type([arr, arr], axis=-1)+ tm.assert_extension_array_equal(result, expected)++ # 1D arrays reject a non-zero axis rather than ignoring it+ with pytest.raises(ValueError, match="out of bounds"):+ type(arr)._concat_same_type([arr, arr], axis=1)pandas/tests/extension/array_with_attr/array.py1 + / 0 −
@@ -82,6 +82,7 @@ def take(self, indexer, allow_fill=False, fill_value=None): def copy(self): return type(self)(self.data.copy(), self.attr) + # no axis keyword, to test third-party EAs that lack it (GH#47514) @classmethod def _concat_same_type(cls, to_concat): data = np.concatenate([x.data for x in to_concat])pandas/tests/extension/decimal/array.py1 + / 0 −
@@ -250,6 +250,7 @@ def _formatter(self, boxed=False): return "Decimal: {}".format return repr + # no axis keyword, to test third-party EAs that lack it (GH#47514) @classmethod def _concat_same_type(cls, to_concat): return cls(np.concatenate([x._data for x in to_concat]))pandas/tests/extension/json/array.py1 + / 0 −
@@ -249,6 +249,7 @@ def unique(self): # a 2-dim object. return type(self)([dict(x) for x in {tuple(d.items()) for d in self.data}]) + # no axis keyword, to test third-party EAs that lack it (GH#47514) @classmethod def _concat_same_type(cls, to_concat): data = list(itertools.chain.from_iterable(x.data for x in to_concat))pandas/tests/extension/list/array.py1 + / 0 −
@@ -119,6 +119,7 @@ def astype(self, dtype, copy=True): else: return np.array(self.data, dtype=dtype, copy=copy) + # no axis keyword, to test third-party EAs that lack it (GH#47514) @classmethod def _concat_same_type(cls, to_concat): data = np.concatenate([x.data for x in to_concat])