Groupby Roll up 或 Roll Down 用于任何类型的聚合

The*_*Guy 12 python dataframe pandas pandas-groupby

TL;DR:我们如何在 Pandas 中使用任何类型的聚合实现类似于Group By Roll Up 的效果?(本学期感谢@Scott Boston

我有以下数据框:

       P   Q  R     S  T
0   PLAC  NR  F   HOL  F
1   PLAC  NR  F  NHOL  F
2   TRTB  NR  M  NHOL  M
3   PLAC  NR  M  NHOL  M
4   PLAC  NR  F  NHOL  F
5   PLAC   R  M  NHOL  M
6   TRTA   R  F   HOL  F
7   TRTA  NR  F   HOL  F
8   TRTB  NR  F  NHOL  F
9   PLAC  NR  F  NHOL  F
10  TRTB  NR  F  NHOL  F
11  TRTB  NR  M  NHOL  M
12  TRTA  NR  F   HOL  F
13  PLAC  NR  F   HOL  F
14  PLAC   R  F  NHOL  F
Run Code Online (Sandbox Code Playgroud)

对于列列表['Q', 'R', 'S', 'T'],我想P在以下 4 个分组列列表上计算列上的一些聚合:

  1. ['Q']
  2. ['Q', 'R']
  3. ['Q', 'R', 'S']
  4. ['Q', 'R', 'S', 'T']

我已经写的代码组上述dataframes在越来越多的字段,并且计算聚集(使用count为了简化的抖动)在每个GROUPBY对象,最后将它们连接起来:

cols = list('QRST')
aggCol = 'P'
groupCols = []
result = []
for col in cols:
    groupCols.append(col)
    result.append(df.groupby(groupCols)[aggCol].agg(count='count').reset_index())
result = pd.concat(result)[groupCols+['count']]
Run Code Online (Sandbox Code Playgroud)

但是,我强烈认为上述方法在 CPU 时间方面效率不高。有没有更有效的方法可以在如此不断增加的列数上应用聚合以进行分组?

为什么我认为它不是那么有效是因为:对于上述值,在第一次迭代中,它对Q列上的数据框进行分组,然后计算聚合。然后在下一次迭代中,它将数据帧分组在Qand 上R,这意味着它再次需要按QthenR对其进行分组,但它已经Q在第一次迭代中分组了,因此重复相同的操作。如果有某种方法可以利用以前创建的组,我认为它会很有效。

输出:

    Q    R     S    T  count
0  NR  NaN   NaN  NaN     12
1   R  NaN   NaN  NaN      3
0  NR    F   NaN  NaN      9
1  NR    M   NaN  NaN      3
2   R    F   NaN  NaN      2
3   R    M   NaN  NaN      1
0  NR    F   HOL  NaN      4
1  NR    F  NHOL  NaN      5
2  NR    M  NHOL  NaN      3
3   R    F   HOL  NaN      1
4   R    F  NHOL  NaN      1
5   R    M  NHOL  NaN      1
0  NR    F   HOL    F      4
1  NR    F  NHOL    F      5
2  NR    M  NHOL    M      3
3   R    F   HOL    F      1
4   R    F  NHOL    F      1
5   R    M  NHOL    M      1
Run Code Online (Sandbox Code Playgroud)

我已经研究过 Python pandas 中是否有等效的 SQL GROUP BY ROLLUP?Pandas 数据透视表行小计,它们在我的情况下不起作用,我已经尝试过它们,即这些方法只能用于获取计数,并且当相同的标识符出现多个值时,即使对于唯一计数也会立即失败:

pd.pivot_table(df, aggCol, columns=cols, aggfunc='count', margins=True).T.reset_index()
    Q    R     S  T  P
0  NR    F   HOL  F  4
1  NR    F  NHOL  F  5
2  NR    M  NHOL  M  3
3  NR  All           3
4   R    F   HOL  F  1
5   R    F  NHOL  F  1
6   R    M  NHOL  M  1
7   R  All           3
Run Code Online (Sandbox Code Playgroud)

更新

为了避免count在评论中仅根据建议获得任何不必要的混淆,我已将其添加为平均值作为聚合,将P列更改为数字类型:

    P   Q  R     S  T
0   9  NR  F   HOL  F
1   7  NR  F  NHOL  F
2   3  NR  M  NHOL  M
3   9  NR  M  NHOL  M
4   1  NR  F  NHOL  F
5   0   R  M  NHOL  M
6   1   R  F   HOL  F
7   7  NR  F   HOL  F
8   2  NR  F  NHOL  F
9   2  NR  F  NHOL  F
10  1  NR  F  NHOL  F
11  2  NR  M  NHOL  M
12  3  NR  F   HOL  F
13  6  NR  F   HOL  F
14  0   R  F  NHOL  F

cols = list('QRST')
cols = list('QRST')
aggCol = 'P'
groupCols = []
result = []
for col in cols:
    groupCols.append(col)
    result.append(df.groupby(groupCols)[aggCol]
                  .agg(agg=np.mean)
                  .round(2).reset_index())
result = pd.concat(result)[groupCols+['agg']]
Run Code Online (Sandbox Code Playgroud)
>>> result
    Q    R     S    T   agg
0  NR  NaN   NaN  NaN  4.33
1   R  NaN   NaN  NaN  0.33
0  NR    F   NaN  NaN  4.22
1  NR    M   NaN  NaN  4.67
2   R    F   NaN  NaN  0.50
3   R    M   NaN  NaN  0.00
0  NR    F   HOL  NaN  6.25
1  NR    F  NHOL  NaN  2.60
2  NR    M  NHOL  NaN  4.67
3   R    F   HOL  NaN  1.00
4   R    F  NHOL  NaN  0.00
5   R    M  NHOL  NaN  0.00
0  NR    F   HOL    F  6.25
1  NR    F  NHOL    F  2.60
2  NR    M  NHOL    M  4.67
3   R    F   HOL    F  1.00
4   R    F  NHOL    F  0.00
5   R    M  NHOL    M  0.00
Run Code Online (Sandbox Code Playgroud)

Pie*_*e D 4

基于 @ScottBoston 的思想(渐进聚合,即重复聚合先前的聚合结果),我们可以做一些关于聚合函数相对通用的事情,如果该函数可以表示为函数的组合((f3 \xe2\x88\x98 f2 \xe2\x88\x98 f2 \xe2\x88\x98 ... \xe2\x88\x98 f1)(x),或者换句话说f3(f2(f2(...(f1(x))))):)。

\n

例如,sum按原样工作正常,因为它sum是关联的,所以组和的总和是整体的总和。

\n

对于count,初始函数 ( f1) 确实是count,但f2必须是sum,最终函数f3必须是恒等式。

\n

对于mean,初始函数 ( f1) 必须产生两个量:sumcount。中间函数f2可以是sum,最终函数 ( f3) 必须是两个量的比率。

\n

这是一个粗略的模板,定义了一些函数。作为额外的好处,该函数还可以选择生成总计:

\n
# think map-reduce: first map, then reduce (arbitrary number of times), then map to result\n\nmyfuncs = {\n    \'sum\': [sum, sum],\n    \'prod\': [\'prod\', \'prod\'],\n    \'count\': [\'count\', sum],\n    \'set\': [set, lambda g: set.union(*g)],\n    \'list\': [list, sum],\n    \'mean\': [[sum, \'count\'], sum, lambda r: r[0]/r[1]],\n    \'var\': [\n        [lambda x: (x**2).sum(), sum, \'count\'],\n        sum,\n        lambda r: (r[0].sum() - r[1].sum()**2 / r[2]) / (r[2] - 1)],\n    \'std\': [\n        [lambda x: (x**2).sum(), sum, \'count\'],\n        sum,\n        lambda r: np.sqrt((r[0].sum() - r[1].sum()**2 / r[2]) / (r[2] - 1))],\n}\n\ntotalCol = \'__total__\'\ndef agg(df, cols, aggCol, fun, total=True):\n    if total:\n        cols = [totalCol] + cols\n        df = df.assign(__total__=0)\n    funs = myfuncs[fun]\n    b = df.groupby(cols).agg({aggCol: funs[0]})\n    frames = [b.reset_index()]\n    for k in range(1, len(cols)):\n        b = b.groupby(cols[:-k]).agg(funs[1])\n        frames.append(b.reset_index())\n    result = pd.concat(frames).reset_index(drop=True)\n    result = result[frames[0].columns]\n    if len(funs) > 2:\n        s = result[aggCol].apply(funs[2], axis=1)\n        result = result.drop(aggCol, axis=1, level=0)\n        result[aggCol] = s\n        result.columns = result.columns.droplevel(-1)\n    if total:\n        result = result.drop(columns=[totalCol])\n    return result\n
Run Code Online (Sandbox Code Playgroud)\n

例子

\n
cols = list(\'QRST\')\naggCol = \'P\'\n\n>>> agg(df, cols, aggCol, \'count\')\n      Q    R     S    T   P\n0    NR    F   HOL    F   4\n1    NR    F  NHOL    F   5\n2    NR    M  NHOL    M   3\n3     R    F   HOL    F   1\n..  ...  ...   ...  ...  ..\n15    R    M   NaN  NaN   1\n16   NR  NaN   NaN  NaN  12\n17    R  NaN   NaN  NaN   3\n18  NaN  NaN   NaN  NaN  15\n
Run Code Online (Sandbox Code Playgroud)\n
>>> agg(df, cols, aggCol, \'mean\')\n      Q    R     S    T         P\n0    NR    F   HOL    F  6.250000\n1    NR    F  NHOL    F  2.600000\n2    NR    M  NHOL    M  4.666667\n3     R    F   HOL    F  1.000000\n..  ...  ...   ...  ...       ...\n15    R    M   NaN  NaN  0.000000\n16   NR  NaN   NaN  NaN  4.333333\n17    R  NaN   NaN  NaN  0.333333\n18  NaN  NaN   NaN  NaN  3.533333\n
Run Code Online (Sandbox Code Playgroud)\n
>>> agg(df, cols, aggCol, \'sum\')\n      Q    R     S    T   P\n0    NR    F   HOL    F  25\n1    NR    F  NHOL    F  13\n2    NR    M  NHOL    M  14\n3     R    F   HOL    F   1\n..  ...  ...   ...  ...  ..\n15    R    M   NaN  NaN   0\n16   NR  NaN   NaN  NaN  52\n17    R  NaN   NaN  NaN   1\n18  NaN  NaN   NaN  NaN  53\n
Run Code Online (Sandbox Code Playgroud)\n
>>> agg(df, cols, aggCol, \'set\')\n      Q    R     S    T                      P\n0    NR    F   HOL    F           {9, 3, 6, 7}\n1    NR    F  NHOL    F              {1, 2, 7}\n2    NR    M  NHOL    M              {9, 2, 3}\n3     R    F   HOL    F                    {1}\n..  ...  ...   ...  ...                    ...\n15    R    M   NaN  NaN                    {0}\n16   NR  NaN   NaN  NaN     {1, 2, 3, 6, 7, 9}\n17    R  NaN   NaN  NaN                 {0, 1}\n18  NaN  NaN   NaN  NaN  {0, 1, 2, 3, 6, 7, 9}\n
Run Code Online (Sandbox Code Playgroud)\n
>>> agg(df, cols, aggCol, \'std\')\n      Q    R     S    T         P\n0    NR    F   HOL    F  2.500000\n1    NR    F  NHOL    F  2.509980\n2    NR    M  NHOL    M  3.785939\n3     R    F   HOL    F       NaN\n..  ...  ...   ...  ...       ...\n15    R    M   NaN  NaN       NaN\n16   NR  NaN   NaN  NaN  3.055050\n17    R  NaN   NaN  NaN  0.577350\n18  NaN  NaN   NaN  NaN  3.181793\n
Run Code Online (Sandbox Code Playgroud)\n

笔记

\n
    \n
  • 该代码并不像我希望的那样“纯粹”。原因有二:

    \n
      \n
    1. groupby喜欢对结果的形状施展一些魔法。例如,在某些情况下(但并不总是如此,奇怪的是),如果只有一个结果组,则输出有时会被压缩为Series.

      \n
    2. \n
    3. pandas 的算术set有时看起来是假的,或者充其量是挑剔的。我最初的定义是:\'set\': [set, sum]并且这工作得相当好(pandas 似乎有时理解.agg(sum)在某些Series对象上set应用 是可取的set.union),但奇怪的是,在某些情况下我们会得到一个NaN结果。

      \n
    4. \n
    \n
  • \n
  • 这仅适用于单个aggCol.

    \n
  • \n
  • std和 的表达方式var相对幼稚。要提高数值稳定性,请参阅标准差:快速计算方法

    \n
  • \n
\n

速度

\n

自从最初发布这个答案以来,@U12-Forward 提出了另一个解决方案。经过一些清理(例如不使用递归,并将 agg dtype 更改为所需的任何内容,而不是 )object,此解决方案变为:

\n
def v_u12(df, cols, aggCol, fun):\n    newdf = pd.DataFrame(columns=cols)\n    for count in range(1, len(cols)+1):\n        groupcols = cols[:count]\n        newdf = newdf.append(\n            df.groupby(groupcols)[aggCol].agg(fun).reset_index().reindex(columns=groupcols + [aggCol]),\n            ignore_index=True,\n        )\n    return newdf\n
Run Code Online (Sandbox Code Playgroud)\n

为了比较速度,让我们生成任意大小的 DataFrame:

\n
def gen_example(n, m=4, seed=-1):\n    if seed >= 0:\n        np.random.seed(seed)\n    aggCol = \'v\'\n    cols = list(ascii_uppercase)[:m]\n    choices = [[\'R\', \'NR\'], [\'F\', \'M\'], [\'HOL\', \'NHOL\']]\n    df = pd.DataFrame({\n        aggCol: np.random.uniform(size=n),\n        **{\n            k: np.random.choice(choices[np.random.randint(0, len(choices))], n)\n            for k in cols\n        }})\n    return df\n\n# example\n>>> gen_example(8, 5, 0)\n          v   A  B  C  D   E\n0  0.548814   R  M  F  M  NR\n1  0.715189   R  M  M  M   R\n2  0.602763  NR  M  M  F   R\n3  0.544883   R  F  F  M  NR\n4  0.423655  NR  M  F  F  NR\n5  0.645894  NR  F  M  M   R\n6  0.437587   R  M  F  M  NR\n7  0.891773   R  F  M  M   R\n
Run Code Online (Sandbox Code Playgroud)\n

perfplot现在,我们可以使用优秀的包以及一些定义来比较各种大小的速度:

\n
m = 4\naggCol, *cols = gen_example(2, m).columns\nfun = \'mean\'\n\ndef ours(df):\n    funname = fun if isinstance(fun, str) else fun.__name__\n    return agg(df, cols, aggCol, funname, total=False)\n\ndef u12(df):\n    return v_u12(df, cols, aggCol, fun)\n\ndef equality_check(a, b):\n    a = a.sort_values(cols).reset_index(drop=True)\n    b = b.sort_values(cols).reset_index(drop=True)\n    non_numeric = a[aggCol].dtype == \'object\'\n    if non_numeric:\n        return a[cols+[aggCol]].equals(b[cols+[aggCol]])\n    return a[cols].equals(b[cols]) and np.allclose(a[aggCol], b[aggCol])\n\n\nperfplot.show(\n    time_unit=\'auto\',\n    setup=lambda n: gen_example(n, m),\n    kernels=[ours, u12],\n    n_range=[2 ** k for k in range(4, 21)],\n    equality_check=equality_check,\n    xlabel=f\'n rows\\n(m={m} columns, fun={fun})\'\n)\n
Run Code Online (Sandbox Code Playgroud)\n

下面是一些聚合函数和m值的比较(y 轴是平均时间:越低越好):

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n
mfun性能图
4\'mean\'
10\'mean\'
10\'sum\'
4\'set\'
\n
\n

对于不关联的函数(例如\'mean\'),我们的“渐进式重新聚合”需要跟踪多个值(例如,for mean:sumcount),因此对于相对较小的 DataFrame,速度大约是 的两倍u12。但随着规模的增长,重新聚合的增益会克服这一点并ours变得更快。

\n