【问题标题】:Why does subclassing a DataFrame mutate the original object?为什么子类化 DataFrame 会改变原始对象?
【发布时间】:2017-07-21 20:16:49
【问题描述】:

我忽略了warnings 并尝试子类化 pandas DataFrame。我这样做的原因如下:

  • 我想保留DataFrame现有的所有方法。
  • 我想在类实例化时设置一些额外的属性,稍后将用于定义我可以在子类上调用的额外方法。

这是一个sn-p:

class SubFrame(pd.DataFrame):

    def __init__(self, *args, **kwargs):
        freq = kwargs.pop('freq', None)
        ddof = kwargs.pop('ddof', None)
        super(SubFrame, self).__init__(*args, **kwargs)
        self.freq = freq
        self.ddof = ddof
        self.index.freq = pd.tseries.frequencies.to_offset(self.freq)

    @property
    def _constructor(self):
        return SubFrame

这是一个使用示例。假设我有DataFrame

print(df)
               col0     col1     col2
2014-07-31  0.28393  1.84587 -1.37899
2014-08-31  5.71914  2.19755  3.97959
2014-09-30 -3.16015 -7.47063 -1.40869
2014-10-31  5.08850  1.14998  2.43273
2014-11-30  1.89474 -1.08953  2.67830

索引没有频率的地方

print(df.index)
DatetimeIndex(['2014-07-31', '2014-08-31', '2014-09-30', '2014-10-31',
               '2014-11-30'],
              dtype='datetime64[ns]', freq=None)

使用SubFrame 允许我一步指定频率:

sf = SubFrame(df, freq='M')
print(sf.index)
DatetimeIndex(['2014-07-31', '2014-08-31', '2014-09-30', '2014-10-31',
               '2014-11-30'],
              dtype='datetime64[ns]', freq='M')

问题是,这修改了df

print(df.index.freq)
<MonthEnd>

这是怎么回事,我该如何避免这种情况?

此外,我自称使用了我不太了解的copied 代码。上面的__init__ 内发生了什么?有必要在这里使用带有pop 的args/kwargs 吗? (为什么我不能像往常一样指定参数?)

【问题讨论】:

  • 子类化的典型用例是以某种方式修改/扩展基类功能。你不是真的在这里这样做。您只是以特定方式设置 DataFrame。您可能希望简单地创建一个工厂类型函数/对象,而不是子类,该函数/对象只是返回一个以您想要的方式构造的 DataFrame。对于这个特定的用例,子类化似乎没有多大意义。
  • “创建一个工厂类型的函数/对象,它只返回一个以你想要的方式构造的 DataFrame。”你能详细说明一下吗?现在我拥有的是一个标准的Class(object),其中数据框是一个属性。是的,我通过定义多个其他方法来扩展功能,此处未显示。
  • 看看 piRSquared 的第二个建议是用管道。注意他不是子类化。他正在创建一个函数,该函数仅以您希望的方式返回一个 DataFrame。没有必要子类化。你没有改变行为。工厂类的用途之一是以您想要的方式创建对象。如果您想在第一个副本的基础上创建一个新 DataFrame,您将创建一个函数,该函数将现有 DataFrame 作为参数,复制它,添加您的频率,然后返回副本。没有子类。

标签: python python-3.x pandas dataframe subclass


【解决方案1】:

我将添加到警告中。我并不是要劝阻你,我实际上是为你的努力鼓掌。

但是,这不会是您对正在发生的事情的最后一个问题。

也就是说,一旦你运行:

super(SubFrame, self).__init__(*args, **kwargs)

self 是一个真正的数据框。您通过将另一个数据框传递给构造函数来创建它。

试试这个作为实验

d1 = pd.DataFrame(1, list('AB'), list('XY'))
d2 = pd.DataFrame(d1)

d2.index.name = 'IDX'

d1

     X  Y
IDX      
A    1  1
B    1  1

因此观察到的行为是一致的,因为当您通过将另一个数据帧传递给构造函数来构造一个数据帧时,您最终会指向相同的对象。

为了回答您的问题,子类化并不是允许原始对象发生变异的原因......它是 pandas 从传递的数据帧构造数据帧的方式。

通过使用副本实例化来避免这种情况

d2 = pd.DataFrame(d1.copy())

__init__ 中发生了什么

您希望将所有argskwargs 传递给pd.DataFrame.__init__,但用于您的子类的特定kwargs 除外。在这种情况下,freqddofpop 是一种方便的方式来获取值并从 kwargs 中删除键,然后再将其传递给 pd.DataFrame.__init__


我将如何实现pipe

def add_freq(df, freq):
    df = df.copy()
    df.index.freq = pd.tseries.frequencies.to_offset(freq)
    return df

df = pd.DataFrame(dict(A=[1, 2]), pd.to_datetime(['2017-03-31', '2017-04-30']))

df.pipe(add_freq, 'M')

【讨论】:

  • 我关注你,除了“通过使用副本实例化来避免这种情况”。 copy() 会在 super(SubFrame, self) ... 内去哪里,为什么?根据super 的工作原理,我会怀疑self.copy,但这会引发错误
  • 我不确定我是否会让SubFrame.__init__ 强制复制(请阅读,因为我真的不确定。我可以左右摇摆)。因为你会改变 pandas 所做的事情。相反,我会将您创建变量 sf: sf = SubFrame(df, freq='M') 的方式更改为 sf = SubFrame(df.copy(), freq='M')
  • 好的--并且有 2 种替代子类化建议here--你会说其中任何一种都适用于我在这种特定情况下尝试做的事情吗?
  • 我不确定什么是合成,因为我没有研究过很多 CS 概念。但我相信作曲是我所做的。我创建了一个新的对象类,其中数据框是一个属性。我以这种方式管理其他所有事情。我相信pipe 会为您解决问题...我会用一个示例更新我的帖子。
  • 啊,好吧。然后组合就是我现在所拥有的(在我尝试子类化之前)。最终可能会在这里使用pipe,感谢编辑
猜你喜欢
  • 2019-10-14
  • 2018-07-29
  • 1970-01-01
  • 2021-09-29
  • 2015-01-07
  • 2019-07-27
  • 1970-01-01
  • 1970-01-01
  • 2022-01-07
相关资源
最近更新 更多