pandas groupby和rolling_apply忽略了NaN

时间:2016-05-02 17:26:05

标签: python pandas dataframe nan pandas-groupby

我有一个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

3 个答案:

答案 0 :(得分:9)

与熊猫一样,坚持使用矢量化方法(即避免apply)对于性能和可扩展性至关重要。

您想要执行的操作有点繁琐,因为groupby对象上的滚动操作目前不支持NaN(版本0.18.1)。因此,我们需要一些简短的代码:

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

在更大的DataFrame(大约100,000行)上测试它,运行时间不到100毫秒,明显快于我尝试的任何基于应用程序的方法。

可能值得测试您的实际数据的不同方法,因为时间可能会受到其他因素(如组数)的影响。不过,相当肯定矢量化计算会胜出。

上面显示的方法适用于简单的计算,例如滚动平均值。它适用于更复杂的计算(例如滚动标准偏差),尽管实施更为复杂。

一般的想法是查看pandas中快速的每个简单例程(例如sum),然后使用标识元素(例如0)填充任何空值。然后,您可以使用groubpy并执行滚动操作(例如.rolling(2).sum())。然后将输出与其他操作的输出组合。

例如,要实现 group by NaN-aware滚动方差(标准偏差是平方根),我们必须找到“平方的平均值减去平均值的平方”。以下是这可能是什么的草图:

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's algorithm来缓解此问题。

无论如何,这个功能虽然使用了几个操作,但仍然非常快。以下是Yakym Pirozhenko建议的更简洁的基于申请的方法的比较:

>>> 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,因为它允许您以通用/简洁为代价而牺牲性能。

答案 1 :(得分:1)

这个结果能否符合您的期望? 我用min_periods参数和右边的过滤器稍微改变了你的解决方案。

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

答案 2 :(得分:1)

这是一个没有列表理解的替代实现,但它也无法使用np.nan

填充输出的第一个条目
means = df.groupby('var1')['value'].apply(
    lambda gp: gp.rolling(2, min_periods=1).apply(np.nanmean))