45

Subclassing Pandas classes seems a common need, but I could not find references on the subject. (It seems that Pandas developers are still working on it: Easier subclassing #60.)

There are some SO questions on the subject, but I am hoping that someone here can provide a more systematic account on the current best way to subclass pandas.DataFrame that satisfies two general requirements:

  1. calling standard DataFrame methods on instances of MyDF should produce instances of MyDF
  2. calling standard DataFrame methods on instances of MyDF should leave all attributes still attached to the output

(And are there any significant differences for subclassing pandas.Series?)

Code for subclassing pd.DataFrame:

import numpy as np
import pandas as pd

class MyDF(pd.DataFrame):
    # how to subclass pandas DataFrame?
    pass

mydf = MyDF(np.random.randn(3,4), columns=['A','B','C','D'])
print(type(mydf))  # <class '__main__.MyDF'>

# Requirement 1: Instances of MyDF, when calling standard methods of DataFrame,
# should produce instances of MyDF.
mydf_sub = mydf[['A','C']]
print(type(mydf_sub))  # <class 'pandas.core.frame.DataFrame'>

# Requirement 2: Attributes attached to instances of MyDF, when calling standard
# methods of DataFrame, should still attach to the output.
mydf.myattr = 1
mydf_cp1 = MyDF(mydf)
mydf_cp2 = mydf.copy()
print(hasattr(mydf_cp1, 'myattr'))  # False
print(hasattr(mydf_cp2, 'myattr'))  # False
smci
  • 32,567
  • 20
  • 113
  • 146
Lei
  • 733
  • 1
  • 5
  • 13
  • 2
    see here for a nice example: https://github.com/kjordahl/geopandas; note that in general IMHO their isn't a reason to ever sub-class, composition works much better, is more flexible, and offers more benefits. – Jeff Mar 03 '14 at 20:00
  • 2
    I think there are reasons to want to subclass, atm it doesn't work, as stated in the linked issue - it's never been priority (though some work has been done towards it...) – Andy Hayden Mar 03 '14 at 22:06
  • @Jeff: Can you please recommend a way to use composition on `pandas.DataFrame`? Thanks! (Please also see [here](http://stackoverflow.com/questions/29569005/error-in-copying-a-composite-object-consisting-mostly-of-pandas-dataframe).) – Lei Jun 01 '15 at 20:52
  • 3
    See the 0.16 docs [here](http://pandas.pydata.org/pandas-docs/stable/internals.html#subclassing-pandas-data-structures) – Jeff Jun 01 '15 at 21:29
  • @Jeff: Thank you for the useful link! However, I don't see a recommended way of composition there. So do you know what goes wrong in the error I got in the link I gave in my last comment? Thanks! – Lei Jun 05 '15 at 20:08
  • 2
    @Jeff It seems to me that inheritance is a fundamental feature of object oriented programming, independent of anyone's views about composition vs inheritance. The difficulty of subclassing DataFrame makes using the package significantly less attractive to me and I guess many others, judging from the issue reports on the pandas GitHub page. – Dave Kielpinski Aug 10 '17 at 23:37
  • https://en.wikipedia.org/wiki/Composition_over_inheritance#Drawbacks succinctly summarizes the issue. I just want to add one little method to DataFrame... – Dave Kielpinski Aug 10 '17 at 23:41
  • so patch in a method; subclassing is almost always a bad idea for a rich complex object – Jeff Aug 11 '17 at 00:03
  • 2
    @Jeff I also have a nontrivial codebase. I am not in a position to chase down whether the patch has propagated through all the import statements in all the modules. – Dave Kielpinski Aug 11 '17 at 00:20
  • For Requirement 2, `pd.DataFrame` inherits its `copy()` method from [`NDFrame`](https://github.com/pandas-dev/pandas/blob/main/pandas/core/generic.py#L199). Look at its`_attrs` builtin attribute. – smci Feb 12 '22 at 01:11

2 Answers2

44

There is now an official guide on how to subclass Pandas data structures, which includes DataFrame as well as Series.

The guide is available here: https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extending-subclassing-pandas

The guide mentions this subclassed DataFrame from the Geopandas project as a good example: https://github.com/geopandas/geopandas/blob/master/geopandas/geodataframe.py

As in HYRY's answer, it seems there are two things you're trying to accomplish:

  1. When calling methods on an instance of your class, return instances of the correct type (your type). For this, you can just add the _constructor property which should return your type.
  2. Adding attributes which will be attached to copies of your object. To do this, you need to store the names of these attributes in a list, as the special _metadata attribute.

Here's an example:

class SubclassedDataFrame(DataFrame):
    _metadata = ['added_property']
    added_property = 1  # This will be passed to copies

    @property
    def _constructor(self):
        return SubclassedDataFrame
as - if
  • 2,729
  • 1
  • 20
  • 26
cjrieds
  • 827
  • 8
  • 13
  • 2
    It is ambiguous whether `_metadata` refers to class variables or instance variables. This example has a class var. Can somebody clarify about `self.??` vars? – pauljohn32 Mar 28 '21 at 04:14
  • 1
    The `finalize` method solves Requirement 2 when objects are merged or concat-ed. I figured out by imitating the GeoPandas code, just search for it and the fix is pretty clear to see. – pauljohn32 Apr 08 '21 at 19:12
18

For Requirement 1, just define _constructor:

import pandas as pd
import numpy as np

class MyDF(pd.DataFrame):
    @property
    def _constructor(self):
        return MyDF


mydf = MyDF(np.random.randn(3,4), columns=['A','B','C','D'])
print type(mydf)

mydf_sub = mydf[['A','C']]
print type(mydf_sub)

I think there is no simple solution for Requirement 2. I think you need define __init__, copy, or do something in _constructor, for example:

import pandas as pd
import numpy as np

class MyDF(pd.DataFrame):
    _attributes_ = "myattr1,myattr2"

    def __init__(self, *args, **kw):
        super(MyDF, self).__init__(*args, **kw)
        if len(args) == 1 and isinstance(args[0], MyDF):
            args[0]._copy_attrs(self)

    def _copy_attrs(self, df):
        for attr in self._attributes_.split(","):
            df.__dict__[attr] = getattr(self, attr, None)

    @property
    def _constructor(self):
        def f(*args, **kw):
            df = MyDF(*args, **kw)
            self._copy_attrs(df)
            return df
        return f

mydf = MyDF(np.random.randn(3,4), columns=['A','B','C','D'])
print type(mydf)

mydf_sub = mydf[['A','C']]
print type(mydf_sub)

mydf.myattr1 = 1
mydf_cp1 = MyDF(mydf)
mydf_cp2 = mydf.copy()
print mydf_cp1.myattr1, mydf_cp2.myattr1
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
HYRY
  • 94,853
  • 25
  • 187
  • 187
  • It seems to me that you'd often what to have a corresponding subclass of Series at the same time (i.e. have them MyDF and MyS link in some way so e.g. mydf.sum() returns a MyS...) – Andy Hayden Mar 04 '14 at 02:16