Pandas DataFrame 多列聚合函数

106

有没有一种方法可以编写聚合函数,就像在 DataFrame.agg 方法中使用的那样,该函数可以访问正在被聚合的数据的多个列?典型的用例是加权平均值、加权标准差等。

我想能够编写类似下面的代码:

def wAvg(c, w):
    return ((c * w).sum() / w.sum())

df = DataFrame(....) # df has columns c and w, i want weighted average
                     # of c using w as weight.
df.aggregate ({"c": wAvg}) # and somehow tell it to use w column as weights ...

1
很棒的文章,解答了这个特定的SO问题:https://pbpython.com/weighted-average.html - ptim
9个回答

127

可以使用.apply(...)函数,在每个子DataFrame上调用它。例如:

grouped = df.groupby(keys)

def wavg(group):
    d = group['data']
    w = group['weights']
    return (d * w).sum() / w.sum()

grouped.apply(wavg)

1
将此操作分解为以下几个步骤可能更有效率:(1)创建一个权重列,(2)通过它们的权重对观测值进行归一化,(3)计算加权观测值和加权权重的分组总和,(4)通过权重总和对加权观测值进行归一化。 - kalu
4
如果我们想要计算许多变量(列)的加权平均值,例如除了df['weights']以外的所有变量,应该怎么做? - CPBL
2
@Wes,有没有办法可以使用 agg() 和一个围绕 np.average(...weights=...)lambda 来实现这一点,或者自此帖子首次出现以来 pandas 中是否有新的原生支持加权平均值的方法? - sparc_spread
4
在你的书中,你建议使用以下方法:get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg)这两种方法可以互换吗? - robroc

16

使用 apply 方法可以从分组对象返回任意数量的聚合值。只需返回一个Series,索引值将变为新的列名。

让我们看一个快速的例子:

df = pd.DataFrame({'group':['a','a','b','b'],
                   'd1':[5,10,100,30],
                   'd2':[7,1,3,20],
                   'weights':[.2,.8, .4, .6]},
                 columns=['group', 'd1', 'd2', 'weights'])
df

  group   d1  d2  weights
0     a    5   7      0.2
1     a   10   1      0.8
2     b  100   3      0.4
3     b   30  20      0.6

定义一个自定义函数,该函数将传递给apply。 它隐式地接受DataFrame - 这意味着data参数是DataFrame。请注意它如何使用多个列,这是使用agg groupby方法不可能的:

def weighted_average(data):
    d = {}
    d['d1_wa'] = np.average(data['d1'], weights=data['weights'])
    d['d2_wa'] = np.average(data['d2'], weights=data['weights'])
    return pd.Series(d)

使用我们自定义的函数调用 groupby 的 apply 方法:

df.groupby('group').apply(weighted_average)

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

通过将加权总数预先计算到新的DataFrame列中,可以获得更好的性能,正如其他答案中所解释的那样,避免使用apply


9

我的解决方案与Nathaniel的解决方案类似,只不过它只适用于单列,并且我没有每次深度复制整个数据帧,这可能会非常慢。相比于使用groupby(...).apply(...)的解决方案,性能提升约为100倍(!)

def weighted_average(df, data_col, weight_col, by_col):
    df['_data_times_weight'] = df[data_col] * df[weight_col]
    df['_weight_where_notnull'] = df[weight_col] * pd.notnull(df[data_col])
    g = df.groupby(by_col)
    result = g['_data_times_weight'].sum() / g['_weight_where_notnull'].sum()
    del df['_data_times_weight'], df['_weight_where_notnull']
    return result

如果您始终使用PEP8并删除多余的“del”行,则代码会更易读。 - MERose
谢谢!del这一行实际上并不是多余的,因为我会就地更改输入的DataFrame以提高性能,所以我必须进行清理。 - ErnestScribbler
但是你在下一行返回结果,这会结束函数。一旦函数完成,所有内部对象都将被清除。 - MERose
3
但请注意,df不是内部对象。它是函数的一个参数,只要你从来不对它赋值(df = something),它就保持为浅拷贝并在原地修改。在这种情况下,新列会被添加到DataFrame中。尝试复制并粘贴此函数并运行它,然后删除del行,看看它如何通过添加列来更改给定的DataFrame。 - ErnestScribbler
这并不回答问题,因为加权平均只是多列聚合的示例之一。 - user__42

9

以下是一种解决方案,具有以下优点:

  1. 无需预先定义函数
  2. 可以在管道内使用(因为它使用了 lambda)
  3. 可以为结果列命名

:

df.groupby('group')
  .apply(lambda x: pd.Series({
'weighted_average': np.average(x.data, weights = x.weights)})

您也可以使用相同的代码执行多个聚合:

df.groupby('group')
  .apply(lambda x: pd.Series({
'weighted_average': np.average(x.data, weights = x.weights), 
'regular_average': np.average(x.data)}))

5

我经常做这个,并发现以下内容非常方便:

def weighed_average(grp):
    return grp._get_numeric_data().multiply(grp['COUNT'], axis=0).sum()/grp['COUNT'].sum()
df.groupby('SOME_COL').apply(weighed_average)

这将计算df中所有数字列的加权平均值并删除非数字列。

如果你有多列,这真的很棒。不错! - Chris
@Allen 你应该使用包含你想要用于加权平均的计数的列名。 - santon
这很酷也非常快。我希望我也能将计数列传递到函数中。那么它就可以更通用地使用了。我尝试过,但是出现了“DataFrame对象没有名为nt_pop的轴”的错误,其中nt_pop是包含计数的列。 - SModi
1
@SModi 你可以轻松修改函数为 def weighted_average(grp, col='COUNT'): ... 来将列名参数化。(当然,在代码中也要用 col 替换 'COUNT'。)然后,只需调用 apply(lambda g: weigthed_average(g, "nt_pop") 即可。 - santon
1
谢谢,这个方法可行。我之前已经正确编辑了函数,但是调用方式不对。 - SModi
显示剩余2条评论

4
通过使用 groupby(...).apply(...) 来实现这个目标是非常低效的。这里提供一个我经常使用的解决方案(本质上使用了kalu的逻辑)。
def grouped_weighted_average(self, values, weights, *groupby_args, **groupby_kwargs):
   """
    :param values: column(s) to take the average of
    :param weights_col: column to weight on
    :param group_args: args to pass into groupby (e.g. the level you want to group on)
    :param group_kwargs: kwargs to pass into groupby
    :return: pandas.Series or pandas.DataFrame
    """

    if isinstance(values, str):
        values = [values]

    ss = []
    for value_col in values:
        df = self.copy()
        prod_name = 'prod_{v}_{w}'.format(v=value_col, w=weights)
        weights_name = 'weights_{w}'.format(w=weights)

        df[prod_name] = df[value_col] * df[weights]
        df[weights_name] = df[weights].where(~df[prod_name].isnull())
        df = df.groupby(*groupby_args, **groupby_kwargs).sum()
        s = df[prod_name] / df[weights_name]
        s.name = value_col
        ss.append(s)
    df = pd.concat(ss, axis=1) if len(ss) > 1 else ss[0]
    return df

pandas.DataFrame.grouped_weighted_average = grouped_weighted_average

1
当你说“非性能优越”的时候,具体指的是多少差距?你有进行过测量吗? - Bouncner
谈论“non-performant”:每次复制“self”到“df”并对整个“df”应用“sum”,看起来是否“performant”,甚至“sound”(有些列可能包含不应被求和的值)? - serge.v

4

以下内容(基于Wes McKinney的回答)完全实现了我所需求的功能。如果在pandas中有更简单的方法,请告诉我,我会很高兴学习。

def wavg_func(datacol, weightscol):
    def wavg(group):
        dd = group[datacol]
        ww = group[weightscol] * 1.0
        return (dd * ww).sum() / ww.sum()
    return wavg


def df_wavg(df, groupbycol, weightscol):
    grouped = df.groupby(groupbycol)
    df_ret = grouped.agg({weightscol:sum})
    datacols = [cc for cc in df.columns if cc not in [groupbycol, weightscol]]
    for dcol in datacols:
        try:
            wavg_f = wavg_func(dcol, weightscol)
            df_ret[dcol] = grouped.apply(wavg_f)
        except TypeError:  # handle non-numeric columns
            df_ret[dcol] = grouped.agg({dcol:min})
    return df_ret

df_wavg()函数返回一个按“groupby”列分组的数据帧,并返回权重列的权重总和。其他列要么是加权平均值,要么(如果不是数字)使用min()函数进行聚合。


1
您可以按照以下方式实现此函数:
(df['c'] * df['w']).groupby(df['groups']).sum() / df.groupby('groups')['w'].sum()

例如:

df = pd.DataFrame({'groups': [1, 1, 2, 2], 'c': [3, 3, 4, 4], 'w': [5, 5, 6, 6]})
(df['c'] * df['w']).groupby(df['groups']).sum() / df.groupby('groups')['w'].sum()

结果:

groups
1    3.0
2    4.0
dtype: float64

0

在 Wes MacKinney 的回答基础上,这将重命名聚合列:

grouped = df.groupby(keys)

def wavg(group):
    d = group['data']
    w = group['weights']
    return (d * w).sum() / w.sum()

grouped.apply(wavg).reset_index().rename(columns={0 : "wavg"})

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