如何将不返回数值的函数应用于pandas滚动窗口?

8

我有一个浮点数类型的日期时间系列。我试图在该系列上应用一个自定义函数来执行滚动窗口操作。我希望此函数返回字符串。然而,这会生成TypeError错误。为什么会产生这个错误?是否有一种方法可以直接使用一个函数来使其正常工作?

以下是一个示例:

import numpy as np
import pandas as pd

np.random.seed(1)
number_series = pd.Series(np.random.randint(low=1,high=100,size=100),index=[pd.date_range(start='2000-01-01',freq='W',periods=100)])
number_series = number_series.apply(lambda x: float(x))

def func(s):
    
    if s[-1] > s[-2] > s[-3]:
        return 'High'
    elif s[-1] > s[-2]:
        return 'Medium'
    else:
        return 'Low'

new_series = number_series.rolling(5).apply(func)

以下是错误的结果:
TypeError: must be real number, not str

目前我使用的解决方法是修改该函数,使其输出整数序列,然后将另一个函数应用于此序列以生成新序列。示例如下:
def func_float(s):
    
    if s[-1] > s[-2] > s[-3]:
        return 1
    elif s[-1] > s[-2]:
        return 2
    else:
        return 3
    
float_series = number_series.rolling(5).apply(func_float)

def func_text(s):

    if s == 1:
        return 'High'
    elif s == 2:
        return 'Medium'
    else:
        return 'Low'
    
new_series = float_series.apply(func_text)

这将得到最初代码所产生错误的预期结果:
new_series

2000-01-02       Low
2000-01-09       Low
2000-01-16       Low
2000-01-23       Low
2000-01-30    Medium
               ...  
2001-10-28       Low
2001-11-04    Medium
2001-11-11      High
2001-11-18      High
2001-11-25       Low
Length: 100, dtype: object

我认为你的问题源于numpy系列必须始终包含相同类型的数据,因此当你尝试将第一个浮点数转换为字符串时会出现错误。 - itprorh66
这就是令人困惑的地方。因为在两步法中,第二步是将数据类型从浮点数更改为字符串。也许将其与滚动方法一起包装会导致某种问题。 - agftrading
pandas 版本? - Carlo Zanocco
pandas版本1.1.5 @CarloZanocco - agftrading
3个回答

5
请注意,对于Rolling对象apply函数与Series对象apply函数不同,我同意您的观点,这有点令人困惑。据我理解,适用于滚动窗口的函数通常是用于数据聚合(例如sumcount等)。
但是,您可以将滚动窗口转换为列表,并将该列表应用于函数中(感谢这个讨论)。因此,我的方法是:
import numpy as np
import pandas as pd

np.random.seed(1)
number_series = pd.Series(np.random.randint(low=1,high=100,size=100),index=[pd.date_range(start='2000-01-01',freq='W',periods=100)])
number_series = number_series.apply(lambda x: float(x))

def func(s):
    if len(s) > 2:
        if s[-1] > s[-2] > s[-3]:
            return 'High'
        elif s[-1] > s[-2]:
            return 'Medium'
        else:
            return 'Low'
    else:
        return ''

list = [func(window) for window in list(number_series.rolling(5))]
new_series = pd.Series(list, index=number_series.index)

需要注意的是,func 需要对第一个项目进行不同处理,否则索引将超出范围。


问题评估得很好!我认为解决方案只需要一行代码: new_series = pd.Series(number_series.rolling(5)).apply(func) - Dustin Michels
@DustinMichels:非常好!但是这会创建一个全新的Series并且会丢失原始的时间戳索引,所以你仍然需要注意这一点。 - Gerd

1
一种方法是:
  1. 获取WindowIndexerrolling()方法。
  2. 应用返回字符串的func并将结果存储为列表。
  3. 将结果转换回系列。
import numpy as np
import pandas as pd

np.random.seed(1)
number_series = pd.Series(np.random.randint(low=1,high=100,size=100),index=[pd.date_range(start='2000-01-01',freq='W',periods=100)])
number_series = number_series.apply(lambda x: float(x))

def func(s):
    if (len(s) >= 3) and (s[-1] > s[-2] > s[-3]):
        return 'High'
    elif (len(s) >= 2) and s[-1] > s[-2]:
        return 'Medium'
    else:
        return 'Low'
  
# Step 1: Get the window indexer  
window_indexer = number_series.rolling(5)._get_window_indexer()
start, end = window_indexer.get_window_bounds(num_values=len(number_series))

# Step 2: Apply func
results = [func(number_series.iloc[slice(s, e)]) for s, e in zip(start, end)]   

# Step 3: Get results back to a pandas Series
new_series = pd.Series(results, index=number_series.index)

new_series
>>>
2000-01-02       Low
2000-01-09       Low
2000-01-16    Medium
2000-01-23       Low
2000-01-30    Medium
               ...  
2001-10-28       Low
2001-11-04    Medium
2001-11-11      High
2001-11-18      High
2001-11-25       Low
Length: 100, dtype: object

0
这是另一种使用布尔“或”技巧与列表和pd.Series构造函数的方法:
import numpy as np
import pandas as pd

np.random.seed(1)
number_series = pd.Series(np.random.randint(low=1,high=100,size=100),index=[pd.date_range(start='2000-01-01',freq='W',periods=100)])
number_series = number_series.apply(lambda x: float(x))

def func(s):
    
    if s[-1] > s[-2] > s[-3]:
        return 'High'
    elif s[-1] > s[-2]:
        return 'Medium'
    else:
        return 'Low'

l = []
new_series = number_series.rolling(5).apply(lambda x: l.append(func(x)) or 0)

pd.Series(l, index=number_series.index[:len(l)])

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