【问题标题】:Hashing a pandas dataframe for calculated column caching散列用于计算列缓存的 pandas 数据帧
【发布时间】:2020-05-29 03:40:17
【问题描述】:

我正在使用组合方法创建一个包含 pandas 数据框的类,如下所示。我正在通过对基列进行一些操作来创建derived 属性。

import numpy as np
import pandas as pd

class myclass:
    def __init__(self, *args, **kwargs):
        self.df = pd.DataFrame(*args, **kwargs)
    @property
    def derived(self):
        return self.df.sum(axis=1)

myobj = myclass(np.random.randint(100, size=(100,6)))
d = mc.derived

derived 的计算是一个昂贵的步骤,因此我想缓存这个函数。我想用functools.lru_cache 做同样的事情。但是,它要求对原始对象进行散列处理。我尝试为该对象创建一个__hash__ 函数,详见此答案https://stackoverflow.com/a/47800021/3679377

现在我遇到了一个新问题,哈希函数是一个昂贵的步骤!。有没有办法解决这个问题?还是我走到了死胡同?

有没有更好的方法来检查数据帧是否已被修改,如果没有,则继续返回相同的哈希值?

【问题讨论】:

  • '我正在通过扩展 pandas 数据框来创建自定义类,如下所示。' - 你没有延长。您有一个包含数据框的类。见packetflow.co.uk/python-inheritance-vs-composition
  • 没错,我正在使用合成。我会这样重新构建我的问题。只是我去了熊猫帮助页面的标题。 pandas.pydata.org/pandas-docs/stable/development/extending.html
  • self.df没有改变的情况下,是否要避免计算derived
  • 是的。没错。
  • 您想只处理derived 操作还是希望拥有一个可以扩展到此数据帧上的其他一些操作的系统?

标签: python pandas oop hash


【解决方案1】:

如果散列对您不起作用,您可以尝试利用类的内部状态。

缓存一种方法

使用类属性作为缓存:在第一次调用方法时,将结果存储到该属性中,并在后续调用时检索它。

import pandas as pd

class MyClass:
    def __init__(self, *args, **kwargs):
        self._df = pd.DataFrame(*args, **kwargs)
        self._cached_value = None

    @property
    def df(self):
        return self._df

    @df.setter
    def df(self, value):
        self._cached_value = None
        self._df = value

    @property
    def derived(self):
        if self._cached_value is None:
            self._cached_value = self._df.sum(axis=1)
        return self._cached_value

cl = MyClass()
cl.derived  # compute
cl.derived  # return cached value

cl.df = my_new_df_value  # cache is emptied
cl.derived  # compute

缓存几种方法

然后您可以将此原则扩展到使用dict 存储每个操作的结果的几种方法。您可以使用方法名称作为此 dict 的键(感谢模块 inspect,示例参见 this response)。

import pandas as pd
import inspect

class MyClass:
    def __init__(self, *args, **kwargs):
        self.df = pd.DataFrame(*args, **kwargs)
        self._cached_values = {}

    @property
    def derived(self):
        method_name = self._get_method_name()
        if method_name not in self._cached_values:
            self._cached_value[method_name] = self.df.sum(axis=1)
        return self._cached_value[method_name]

    @property
    def derived_bis(self):
        method_name = self._get_method_name()
        if method_name not in self._cached_values:
            self._cached_value[method_name] = your_expensive_op
        return self._cached_value[method_name]

    def _get_method_name(self):
        return inspect.stack()[1][3]  # returns the name of this method's caller


cl = MyClass()
cl.derived  # compute  --> self._cached_value = {'derived': your_result}
cl.derived  # return cached value

cl.derived_bis # compute  --> self._cached_value = {'derived': your_result, 'derived_bis': your_other_result}
cl.derived_bis # return cached value

您可以分解这两个属性的主体以遵守 DRY 原则,但请务必相应地修改 _get_method_name

【讨论】:

  • 如果数据帧在随后的派生调用之间发生更改,这将不起作用!例如:c1 = MyClass(); c1.derived; c1.df*=10; c1.derived 会给我已经缓存的错误数据。当我修改 df 时,代码应该知道足以丢弃缓存。
  • 啊,是的,我不明白这是一个要求,我的错。但是如果在更新数据帧的值时清空缓存,它仍然可以工作。这可以作为setter 的第一步完成:)
  • 没错。那么班级什么时候知道“现在我的数据框已经改变了”?我试图使用lru_cache 来实现这一切。但是,它要求我计算一个哈希值。我可以根据易于计算的内容为数据帧设置哈希值,例如:df 中所有值的总和。但这不是万无一失的。任何像样的哈希值都需要与 derived 属性本身一样多的时间。
  • 我已经编辑了答案的第一部分以描述完整的机制。它与您正在寻找的相似吗?如果是,我将相应地编辑其余答案。另外我认为derived 方法不应该是properties,所以我将它们删除以更清晰。
  • 我认为上面的解决方案 + 添加哈希检查,即 (pandas.util.hash_pandas_object(df) -- 在每次调用派生时都会起作用。每次哈希都是适度的开销,但如果你需要检测更改我没有看到其他方法。我认为数据框没有事件模型。
【解决方案2】:

如果您知道哪些方法可能会更新您的 df,您可以在您的自定义类中覆盖它们,并保留一个标志。 这里我就不细说了,基本原理如下:

import numpy as np
import pandas as pd

class myclass:
    def __init__(self, *args, **kwargs):
        self.df = pd.DataFrame(*args, **kwargs)
        self.derived_is_calculated = False
        
    @property
    def derived(self):
        if not self.derived_is_calculated:
            d = self.df.sum(axis=1)
            self.derived_is_calculated = True
            return d

    def update(self, other, **kwargs):
        """ Implements the normal update method, and sets a flag to track if df has changed """
        old_df = self.df.copy()  # Make a copy for comparison
        pd.DataFrame.update(self.df, other, **kwargs) # Call the base'update' method
        if not self.df.equals(old_df): # Compare before and after update
            self.derived_is_calculated = False
        
random_array = np.random.randint(100, size=(2,10))
myobj = myclass(random_array)

print(myobj.derived) # Prints the summed df
print(myobj.derived) # Prints None

myobj.update([1,2,3])
print(myobj.derived) # Prints the new summed df

DataFrame 内容的每次更改都会调用 DataFrame 或 pandas 的更深层次的方法,我会继续寻找。

但是您可以设置程序将使用的方法列表,并制作一个装饰器来完成我在update 中所做的基本操作,并在每个列出的方法上调用它...

【讨论】:

  • 谢谢,但我不知道用户将如何修改数据框。这是一个普通的 pandas 数据框,我相信有很多方法可以修改它。
  • 其实我相信pd.DataFrame 的所有更新都会通过__setitem__ 方法(虽然我没有彻底检查)。
猜你喜欢
  • 2017-10-27
  • 1970-01-01
  • 1970-01-01
  • 2016-01-14
  • 2017-09-30
  • 1970-01-01
  • 2018-09-19
  • 2013-01-15
  • 1970-01-01
相关资源
最近更新 更多