Skip to content

Commit fbc8d72

Browse files
jaumebonetharisbal
authored and
harisbal
committed
Keep subclassing in apply (pandas-dev#19823)
1 parent 5d0f3d5 commit fbc8d72

File tree

3 files changed

+66
-8
lines changed

3 files changed

+66
-8
lines changed

doc/source/whatsnew/v0.23.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,10 @@ Other Enhancements
295295
- ``IntervalIndex.astype`` now supports conversions between subtypes when passed an ``IntervalDtype`` (:issue:`19197`)
296296
- :class:`IntervalIndex` and its associated constructor methods (``from_arrays``, ``from_breaks``, ``from_tuples``) have gained a ``dtype`` parameter (:issue:`19262`)
297297
- Added :func:`SeriesGroupBy.is_monotonic_increasing` and :func:`SeriesGroupBy.is_monotonic_decreasing` (:issue:`17015`)
298+
- For subclassed ``DataFrames``, :func:`DataFrame.apply` will now preserve the ``Series`` subclass (if defined) when passing the data to the applied function (:issue:`19822`)
298299
- :func:`DataFrame.from_dict` now accepts a ``columns`` argument that can be used to specify the column names when ``orient='index'`` is used (:issue:`18529`)
299300

301+
300302
.. _whatsnew_0230.api_breaking:
301303

302304
Backwards incompatible API changes

pandas/core/apply.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def apply_empty_result(self):
162162
pass
163163

164164
if reduce:
165-
return Series(np.nan, index=self.agg_axis)
165+
return self.obj._constructor_sliced(np.nan, index=self.agg_axis)
166166
else:
167167
return self.obj.copy()
168168

@@ -175,11 +175,13 @@ def apply_raw(self):
175175
result = np.apply_along_axis(self.f, self.axis, self.values)
176176

177177
# TODO: mixed type case
178-
from pandas import DataFrame, Series
179178
if result.ndim == 2:
180-
return DataFrame(result, index=self.index, columns=self.columns)
179+
return self.obj._constructor(result,
180+
index=self.index,
181+
columns=self.columns)
181182
else:
182-
return Series(result, index=self.agg_axis)
183+
return self.obj._constructor_sliced(result,
184+
index=self.agg_axis)
183185

184186
def apply_broadcast(self, target):
185187
result_values = np.empty_like(target.values)
@@ -232,7 +234,7 @@ def apply_standard(self):
232234
axis=self.axis,
233235
dummy=dummy,
234236
labels=labels)
235-
return Series(result, index=labels)
237+
return self.obj._constructor_sliced(result, index=labels)
236238
except Exception:
237239
pass
238240

@@ -291,8 +293,7 @@ def wrap_results(self):
291293
return self.wrap_results_for_axis()
292294

293295
# dict of scalars
294-
from pandas import Series
295-
result = Series(results)
296+
result = self.obj._constructor_sliced(results)
296297
result.index = self.res_index
297298

298299
return result
@@ -379,7 +380,6 @@ def wrap_results_for_axis(self):
379380
# we have a non-series and don't want inference
380381
elif not isinstance(results[0], ABCSeries):
381382
from pandas import Series
382-
383383
result = Series(results)
384384
result.index = self.res_index
385385

pandas/tests/frame/test_subclass.py

+56
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,59 @@ def test_subclassed_wide_to_long(self):
514514
long_frame = pd.wide_to_long(df, ["A", "B"], i="id", j="year")
515515

516516
tm.assert_frame_equal(long_frame, expected)
517+
518+
def test_subclassed_apply(self):
519+
# GH 19822
520+
521+
def check_row_subclass(row):
522+
assert isinstance(row, tm.SubclassedSeries)
523+
524+
def strech(row):
525+
if row["variable"] == "height":
526+
row["value"] += 0.5
527+
return row
528+
529+
df = tm.SubclassedDataFrame([
530+
['John', 'Doe', 'height', 5.5],
531+
['Mary', 'Bo', 'height', 6.0],
532+
['John', 'Doe', 'weight', 130],
533+
['Mary', 'Bo', 'weight', 150]],
534+
columns=['first', 'last', 'variable', 'value'])
535+
536+
df.apply(lambda x: check_row_subclass(x))
537+
df.apply(lambda x: check_row_subclass(x), axis=1)
538+
539+
expected = tm.SubclassedDataFrame([
540+
['John', 'Doe', 'height', 6.0],
541+
['Mary', 'Bo', 'height', 6.5],
542+
['John', 'Doe', 'weight', 130],
543+
['Mary', 'Bo', 'weight', 150]],
544+
columns=['first', 'last', 'variable', 'value'])
545+
546+
result = df.apply(lambda x: strech(x), axis=1)
547+
assert isinstance(result, tm.SubclassedDataFrame)
548+
tm.assert_frame_equal(result, expected)
549+
550+
expected = tm.SubclassedDataFrame([
551+
[1, 2, 3],
552+
[1, 2, 3],
553+
[1, 2, 3],
554+
[1, 2, 3]])
555+
556+
result = df.apply(lambda x: tm.SubclassedSeries([1, 2, 3]), axis=1)
557+
assert isinstance(result, tm.SubclassedDataFrame)
558+
tm.assert_frame_equal(result, expected)
559+
560+
result = df.apply(lambda x: [1, 2, 3], axis=1, result_type="expand")
561+
assert isinstance(result, tm.SubclassedDataFrame)
562+
tm.assert_frame_equal(result, expected)
563+
564+
expected = tm.SubclassedSeries([
565+
[1, 2, 3],
566+
[1, 2, 3],
567+
[1, 2, 3],
568+
[1, 2, 3]])
569+
570+
result = df.apply(lambda x: [1, 2, 3], axis=1)
571+
assert not isinstance(result, tm.SubclassedDataFrame)
572+
tm.assert_series_equal(result, expected)

0 commit comments

Comments
 (0)