【问题标题】:Pandas apply, rolling, groupby with multiple input & multiple output columns具有多个输入和多个输出列的 Pandas 应用、滚动、分组
【发布时间】:2020-08-20 08:30:15
【问题描述】:

过去一周我一直在努力尝试使用 apply 在整个 pandas 数据框上使用函数,包括 rolling 窗口、groupby ,尤其是多输入列和多输出列。我在 SO 上发现了大量关于这个主题的问题以及许多过时的答案。因此,我开始为 x 输入和输出、滚动、滚动和 groupby 组合的每种可能组合创建一个笔记本,并且我也专注于 性能。由于我不是唯一一个在这些问题上苦苦挣扎的人,我想我会在这里提供我的解决方案和工作示例,希望它可以帮助任何现有/未来的 pandas 用户。

【问题讨论】:

    标签: python pandas


    【解决方案1】:

    重要提示

    1. pandas中apply & rolling的组合对输出的要求非常高。您必须返回一个单一的值。您不能返回 pd.Series,不能返回列表,不能返回数组,不能秘密返回数组中的数组,而只能返回一个值,例如一个整数。当尝试为多个列返回多个输出时,此要求很难获得有效的解决方案。我不明白为什么它对“应用和滚动”有这个要求,因为不滚动“应用”就没有这个要求。一定是因为 pandas 的一些内部函数。
    2. “应用和滚动”与多个输入列的组合根本不起作用!想象一个具有 2 列 6 行的数据框,并且您想应用滚动窗口为 2 的自定义函数。您的函数应该获得一个具有 2x2 值的输入数组 - 每列 2 个值,2 行。但似乎 pandas 无法同时处理滚动和多个输入列。我尝试使用 axis 参数让它工作,但是:
      • Axis = 0,将按列调用您的函数。在上述数据框中,它将调用您的函数 10 次(不是 12 次,因为 rolling=2),并且由于它是每列的,它只提供该列的 2 个滚动值……
      • Axis = 1,将按行调用您的函数。这可能是您想要的,但 pandas 不会提供 2x2 输入。它实际上完全忽略了滚动,只提供了一行 2 列的值...
    3. 当对多个输入列使用“应用”时,您可以提供一个名为 raw(布尔值)的参数。默认为 False,这意味着输入将是一个 pd.Series,因此在值旁边包含索引。如果您不需要索引,可以将 raw 设置为 True 以获取 Numpy 数组,这通常会获得更好的性能。
    4. 当组合 'rolling & groupby' 时,它会返回一个多索引系列,不能轻易地用作新列的输入。最简单的解决方案是在此处 (Python - rolling functions for GroupBy object) 附加一个 reset_index(drop=True) 作为回答和评论。
    5. 您可能会问我,您什么时候想使用具有多个输出的滚动、groupby 自定义函数!?回答:我最近不得不在一个包含 500 万条记录(速度/性能很重要)的数据集上使用滑动窗口(滚动)进行傅里叶变换,数据集中有不同的批次(groupby)。而且我需要将傅里叶变换的功率和相位保存在不同的列(多个输出)中。大多数人可能只需要下面的一些基本示例,但我相信,尤其是在机器学习/数据科学领域,更复杂的示例会很有用。
    6. 如果您有更好、更清晰或更快的方法来执行以下任何解决方案,告诉我。我会更新我的答案,我们都可以受益!


    代码示例

    让我们首先创建一个数据框,它将在下面的所有示例中使用,包括用于 groupby 示例的 group-column。 对于滚动窗口和多个输入/输出列,我在下面的所有代码示例中只使用 2,但显然这可以是任何大于 1 的数字。

    df = pd.DataFrame(np.random.randint(0,5,size=(6, 2)), columns=list('ab'))
    df['group'] = [0, 0, 0, 1, 1, 1]
    df = df[['group', 'a', 'b']]
    

    看起来像这样:

    group   a   b
    0   0   2   2
    1   0   4   1
    2   0   0   4
    3   1   0   2
    4   1   3   2
    5   1   3   0
    


    输入1列,输出1列

    基本

    def func_i1_o1(x):    
        return x+1
    
    df['c'] = df['b'].apply(func_i1_o1)
    


    滚动

    def func_i1_o1_rolling(x):
        return (x[0] + x[1])
    
    df['d'] = df['c'].rolling(2).apply(func_i1_o1_rolling, raw=True)
    


    Roling & Groupby

    将 reset_index 解决方案(见上面的注释)添加到滚动函数中。

    df['e'] = df.groupby('group')['c'].rolling(2).apply(func_i1_o1_rolling, raw=True).reset_index(drop=True)
    




    输入2列,输出1列

    基本

    def func_i2_o1(x):
        return np.sum(x)
    
    df['f'] = df[['b', 'c']].apply(func_i2_o1, axis=1, raw=True)
    


    滚动

    如上述注释中的第 2 点所述,对于 2 个输入没有“正常”解决方案。下面的解决方法使用 'raw=False' 来确保输入是 pd.Series,这意味着我们还可以获取值旁边的索引。这使我们能够从要使用的正确索引处的其他列中获取值。

    def func_i2_o1_rolling(x):
        values_b = x
        values_c = df.loc[x.index, 'c'].to_numpy()
        return np.sum(values_b) + np.sum(values_c)
    
    df['g'] = df['b'].rolling(2).apply(func_i2_o1_rolling, raw=False)
    


    滚动和分组

    将 reset_index 解决方案(见上面的注释)添加到滚动函数中。

    df['h'] = df.groupby('group')['b'].rolling(2).apply(func_i2_o1_rolling, raw=False).reset_index(drop=True)
    




    输入1列,输出2列

    基本

    您可以通过返回 pd.Series 来使用“正常”解决方案:

    def func_i1_o2(x):
        return pd.Series((x+1, x+2))
    
    df[['i', 'j']] = df['b'].apply(func_i1_o2)
    

    或者你可以使用快 8 倍的 zip/tuple 组合!

    def func_i1_o2_fast(x):
        return x+1, x+2
    
    df['k'], df['l'] = zip(*df['b'].apply(func_i1_o2_fast))
    


    滚动

    正如上面注释中的第 1 点所述,如果我们在使用 滚动和应用 组合时想要返回超过 1 个值,我们需要一种解决方法。我找到了 2 个可行的解决方案。

    1

    def func_i1_o2_rolling_solution1(x):
        output_1 = np.max(x)
        output_2 = np.min(x)
        # Last index is where to place the final values: x.index[-1]
        df.at[x.index[-1], ['m', 'n']] = output_1, output_2
        return 0
    
    df['m'], df['n'] = (np.nan, np.nan)
    df['b'].rolling(2).apply(func_i1_o2_rolling_solution1, raw=False)
    

    优点:一切都在 1 个函数内完成。
    缺点:您必须先创建列,而且速度较慢,因为它不使用 原始输入。

    2

    rolling_w = 2
    nan_prefix = (rolling_w - 1) * [np.nan]
    output_list_1 = nan_prefix.copy()
    output_list_2 = nan_prefix.copy()
    
    def func_i1_o2_rolling_solution2(x):
        output_list_1.append(np.max(x))
        output_list_2.append(np.min(x))
        return 0
    
    df['b'].rolling(rolling_w).apply(func_i1_o2_rolling_solution2, raw=True)
    df['o'] = output_list_1
    df['p'] = output_list_2
    

    优点:它使用原始输入,使其速度提高了大约两倍。而且由于它不使用索引来设置输出值,因此代码看起来更清晰(至少对我而言)。
    缺点:您必须自己创建 nan-prefix需要更多的代码行。


    滚动和分组

    通常,我会使用上面更快的第二种解决方案。但是,由于我们正在组合组并滚动,这意味着您必须在数据集中间某处的正确索引处手动设置 NaN/零(取决于组的数量)。在我看来,当结合滚动、分组和多个输出列时,第一个解决方案更容易,并且自动解决了自动 NaN/分组。最后,我再次使用了 reset_index 解决方案。

    def func_i1_o2_rolling_groupby(x):
        output_1 = np.max(x)
        output_2 = np.min(x)
        # Last index is where to place the final values: x.index[-1]
        df.at[x.index[-1], ['q', 'r']] = output_1, output_2
        return 0
    
    df['q'], df['r'] = (np.nan, np.nan)
    df.groupby('group')['b'].rolling(2).apply(func_i1_o2_rolling_groupby, raw=False).reset_index(drop=True)
    




    输入2列,输出2列

    基本

    我建议使用与 i1_o2 相同的“快速”方式,唯一的区别是您可以使用 2 个输入值。

    def func_i2_o2(x):
        return np.mean(x), np.median(x)
    
    df['s'], df['t'] = zip(*df[['b', 'c']].apply(func_i2_o2, axis=1))
    


    滚动

    由于我使用一种解决方法来应用 多个输入 滚动,而我使用另一种解决方法来滚动 多个输出,您可以猜到我需要将它们组合起来.
    1. 使用索引从其他列获取值(参见 func_i2_o1_rolling)
    2.在正确的索引上设置最终的多个输出(参见func_i1_o2_rolling_solution1)

    def func_i2_o2_rolling(x):
        values_b = x.to_numpy()
        values_c = df.loc[x.index, 'c'].to_numpy()
        output_1 = np.min([np.sum(values_b), np.sum(values_c)])
        output_2 = np.max([np.sum(values_b), np.sum(values_c)])    
        # Last index is where to place the final values: x.index[-1]
        df.at[x.index[-1], ['u', 'v']] = output_1, output_2
        return 0
    
    df['u'], df['v'] = (np.nan, np.nan)
    df['b'].rolling(2).apply(func_i2_o2_rolling, raw=False)
    


    滚动和分组

    将 reset_index 解决方案(见上面的注释)添加到滚动函数中。

    def func_i2_o2_rolling_groupby(x):
        values_b = x.to_numpy()
        values_c = df.loc[x.index, 'c'].to_numpy()
        output_1 = np.min([np.sum(values_b), np.sum(values_c)])
        output_2 = np.max([np.sum(values_b), np.sum(values_c)])    
        # Last index is where to place the final values: x.index[-1]
        df.at[x.index[-1], ['w', 'x']] = output_1, output_2
        return 0
    
    df['w'], df['x'] = (np.nan, np.nan)
    df.groupby('group')['b'].rolling(2).apply(func_i2_o2_rolling_groupby, raw=False).reset_index(drop=True)
    

    【讨论】:

    • 我绕过了使用 numpy 滚动/应用的限制,特别是 np.lib.stride_tricks.sliding_window_viewnp.apply_along_axis。它需要将输出数组添加回原始 pandas DF 并填充初始行。与您在此处显示的相比,不确定性能。不确定您是否对 pandas 之外的解决方案感兴趣?
    • @Olivier 很高兴您找到了绕过限制的另一种方法!我更喜欢使用尽可能少的框架,所以我不确定是否使用您的解决方案,但我对性能比较感到好奇。你有没有尝试过?
    • 感谢您的帖子 - 经过数小时的搜索,这绝对是我的起点。您对如何处理诸如.rolling("10d", on="date", closed="left") 之类的时间序列有任何想法吗?如果我与groupby("some_col")["date"] 分组,则会收到错误ValueError: invalid on specified as date, must be a column (of DataFrame), an Index or None
    • @Jossy 这感觉更像是一个新问题,我不知道你的 df 到底长什么样。但是给我发个 DM,我会尽力帮助你 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-17
    • 1970-01-01
    • 2020-01-29
    • 1970-01-01
    • 1970-01-01
    • 2018-08-07
    • 1970-01-01
    相关资源
    最近更新 更多