From 0df868934d8d205c27e5014dd5600d6595d11cf6 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Wed, 21 Feb 2018 19:47:18 +0100 Subject: [PATCH 01/13] Keep subclassing in apply When generating new objects from apply, it calls the self.obj._constructor, self.obj._constructior_sliced instead of DataFrame, Series; keeping subclassing. --- pandas/core/apply.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/pandas/core/apply.py b/pandas/core/apply.py index c65943fbbb201..082977077902e 100644 --- a/pandas/core/apply.py +++ b/pandas/core/apply.py @@ -151,18 +151,17 @@ def apply_empty_result(self): # we may need to infer reduce = self.result_type == 'reduce' - from pandas import Series if not reduce: - EMPTY_SERIES = Series([]) + EMPTY_SERIES = self.obj._constructor_sliced([]) try: r = self.f(EMPTY_SERIES, *self.args, **self.kwds) - reduce = not isinstance(r, Series) + reduce = not isinstance(r, self.obj._constructor_sliced) except Exception: pass if reduce: - return Series(np.nan, index=self.agg_axis) + return self.obj._constructor_sliced(np.nan, index=self.agg_axis) else: return self.obj.copy() @@ -175,11 +174,10 @@ def apply_raw(self): result = np.apply_along_axis(self.f, self.axis, self.values) # TODO: mixed type case - from pandas import DataFrame, Series if result.ndim == 2: - return DataFrame(result, index=self.index, columns=self.columns) + return self.obj._constructor(result, index=self.index, columns=self.columns) else: - return Series(result, index=self.agg_axis) + return self.obj._constructor_sliced(result, index=self.agg_axis) def apply_broadcast(self, target): result_values = np.empty_like(target.values) @@ -220,19 +218,18 @@ def apply_standard(self): not self.dtypes.apply(is_extension_type).any()): # Create a dummy Series from an empty array - from pandas import Series values = self.values index = self.obj._get_axis(self.axis) labels = self.agg_axis empty_arr = np.empty(len(index), dtype=values.dtype) - dummy = Series(empty_arr, index=index, dtype=values.dtype) + dummy = self.obj._constructor_sliced(empty_arr, index=index, dtype=values.dtype) try: result = reduction.reduce(values, self.f, axis=self.axis, dummy=dummy, labels=labels) - return Series(result, index=labels) + return self.obj._constructor_sliced(result, index=labels) except Exception: pass @@ -291,8 +288,7 @@ def wrap_results(self): return self.wrap_results_for_axis() # dict of scalars - from pandas import Series - result = Series(results) + result = self.obj._constructor_sliced(results) result.index = self.res_index return result @@ -378,9 +374,8 @@ def wrap_results_for_axis(self): # we have a non-series and don't want inference elif not isinstance(results[0], ABCSeries): - from pandas import Series - result = Series(results) + result = self.obj._constructor_sliced(results) result.index = self.res_index # we may want to infer results From c3c1e5fea4246f5ac5d96a1d0c986d18f3e3f3f7 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Wed, 21 Feb 2018 22:28:02 +0100 Subject: [PATCH 02/13] apply changes due to tests break --- pandas/core/apply.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pandas/core/apply.py b/pandas/core/apply.py index 082977077902e..211d90163944f 100644 --- a/pandas/core/apply.py +++ b/pandas/core/apply.py @@ -152,11 +152,11 @@ def apply_empty_result(self): reduce = self.result_type == 'reduce' if not reduce: - - EMPTY_SERIES = self.obj._constructor_sliced([]) + from pandas import Series + EMPTY_SERIES = Series([]) try: r = self.f(EMPTY_SERIES, *self.args, **self.kwds) - reduce = not isinstance(r, self.obj._constructor_sliced) + reduce = not isinstance(r, Series) except Exception: pass @@ -175,9 +175,12 @@ def apply_raw(self): # TODO: mixed type case if result.ndim == 2: - return self.obj._constructor(result, index=self.index, columns=self.columns) + return self.obj._constructor(result, + index=self.index, + columns=self.columns) else: - return self.obj._constructor_sliced(result, index=self.agg_axis) + return self.obj._constructor_sliced(result, + index=self.agg_axis) def apply_broadcast(self, target): result_values = np.empty_like(target.values) @@ -218,11 +221,12 @@ def apply_standard(self): not self.dtypes.apply(is_extension_type).any()): # Create a dummy Series from an empty array + from pandas import Series values = self.values index = self.obj._get_axis(self.axis) labels = self.agg_axis empty_arr = np.empty(len(index), dtype=values.dtype) - dummy = self.obj._constructor_sliced(empty_arr, index=index, dtype=values.dtype) + dummy = Series(empty_arr, index=index, dtype=values.dtype) try: result = reduction.reduce(values, self.f, @@ -374,15 +378,15 @@ def wrap_results_for_axis(self): # we have a non-series and don't want inference elif not isinstance(results[0], ABCSeries): - - result = self.obj._constructor_sliced(results) + from pandas import Series + result = Series(results) result.index = self.res_index # we may want to infer results else: result = self.infer_to_same_shape() - return result + return self.obj._constructor_sliced(result) def infer_to_same_shape(self): """ infer the results to the same shape as the input object """ From 0ef4bcfa6972a76db0da78c8983aa5cac545517b Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 00:12:40 +0100 Subject: [PATCH 03/13] FrameColumnApply.fix errors in wrap_result_for_axis --- pandas/core/apply.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/apply.py b/pandas/core/apply.py index 211d90163944f..71e327180b9a3 100644 --- a/pandas/core/apply.py +++ b/pandas/core/apply.py @@ -151,8 +151,8 @@ def apply_empty_result(self): # we may need to infer reduce = self.result_type == 'reduce' + from pandas import Series if not reduce: - from pandas import Series EMPTY_SERIES = Series([]) try: r = self.f(EMPTY_SERIES, *self.args, **self.kwds) @@ -386,7 +386,7 @@ def wrap_results_for_axis(self): else: result = self.infer_to_same_shape() - return self.obj._constructor_sliced(result) + return result def infer_to_same_shape(self): """ infer the results to the same shape as the input object """ From 7fd1178682b6e09e72c8aabc77d262770e0d4e2c Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 08:23:51 +0100 Subject: [PATCH 04/13] Adding test and release note --- doc/source/whatsnew/v0.23.0.txt | 1 + pandas/core/apply.py | 1 + pandas/tests/frame/test_subclass.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index a4b943f995a33..2115b3f32fe51 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -295,6 +295,7 @@ Other Enhancements - ``IntervalIndex.astype`` now supports conversions between subtypes when passed an ``IntervalDtype`` (:issue:`19197`) - :class:`IntervalIndex` and its associated constructor methods (``from_arrays``, ``from_breaks``, ``from_tuples``) have gained a ``dtype`` parameter (:issue:`19262`) - Added :func:`SeriesGroupBy.is_monotonic_increasing` and :func:`SeriesGroupBy.is_monotonic_decreasing` (:issue:`17015`) +- :func:``DataFrame.apply`` keeps provides the specified ``Series`` subclass when `DataFrame._constructor_sliced`` is defined (:issue:`19822`) .. _whatsnew_0230.api_breaking: diff --git a/pandas/core/apply.py b/pandas/core/apply.py index 71e327180b9a3..9056f78ee02ed 100644 --- a/pandas/core/apply.py +++ b/pandas/core/apply.py @@ -153,6 +153,7 @@ def apply_empty_result(self): from pandas import Series if not reduce: + EMPTY_SERIES = Series([]) try: r = self.f(EMPTY_SERIES, *self.args, **self.kwds) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index c52b512c2930a..188f9618ed14a 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -514,3 +514,20 @@ def test_subclassed_wide_to_long(self): long_frame = pd.wide_to_long(df, ["A", "B"], i="id", j="year") tm.assert_frame_equal(long_frame, expected) + + def test_subclassed_apply(self): + #GH 19822 + + def check_row_subclass( row ): + assert isinstance(row, tm.SubclassedSeries) + + df = tm.SubclassedDataFrame({ + ['John', 'Doe', 'height', 5.5], + ['Mary', 'Bo', 'height', 6.0], + ['John', 'Doe', 'weight', 130], + ['Mary', 'Bo', 'weight', 150]], + columns=['first', 'last', 'variable', 'value'] + }) + + df.apply(lambda x: check_row_subclass(x)) + df.apply(lambda x: check_row_subclass(x), axis=1) From 021e681be2b84094bb9f40cf9f3db963e57aa533 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 09:19:13 +0100 Subject: [PATCH 05/13] fix typo --- pandas/tests/frame/test_subclass.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 188f9618ed14a..7789ba2279ff7 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -521,13 +521,12 @@ def test_subclassed_apply(self): def check_row_subclass( row ): assert isinstance(row, tm.SubclassedSeries) - df = tm.SubclassedDataFrame({ + df = tm.SubclassedDataFrame([ ['John', 'Doe', 'height', 5.5], ['Mary', 'Bo', 'height', 6.0], ['John', 'Doe', 'weight', 130], ['Mary', 'Bo', 'weight', 150]], - columns=['first', 'last', 'variable', 'value'] - }) + columns=['first', 'last', 'variable', 'value']) df.apply(lambda x: check_row_subclass(x)) df.apply(lambda x: check_row_subclass(x), axis=1) From c5493979868ed427b6e925c07f73c82a06a546b8 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 14:21:16 +0100 Subject: [PATCH 06/13] update test_subclassed_apply and release note --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/tests/frame/test_subclass.py | 42 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 2115b3f32fe51..f93d0f17c35b2 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -295,7 +295,7 @@ Other Enhancements - ``IntervalIndex.astype`` now supports conversions between subtypes when passed an ``IntervalDtype`` (:issue:`19197`) - :class:`IntervalIndex` and its associated constructor methods (``from_arrays``, ``from_breaks``, ``from_tuples``) have gained a ``dtype`` parameter (:issue:`19262`) - Added :func:`SeriesGroupBy.is_monotonic_increasing` and :func:`SeriesGroupBy.is_monotonic_decreasing` (:issue:`17015`) -- :func:``DataFrame.apply`` keeps provides the specified ``Series`` subclass when `DataFrame._constructor_sliced`` is defined (:issue:`19822`) +- :func:``DataFrame.apply`` keeps the specified ``Series`` subclass when ``Series`` and ``DataFrame`` subclasses are defined (:issue:`19822`) .. _whatsnew_0230.api_breaking: diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 7789ba2279ff7..5532e53c9c90a 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -516,11 +516,16 @@ def test_subclassed_wide_to_long(self): tm.assert_frame_equal(long_frame, expected) def test_subclassed_apply(self): - #GH 19822 + # GH 19822 - def check_row_subclass( row ): + def check_row_subclass(row): assert isinstance(row, tm.SubclassedSeries) + def strech(row): + if row["variable"] == "height": + row["value"] += 0.5 + return row + df = tm.SubclassedDataFrame([ ['John', 'Doe', 'height', 5.5], ['Mary', 'Bo', 'height', 6.0], @@ -528,5 +533,38 @@ def check_row_subclass( row ): ['Mary', 'Bo', 'weight', 150]], columns=['first', 'last', 'variable', 'value']) + expected1 = tm.SubclassedDataFrame([ + ['John', 'Doe', 'height', 6.0], + ['Mary', 'Bo', 'height', 6.5], + ['John', 'Doe', 'weight', 130], + ['Mary', 'Bo', 'weight', 150]], + columns=['first', 'last', 'variable', 'value']) + + expected2 = tm.SubclassedDataFrame([ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3],]) + + expected3 = DesignSeries([[1,2,3],[1,2,3],[1,2,3],[1,2,3],]) + df.apply(lambda x: check_row_subclass(x)) df.apply(lambda x: check_row_subclass(x), axis=1) + + result1 = df.apply(lambda x: strech(x), axis=1) + assert isinstance(result1, tm.SubclassedDataFrame) + tm.assert_frame_equal(result1, expected1) + + result2 = df.apply(lambda x: DesignSeries([1, 2, 3]), axis=1) + assert isinstance(result2, tm.SubclassedDataFrame) + tm.assert_frame_equal(result2, expected2) + + result3 = df.apply(lambda x: [1, 2, 3], axis=1) + assert not isinstance(result3, tm.SubclassedDataFrame) + tm.assert_series_equal(result3, expected3) + + result4 = df.apply(lambda x: [1, 2, 3], axis=1, result_type="expand") + assert isinstance(result4, tm.SubclassedDataFrame) + tm.assert_frame_equal(result4, expected2) + + From 956143c957b9e397ad32022264749dae7bedec78 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 14:23:44 +0100 Subject: [PATCH 07/13] pep8 styling --- pandas/tests/frame/test_subclass.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 5532e53c9c90a..fc304c5b4aba5 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -544,9 +544,9 @@ def strech(row): [1, 2, 3], [1, 2, 3], [1, 2, 3], - [1, 2, 3],]) + [1, 2, 3]]) - expected3 = DesignSeries([[1,2,3],[1,2,3],[1,2,3],[1,2,3],]) + expected3 = DesignSeries([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) df.apply(lambda x: check_row_subclass(x)) df.apply(lambda x: check_row_subclass(x), axis=1) @@ -566,5 +566,3 @@ def strech(row): result4 = df.apply(lambda x: [1, 2, 3], axis=1, result_type="expand") assert isinstance(result4, tm.SubclassedDataFrame) tm.assert_frame_equal(result4, expected2) - - From 06e11d21bd3c8df865357d51e8c15e1189ac5564 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 16:30:36 +0100 Subject: [PATCH 08/13] test fix --- pandas/tests/frame/test_subclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index fc304c5b4aba5..742419c598d4d 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -546,7 +546,7 @@ def strech(row): [1, 2, 3], [1, 2, 3]]) - expected3 = DesignSeries([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) + expected3 = tm.SubclassedSeries([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) df.apply(lambda x: check_row_subclass(x)) df.apply(lambda x: check_row_subclass(x), axis=1) From e6bac37df88d8400b12513a9a4bc9e5f11d0214f Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 16:31:57 +0100 Subject: [PATCH 09/13] fix test --- pandas/tests/frame/test_subclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 742419c598d4d..0a4895605740b 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -555,7 +555,7 @@ def strech(row): assert isinstance(result1, tm.SubclassedDataFrame) tm.assert_frame_equal(result1, expected1) - result2 = df.apply(lambda x: DesignSeries([1, 2, 3]), axis=1) + result2 = df.apply(lambda x: tm.SubclassedSeries([1, 2, 3]), axis=1) assert isinstance(result2, tm.SubclassedDataFrame) tm.assert_frame_equal(result2, expected2) From e4f1c3e6a2d4fc0c2a4c3cc36800d44d8551e891 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Thu, 22 Feb 2018 20:06:32 +0100 Subject: [PATCH 10/13] fixed lint "line too long" error in travis ci --- pandas/tests/frame/test_subclass.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 0a4895605740b..704fb5767a82e 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -546,7 +546,11 @@ def strech(row): [1, 2, 3], [1, 2, 3]]) - expected3 = tm.SubclassedSeries([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) + expected3 = tm.SubclassedSeries([ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3]]) df.apply(lambda x: check_row_subclass(x)) df.apply(lambda x: check_row_subclass(x), axis=1) From 8f7a0b46f07fcfd3b873682726efcc3d3f3ad271 Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Fri, 23 Feb 2018 07:20:02 +0100 Subject: [PATCH 11/13] fix test styling --- pandas/tests/frame/test_subclass.py | 42 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 704fb5767a82e..7cc7ee8e36e6a 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -533,40 +533,40 @@ def strech(row): ['Mary', 'Bo', 'weight', 150]], columns=['first', 'last', 'variable', 'value']) - expected1 = tm.SubclassedDataFrame([ + df.apply(lambda x: check_row_subclass(x)) + df.apply(lambda x: check_row_subclass(x), axis=1) + + expected = tm.SubclassedDataFrame([ ['John', 'Doe', 'height', 6.0], ['Mary', 'Bo', 'height', 6.5], ['John', 'Doe', 'weight', 130], ['Mary', 'Bo', 'weight', 150]], columns=['first', 'last', 'variable', 'value']) - expected2 = tm.SubclassedDataFrame([ + result = df.apply(lambda x: strech(x), axis=1) + assert isinstance(result, tm.SubclassedDataFrame) + tm.assert_frame_equal(result, expected1) + + expected = tm.SubclassedDataFrame([ [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) - expected3 = tm.SubclassedSeries([ + result = df.apply(lambda x: tm.SubclassedSeries([1, 2, 3]), axis=1) + assert isinstance(result, tm.SubclassedDataFrame) + tm.assert_frame_equal(result, expected) + + result = df.apply(lambda x: [1, 2, 3], axis=1, result_type="expand") + assert isinstance(result, tm.SubclassedDataFrame) + tm.assert_frame_equal(result, expected) + + expected = tm.SubclassedSeries([ [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) - df.apply(lambda x: check_row_subclass(x)) - df.apply(lambda x: check_row_subclass(x), axis=1) - - result1 = df.apply(lambda x: strech(x), axis=1) - assert isinstance(result1, tm.SubclassedDataFrame) - tm.assert_frame_equal(result1, expected1) - - result2 = df.apply(lambda x: tm.SubclassedSeries([1, 2, 3]), axis=1) - assert isinstance(result2, tm.SubclassedDataFrame) - tm.assert_frame_equal(result2, expected2) - - result3 = df.apply(lambda x: [1, 2, 3], axis=1) - assert not isinstance(result3, tm.SubclassedDataFrame) - tm.assert_series_equal(result3, expected3) - - result4 = df.apply(lambda x: [1, 2, 3], axis=1, result_type="expand") - assert isinstance(result4, tm.SubclassedDataFrame) - tm.assert_frame_equal(result4, expected2) + result = df.apply(lambda x: [1, 2, 3], axis=1) + assert not isinstance(result, tm.SubclassedDataFrame) + tm.assert_series_equal(result, expected) From 87f2c5ca26c8e68c6f4807faffcedc8d617f827e Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Fri, 23 Feb 2018 08:47:34 +0100 Subject: [PATCH 12/13] fixed test typo --- pandas/tests/frame/test_subclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 7cc7ee8e36e6a..caaa311e9ee96 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -545,7 +545,7 @@ def strech(row): result = df.apply(lambda x: strech(x), axis=1) assert isinstance(result, tm.SubclassedDataFrame) - tm.assert_frame_equal(result, expected1) + tm.assert_frame_equal(result, expected) expected = tm.SubclassedDataFrame([ [1, 2, 3], From b22e090950e37699ede10ddb5f4130ee19ef215b Mon Sep 17 00:00:00 2001 From: Jaume Bonet Date: Fri, 23 Feb 2018 09:48:18 +0100 Subject: [PATCH 13/13] fix docs --- doc/source/whatsnew/v0.23.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 81d78cc5682da..9dbbe3f9e2b77 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -295,7 +295,7 @@ Other Enhancements - ``IntervalIndex.astype`` now supports conversions between subtypes when passed an ``IntervalDtype`` (:issue:`19197`) - :class:`IntervalIndex` and its associated constructor methods (``from_arrays``, ``from_breaks``, ``from_tuples``) have gained a ``dtype`` parameter (:issue:`19262`) - Added :func:`SeriesGroupBy.is_monotonic_increasing` and :func:`SeriesGroupBy.is_monotonic_decreasing` (:issue:`17015`) -- :func:``DataFrame.apply`` keeps the specified ``Series`` subclass when ``Series`` and ``DataFrame`` subclasses are defined (:issue:`19822`) +- For subclassed ``DataFrames``, :func:`DataFrame.apply` will now preserve the ``Series`` subclass (if defined) when passing the data to the applied function (:issue:`19822`) - :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`)