在Python pandas中自定义rolling_apply函数

3

设置

我有一个包含三列的DataFrame:

  • "Category" 包含 True 和 False,我已经使用 df.groupby('Category') 进行了分组。
  • "Time" 包含时间戳(以秒为单位),记录了数值记录的时间。
  • "Value" 包含数值本身。

在每个时间点,都会记录两个值:一个具有 "True" 类别,另一个具有 "False" 类别。

滚动应用问题

在每个类别组内,我想要计算并存储每个时间点的数字。结果是在时间t-60t之间的数值中,落在1到3之间的百分比.

最简单的方法可能是通过 rolling_count 计算该时间间隔内的总数,然后使用 rolling_apply 仅计算该区间内落在1到3之间的值。

这是我的代码:

groups = df.groupby(['Category'])
for key, grp in groups:
    grp = grp.reindex(grp['Time']) # reindex by time so we can count with rolling windows
    grp['total'] = pd.rolling_count(grp['Value'], window=60) # count number of values in the last 60 seconds
    grp['in_interval'] = ? ## Need to count number of values where 1<v<3 in the last 60 seconds

    grp['Result'] = grp['in_interval'] / grp['total'] # percentage of values between 1 and 3 in the last 60 seconds

什么是适当的 rolling_apply() 调用来查找 grp['in_interval']
2个回答

7

让我们通过一个例子来进行说明:

import pandas as pd
import numpy as np
np.random.seed(1)

def setup(regular=True):
    N = 10
    x = np.arange(N)
    a = np.arange(N)
    b = np.arange(N)

    if regular:
        timestamps = np.linspace(0, 120, N)
    else:
        timestamps = np.random.uniform(0, 120, N)

    df = pd.DataFrame({
        'Category': [True]*N + [False]*N,
        'Time': np.hstack((timestamps, timestamps)),
        'Value': np.hstack((a,b))
        })
    return df

df = setup(regular=False)
df.sort(['Category', 'Time'], inplace=True)

因此DataFrame,df,看起来像这样:

In [4]: df
Out[4]: 
   Category       Time  Value    Result
12    False   0.013725      2  1.000000
15    False  11.080631      5  0.500000
14    False  17.610707      4  0.333333
16    False  22.351225      6  0.250000
13    False  36.279909      3  0.400000
17    False  41.467287      7  0.333333
18    False  47.612097      8  0.285714
10    False  50.042641      0  0.250000
19    False  64.658008      9  0.125000
11    False  86.438939      1  0.333333
2      True   0.013725      2  1.000000
5      True  11.080631      5  0.500000
4      True  17.610707      4  0.333333
6      True  22.351225      6  0.250000
3      True  36.279909      3  0.400000
7      True  41.467287      7  0.333333
8      True  47.612097      8  0.285714
0      True  50.042641      0  0.250000
9      True  64.658008      9  0.125000
1      True  86.438939      1  0.333333

现在,仿照@herrfz的做法,我们来定义:
def between(a, b):
    def between_percentage(series):
        return float(len(series[(a <= series) & (series < b)])) / float(len(series))
    return between_percentage

between(1,3) 是一个函数,它以序列为输入并返回其中位于半开区间 [1,3) 内的元素所占比例。例如:

In [9]: series = pd.Series([1,2,3,4,5])

In [10]: between(1,3)(series)
Out[10]: 0.4

现在我们将对DataFrame(df)进行分组,按照类别(Category)进行分组:
df.groupby(['Category'])

对于groupby对象中的每个分组,我们都想应用一个函数:
df['Result'] = df.groupby(['Category']).apply(toeach_category)

函数toeach_category将接受一个(子)DataFrame作为输入,并返回一个DataFrame作为输出。整个结果将被赋值给df的一个名为Result的新列。

那么toeach_category究竟要做什么呢?如果我们像这样编写toeach_category

def toeach_category(subf):
    print(subf)

然后我们可以看到每个subf都是一个DataFrame,例如这个(当Category为False时):

   Category       Time  Value    Result
12    False   0.013725      2  1.000000
15    False  11.080631      5  0.500000
14    False  17.610707      4  0.333333
16    False  22.351225      6  0.250000
13    False  36.279909      3  0.400000
17    False  41.467287      7  0.333333
18    False  47.612097      8  0.285714
10    False  50.042641      0  0.250000
19    False  64.658008      9  0.125000
11    False  86.438939      1  0.333333

我们想要针对每一个时间,对《纽约时报》专栏进行处理。这可以通过使用applymap函数来实现:
def toeach_category(subf):
    result = subf[['Time']].applymap(percentage)

函数percentage需要一个时间值作为输入,并返回一个值作为输出。该值将是具有值在1和3之间的行的分数。applymap非常严格:percentage不能接受任何其他参数。

给定时间t,我们可以使用ix方法从subf中选择其时间在半开区间(t-60, t]Values

subf.ix[(t-60 < subf['Time']) & (subf['Time'] <= t), 'Value']

因此,我们可以通过应用 between(1,3) 来找到 1 到 3 之间的那些 Values 百分比:

between(1,3)(subf.ix[(t-60 < subf['Time']) & (subf['Time'] <= t), 'Value'])

现在请记住,我们需要一个名为percentage 的函数,它以t作为输入,并将上述表达式作为输出返回:
def percentage(t):
    return between(1,3)(subf.ix[(t-60 < subf['Time']) & (subf['Time'] <= t), 'Value'])

但要注意,percentage 取决于 subf,我们不允许将 subf 作为参数传递给 percentage(再次强调,因为 applymap 非常严格)。
那么我们该如何解决这个问题呢?解决方法是在 toeach_category 内定义 percentage。Python 的作用域规则表明,像 subf 这样的裸名字首先在 Local 作用域中查找,然后在 Enclosing 作用域、Global 作用域和最后在 Builtin 作用域中查找。当调用 percentage(t) 时,Python 遇到 subf,Python 首先在 Local 作用域中查找 subf 的值。由于 subf 不是 percentage 中的本地变量,Python 在函数 toeach_category 的 Enclosing 作用域中查找它。它在那里找到了 subf。完美。这正是我们所需要的。
现在我们有了函数 toeach_category
def toeach_category(subf):
    def percentage(t):
        return between(1, 3)(
            subf.ix[(t - 60 < subf['Time']) & (subf['Time'] <= t), 'Value'])
    result = subf[['Time']].applymap(percentage)
    return result

将所有内容整合在一起,
import pandas as pd
import numpy as np
np.random.seed(1)


def setup(regular=True):
    N = 10
    x = np.arange(N)
    a = np.arange(N)
    b = np.arange(N)

    if regular:
        timestamps = np.linspace(0, 120, N)
    else:
        timestamps = np.random.uniform(0, 120, N)

    df = pd.DataFrame({
        'Category': [True] * N + [False] * N,
        'Time': np.hstack((timestamps, timestamps)),
        'Value': np.hstack((a, b))
    })
    return df


def between(a, b):
    def between_percentage(series):
        return float(len(series[(a <= series) & (series < b)])) / float(len(series))
    return between_percentage


def toeach_category(subf):
    def percentage(t):
        return between(1, 3)(
            subf.ix[(t - 60 < subf['Time']) & (subf['Time'] <= t), 'Value'])
    result = subf[['Time']].applymap(percentage)
    return result


df = setup(regular=False)
df.sort(['Category', 'Time'], inplace=True)
df['Result'] = df.groupby(['Category']).apply(toeach_category)
print(df)

产量
   Category       Time  Value    Result
12    False   0.013725      2  1.000000
15    False  11.080631      5  0.500000
14    False  17.610707      4  0.333333
16    False  22.351225      6  0.250000
13    False  36.279909      3  0.200000
17    False  41.467287      7  0.166667
18    False  47.612097      8  0.142857
10    False  50.042641      0  0.125000
19    False  64.658008      9  0.000000
11    False  86.438939      1  0.166667
2      True   0.013725      2  1.000000
5      True  11.080631      5  0.500000
4      True  17.610707      4  0.333333
6      True  22.351225      6  0.250000
3      True  36.279909      3  0.200000
7      True  41.467287      7  0.166667
8      True  47.612097      8  0.142857
0      True  50.042641      0  0.125000
9      True  64.658008      9  0.000000
1      True  86.438939      1  0.166667

这看起来是一个有趣且详细的解决方案。但为什么不像原问题所问的那样使用apply_rolling呢?它肯定可以用吧?我有一个非常类似的问题。 - Carl
@Carl:pd.rolling_apply需要一个固定的(整数)窗口大小。在这个问题中,窗口大小随着每一行而改变,因为窗口取决于“Time”列中的值。 - unutbu
谢谢unutbu,非常感谢。在固定窗口大小的情况下,如何使用它? - Carl
那个问题比较宽泛。请开一个新的问题,详细说明细节,或者谷歌搜索“pandas pd.rolling_apply site: stackoverflow.com”以找到大量示例。 - unutbu
文档中还有一个示例(http://pandas.pydata.org/pandas-docs/version/0.17.1/computation.html#moving-rolling-statistics-moments)。 - unutbu

2
如果我正确理解您的问题陈述,那么如果您仅出于计算百分比的目的使用它,您可以跳过"rolling count"。 "rolling_apply"的参数是执行聚合的函数,即将数组作为输入并返回数字作为输出的函数。
考虑到这一点,让我们首先定义一个函数:
def between_1_3_perc(x):
    # pandas Series is basically a numpy array, we can do boolean indexing
    return float(len(x[(x > 1) & (x < 3)])) / float(len(x))

然后在for循环中使用函数名作为rolling_apply的参数:

grp['Result'] = pd.rolling_apply(grp['Value'], 60, between_1_3_perc)

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