如何对Pandas DataFrame进行子类化?

47

继承Pandas类似乎是一个普遍的需求,但我找不到相关的参考资料。(似乎Pandas开发人员仍在处理这个问题:更容易的子类化 #60。)

有一些关于此主题的SO问题,但我希望这里的某个人可以提供一个更系统的说明,以了解满足两个一般要求的子类 pandas.DataFrame 的当前最佳方式:

  1. 在 MyDF 实例上调用标准 DataFrame 方法应生成 MyDF 实例
  2. 在 MyDF 实例上调用标准 DataFrame 方法应使所有属性仍然连接到输出

(在子类化 pandas.Series 方面是否有任何重大差异?)

用于子类化 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

2
请注意,通常情况下,我认为没有理由去子类化,组合更好用,更灵活,并且提供更多的好处。参见此处一个不错的例子:https://github.com/kjordahl/geopandas。 - Jeff
2
我认为有理由想要子类化,但目前它不起作用,正如链接问题中所述 - 它从未是优先事项(尽管已经做了一些工作...) - Andy Hayden
3
请查看0.16文档这里 - Jeff
2
@Jeff 在我看来,继承是面向对象编程的一个基本特性,与任何人对组合与继承的观点无关。子类化DataFrame的困难使得使用该包变得不那么吸引人,我猜很多人都这样想,从pandas GitHub页面上的问题报告中可以看出。 - Dave Kielpinski
2
@Jeff 我也有一个不简单的代码库。我没有时间去追踪补丁是否已经在所有模块的导入语句中传播。 - Dave Kielpinski
显示剩余5条评论
2个回答

46

现在有一份官方指南,介绍如何对Pandas数据结构进行子类化,包括DataFrame和Series。

该指南可在此处找到:https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extending-subclassing-pandas

该指南提到了来自Geopandas项目的这个子类化DataFrame作为一个很好的示例:https://github.com/geopandas/geopandas/blob/master/geopandas/geodataframe.py

与HYRY的答案中提到的一样,似乎有两件事情你想要实现:

  1. 在调用类实例的方法时,返回正确类型(即你的类型)的实例。为此,只需添加_constructor属性,该属性应返回你的数据类型。
  2. 添加属性,这些属性将附加到对象的副本上。要做到这一点,需要将这些属性的名称存储在列表中,作为特殊的_metadata属性。

下面是一个例子:

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

    @property
    def _constructor(self):
        return SubclassedDataFrame

2
“_metadata” 是指类变量还是实例变量并不明确。这个例子中有一个类变量。有人能解释一下关于 “self.??” 变量的问题吗? - pauljohn32
1
finalize 方法是当对象合并或连接时解决需求2的方法。我通过模仿 GeoPandas 代码发现了这一点,只需要搜索它,修复方案就很清晰明了。 - pauljohn32

18

对于要求1,只需要定义_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)

我认为对于需求2没有简单的解决方案。我认为你需要定义__init__copy或在_constructor中做一些事情,例如:

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

在我看来,你经常希望同时拥有一个相应的Series子类(即使它们是MyDF和MyS,也要以某种方式链接,以便例如mydf.sum()返回一个MyS...)。 - Andy Hayden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接