大熊猫通过多个输入和多个输出列进行应用,滚动,分组

时间:2020-05-05 15:47:10

标签: python pandas

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

1 个答案:

答案 0 :(得分:3)

重要提示

  1. 应用和滚动在大熊猫中的组合具有非常强的输出要求。您必须返回一个单一值。您不能返回pd.Series,不是列表,不是数组,不是秘密地返回数组中的数组,而只能返回一个值,例如一个整数。尝试返回多个列的多个输出时,此要求使得很难获得有效的解决方案。我不明白为什么对“申请并滚动”有此要求,因为不进行滚动“申请”就没有此要求。必须归功于一些内部的熊猫功能。
  2. “应用和滚动”与多个输入列的组合根本行不通!想象一下一个具有2列6行的数据框,并且您想应用滚动窗口为2的自定义函数。您的函数应该获得一个2x2值的输入数组-每行2列的2个值。但是,熊猫似乎无法同时处理滚动和多个输入列。我尝试使用 axis 参数使其正常运行,但是:
    • Axis = 0,将按列调用您的函数。在上述数据框中,它将调用您的函数10次(而不是12次,因为rolling = 2),并且由于它是每列,因此仅提供该列的2个滚动值…
    • Axis = 1,将每行调用一次函数。这可能是您想要的,但是熊猫不会提供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)的500万条记录(速度/性能很重要)的数据集进行滑动窗口(滚动)傅里叶变换。而且我需要将傅立叶变换的功率和相位都保存在不同的列(多个输出)中。大多数人可能只需要下面的一些基本示例,但是我相信,尤其是在机器学习/数据科学领域,更复杂的示例可能会有用。
  6. ,请告诉我您是否有更好,更清晰或更快速的方法来执行以下任何解决方案。我将更新我的答案,我们都将从中受益!


代码示例

首先创建一个数据框,该数据框将在以下所有示例中使用,其中包括groupby示例的组列。 对于滚动窗口和多个输入/输出列,我在下面的所有代码示例中仅使用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)


滚动和分组方式

将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点所述,如果要结合使用 rolling&apply 来返回多个值,则需要一种解决方法。我找到了两种可行的解决方案。

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)