Skip to content

Commit d3f5a44

Browse files
jbrockmendelrhshadrach
authored andcommitted
BUG: DataFrame arithmetic with subclass where constructor is not the subclass itself (pandas-dev#43897)
1 parent eefd0f0 commit d3f5a44

File tree

3 files changed

+41
-2
lines changed

3 files changed

+41
-2
lines changed

doc/source/whatsnew/v1.4.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,8 @@ Numeric
408408
- Bug in :meth:`DataFrame.rank` raising ``ValueError`` with ``object`` columns and ``method="first"`` (:issue:`41931`)
409409
- Bug in :meth:`DataFrame.rank` treating missing values and extreme values as equal (for example ``np.nan`` and ``np.inf``), causing incorrect results when ``na_option="bottom"`` or ``na_option="top`` used (:issue:`41931`)
410410
- Bug in ``numexpr`` engine still being used when the option ``compute.use_numexpr`` is set to ``False`` (:issue:`32556`)
411+
- Bug in :class:`DataFrame` arithmetic ops with a subclass whose :meth:`_constructor` attribute is a callable other than the subclass itself (:issue:`43201`)
412+
-
411413

412414
Conversion
413415
^^^^^^^^^^

pandas/core/frame.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6955,7 +6955,7 @@ def _dispatch_frame_op(self, right, func: Callable, axis: int | None = None):
69556955
# i.e. scalar, faster than checking np.ndim(right) == 0
69566956
with np.errstate(all="ignore"):
69576957
bm = self._mgr.apply(array_op, right=right)
6958-
return type(self)(bm)
6958+
return self._constructor(bm)
69596959

69606960
elif isinstance(right, DataFrame):
69616961
assert self.index.equals(right.index)
@@ -6976,7 +6976,7 @@ def _dispatch_frame_op(self, right, func: Callable, axis: int | None = None):
69766976
right._mgr, # type: ignore[arg-type]
69776977
array_op,
69786978
)
6979-
return type(self)(bm)
6979+
return self._constructor(bm)
69806980

69816981
elif isinstance(right, Series) and axis == 1:
69826982
# axis=1 means we want to operate row-by-row

pandas/tests/frame/test_arithmetic.py

+37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections import deque
22
from datetime import datetime
3+
import functools
34
import operator
45
import re
56

@@ -1845,3 +1846,39 @@ def test_bool_frame_mult_float():
18451846
result = df * 1.0
18461847
expected = DataFrame(np.ones((2, 2)), list("ab"), list("cd"))
18471848
tm.assert_frame_equal(result, expected)
1849+
1850+
1851+
def test_frame_op_subclass_nonclass_constructor():
1852+
# GH#43201 subclass._constructor is a function, not the subclass itself
1853+
1854+
class SubclassedSeries(Series):
1855+
@property
1856+
def _constructor(self):
1857+
return SubclassedSeries
1858+
1859+
@property
1860+
def _constructor_expanddim(self):
1861+
return SubclassedDataFrame
1862+
1863+
class SubclassedDataFrame(DataFrame):
1864+
_metadata = ["my_extra_data"]
1865+
1866+
def __init__(self, my_extra_data, *args, **kwargs):
1867+
self.my_extra_data = my_extra_data
1868+
super().__init__(*args, **kwargs)
1869+
1870+
@property
1871+
def _constructor(self):
1872+
return functools.partial(type(self), self.my_extra_data)
1873+
1874+
@property
1875+
def _constructor_sliced(self):
1876+
return SubclassedSeries
1877+
1878+
sdf = SubclassedDataFrame("some_data", {"A": [1, 2, 3], "B": [4, 5, 6]})
1879+
result = sdf * 2
1880+
expected = SubclassedDataFrame("some_data", {"A": [2, 4, 6], "B": [8, 10, 12]})
1881+
tm.assert_frame_equal(result, expected)
1882+
1883+
result = sdf + sdf
1884+
tm.assert_frame_equal(result, expected)

0 commit comments

Comments
 (0)