Repository: pandas-dev/pandas

Issue:
Subclassing is lost when using DataFrame.apply
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd

# Define a subclass of Series
class ExtendedSeries( pd.Series ):
    @property
    def _constructor(self):
        return ExtendedSeries

# Define a subclass of DataFrame that slices into the ExtendedSeries class
class ExtendedFrame( pd.DataFrame ):
    @property
    def _constructor(self):
        return ExtendedFrame
    _constructor_sliced = ExtendedSeries

# Declare instance of ExtendedFrame
df = ExtendedFrame({"c1": [1, 2, 3, 4], "c2": [2, 3, 4, 5], "c3": [3, 4, 5, 6]})

print(type(df))
# <class '__main__.ExtendedFrame'>

print(type(df.iloc[0]))
# <class '__main__.ExtendedSeries'>

for x in df.apply(lambda x: type(x), axis=1):
    print(x)
#<class 'pandas.core.series.Series'>
#<class 'pandas.core.series.Series'>
#<class 'pandas.core.series.Series'>
#<class 'pandas.core.series.Series'>
```

#### Problem description

The class type of the _applied_ objects should still be `<class '__main__.ExtendedSeries'>` instead of going back to `<class 'pandas.core.series.Series'>`.

The issue here is that in `pandas.core.apply`, whenever the returning objects are generated, they are declared as `DataFrame` and `Series` instead of `self.obj._constructor` and `self.obj._constructor_slice`.

This behaviour makes it difficult to properly work with subclasses from pandas. 

#### Expected Output

Following the initial code example, the expected output should be:
```python
import pandas as pd

# Define a subclass of Series
class ExtendedSeries( pd.Series ):
    @property
    def _constructor(self):
        return ExtendedSeries

# Define a subclass of DataFrame that slices into the ExtendedSeries class
class ExtendedFrame( pd.DataFrame ):
    @property
    def _constructor(self):
        return ExtendedFrame
    _constructor_sliced = ExtendedSeries

# Declare instance of ExtendedFrame
df = ExtendedFrame({"c1": [1, 2, 3, 4], "c2": [2, 3, 4, 5], "c3": [3, 4, 5, 6]})

print(type(df))
# <class '__main__.ExtendedFrame'>

print(type(df.iloc[0]))
# <class '__main__.ExtendedSeries'>

for x in df.apply(lambda x: type(x), axis=1):
    print(x)
#<class '__main__.ExtendedSeries'>
#<class '__main__.ExtendedSeries'>
#<class '__main__.ExtendedSeries'>
#<class '__main__.ExtendedSeries'>
```
(note the change in the last printed lines)

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 2.7.12.final.0
python-bits: 64
OS: Darwin
OS-release: 17.4.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.21.1
pytest: None
pip: 9.0.1
setuptools: 38.4.0
Cython: None
numpy: 1.14.0
scipy: 0.18.1
pyarrow: None
xarray: None
IPython: 5.4.1
sphinx: 1.6.6
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 2.1.1
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: 4.5.1
html5lib: 0.999999999
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/apply.py b/pandas/core/apply.py
--- a/pandas/core/apply.py
+++ b/pandas/core/apply.py
@@ -162,7 +162,7 @@ def apply_empty_result(self):
                 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 +175,13 @@ 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)
@@ -232,7 +234,7 @@ def apply_standard(self):
                                           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 +293,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
@@ -379,7 +380,6 @@ 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.index = self.res_index
 


Test patch:
diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py
--- a/pandas/tests/frame/test_subclass.py
+++ b/pandas/tests/frame/test_subclass.py
@@ -514,3 +514,59 @@ 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)
+
+        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],
+            ['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)
+
+        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'])
+
+        result = df.apply(lambda x: strech(x), axis=1)
+        assert isinstance(result, tm.SubclassedDataFrame)
+        tm.assert_frame_equal(result, expected)
+
+        expected = tm.SubclassedDataFrame([
+            [1, 2, 3],
+            [1, 2, 3],
+            [1, 2, 3],
+            [1, 2, 3]])
+
+        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]])
+
+        result = df.apply(lambda x: [1, 2, 3], axis=1)
+        assert not isinstance(result, tm.SubclassedDataFrame)
+        tm.assert_series_equal(result, expected)