pandas按组分组并滚动应用,忽略NaN值

10
我有一个pandas数据框,想要在经过groupby处理的列上计算滚动平均值。然而,我希望排除NaN值。 例如,如果groupby返回[2, NaN, 1],结果应该是1.5,但目前返回NaN。 我尝试了以下方法,但好像不起作用:
df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 3,  lambda x: np.mean([i for i in x if i is not np.nan and i!='NaN']))

如果我尝试这样做:

df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 3,  lambda x: 1)

输出结果中出现NaN,因此可能与pandas在后台的工作方式有关。

有任何想法吗?

编辑: 这是我尝试做的一个代码示例:

import pandas as pd
import numpy as np

df = pd.DataFrame({'var1' : ['a', 'b', 'a', 'b', 'a', 'b', 'a', 'b'], 'value' : [1, 2, 3, np.nan, 2, 3, 4, 1] })
print df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 2,  lambda x: np.mean([i for i in x if i is not np.nan and i!='NaN']))

结果是:

0    NaN
1    NaN
2    2.0
3    NaN
4    2.5
5    NaN
6    3.0
7    2.0

我想要的是以下内容:

0    NaN
1    NaN
2    2.0
3    2.0
4    2.5
5    3.0
6    3.0
7    2.0

1
请提供一个小的可重现的代码集,以便我可以使用类似的信息进行测试,而不必自己编写。 - piRSquared
@piRSquared,我刚刚添加了一段代码示例。谢谢。 - Stergios
3个回答

11

像往常一样,在pandas中,坚持使用向量化的方法(即避免使用apply)对于性能和可扩展性至关重要。

您想要执行的操作有点棘手,因为目前(版本0.18.1),在groupby对象上进行滚动操作时不会考虑NaN。因此,我们需要几行简短的代码:

g1 = df.groupby(['var1'])['value']              # group values  
g2 = df.fillna(0).groupby(['var1'])['value']    # fillna, then group values

s = g2.rolling(2).sum() / g1.rolling(2).count() # the actual computation

s.reset_index(level=0, drop=True).sort_index()  # drop/sort index

这个想法是对窗口中的值求和(使用sum),计算NaN值的数量(使用count),然后除以总数得到平均值。这段代码输出与您期望的输出相匹配:
0    NaN
1    NaN
2    2.0
3    2.0
4    2.5
5    3.0
6    3.0
7    2.0
Name: value, dtype: float64

在一个更大的数据框(约10万行)上测试,运行时间不到100毫秒,比我尝试过的任何基于apply的方法都要快得多。

值得一试的是,在实际数据上测试不同的方法,因为计时可能会受到其他因素的影响,例如组数。虽然很有把握向量化计算会胜出。


上述方法适用于简单的计算,例如滚动平均值。它也适用于更复杂的计算(例如滚动标准差),尽管实现更为复杂。
一般思路是查看每个在pandas中快速的简单例程(例如sum),然后用一个身份元素(例如0)填充任何空值。然后可以使用groupby执行滚动操作(例如.rolling(2).sum())。然后将输出与其他操作的输出组合。
例如,要实现“groupby NaN-aware rolling variance”(其中标准差是平方根),我们必须找到“平方的均值减去均值的平方”。以下是这个过程的草图:
def rolling_nanvar(df, window):
    """
    Group df by 'var1' values and then calculate rolling variance,
    adjusting for the number of NaN values in the window.

    Note: user may wish to edit this function to control degrees of
    freedom (n), depending on their overall aim.
    """
    g1 = df.groupby(['var1'])['value']
    g2 = df.fillna(0).groupby(['var1'])['value']
    # fill missing values with 0, square values and groupby
    g3 = df['value'].fillna(0).pow(2).groupby(df['var1'])

    n = g1.rolling(window).count()

    mean_of_squares = g3.rolling(window).sum() / n
    square_of_mean = (g2.rolling(window).sum() / n)**2
    variance = mean_of_squares - square_of_mean
    return variance.reset_index(level=0, drop=True).sort_index()

请注意,此函数可能不具有数值稳定性(平方可能导致溢出)。Pandas 在内部使用 Welford 算法来缓解此问题。
无论如何,此函数虽然使用了多个操作,但仍非常快速。以下是与 Yakym Pirozhenko建议的更简洁的 apply-based 方法进行比较:
>>> df2 = pd.concat([df]*10000, ignore_index=True) # 80000 rows
>>> %timeit df2.groupby('var1')['value'].apply(\
         lambda gp: gp.rolling(7, min_periods=1).apply(np.nanvar))
1 loops, best of 3: 11 s per loop

>>> %timeit rolling_nanvar(df2, 7)
10 loops, best of 3: 110 ms per loop

在这种情况下,向量化速度比应用函数快100倍。当然,根据你拥有的数据量,你可能希望继续使用apply,因为它可以在性能上以一定的通用性/简洁为代价。

请注意,此处使用的是“rolling”方法,该方法仅适用于pandas 18及以上版本,而OP使用了“pd.rolling_apply”,因此很可能安装的是pandas 17或更低版本。 - IanS
@ajcr 这解决了我的当前问题,但如果我想应用除平均值之外的其他函数(例如滚动标准差),则需要完全重写。有没有办法让它也适用于其他函数? - Stergios
@Stergios:我会再仔细思考一下这个问题,并在今天/明天向这个答案添加更多的指导/建议。当然,使用apply有时是最方便的选择,并提供了最通用的解决方案(并非所有函数都易于使用加速的pandas例程模拟)。 - Alex Riley
@Stergios:添加了一个滚动方差的代码示例(您可以从中获取标准差),以及一个时间比较。如果您需要我添加其他内容,请告诉我。 - Alex Riley

1

这个结果是否符合您的期望?我稍微更改了您的解决方案,使用了min_periods参数和适用于nan的right过滤器。

In [164]: df.groupby(by=['var1'])['value'].apply(pd.rolling_apply, 2,  lambda x: np.mean([i for i in x if not np.isnan(i)]), min_periods=1)
Out[164]: 
0    1.0
1    2.0
2    2.0
3    2.0
4    2.5
5    3.0
6    3.0
7    2.0
dtype: float64

这是 min_period 的巧妙运用! - IanS

1
这是一种不使用列表推导的替代实现,但它也无法将输出的第一个条目填充为np.nan
means = df.groupby('var1')['value'].apply(
    lambda gp: gp.rolling(2, min_periods=1).apply(np.nanmean))

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