使用多列的Pandas DataFrame聚合函数


80

有没有一种方法可以像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 ...

不错的文章解决了这个特定的SO问题:pbpython.com/weighted-average.html
ptim

Answers:


104

是; 使用该.apply(...)函数,该函数将在每个sub-上调用DataFrame。例如:

grouped = df.groupby(keys)

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

grouped.apply(wavg)

将其分解为以下几个操作可能更有效:(1)创建一列权重,(2)通过其权重对观测值进行归一化,(3)计算加权观测值的分组和与加权值的分组和,(4)用权重之和归一化观测值的加权和。
kalu 2014年

4
如果我们要计算许多变量(列)的wavg,例如除df ['weights']外的所有东西,该怎么办?
CPBL 2014年

2
@Wes,自从这篇文章首次出现以来,是否有任何办法可以解决这个问题agg()lambda建立基础np.average(...weights=...),或者在熊猫中有任何新的本地支持以加权方式使用?
sparc_spread 2015年

4
@Wes麦金尼:您在书中建议这种做法: get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg) 两者可以互换吗?
robroc

9

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

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

1
但是请注意,df不是内部对象。它是函数的一个参数,只要您从未分配给它(df = something),它就一直是浅表副本并就地更改。在这种情况下,列将添加到DataFrame中。尝试复制粘贴此函数并在不使用任何del行的情况下运行它,并查看它是否通过添加列来更改给定的DataFrame。
ErnestScribbler

这不能回答问题,因为加权平均值仅作为多列上任何聚合的示例。
user__42 '20

8

使用可以从groupby对象返回任意数量的聚合值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。请注意,它如何使用多列,而agggroupby方法是不可能的:

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)

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

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

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

如其他答案中所述,可以通过将加权总数预先计算到新的DataFrame列中来获得更好的性能,并避免apply完全使用。


4

以下内容(基于韦斯·麦金尼的回答)完全可以满足我的需求。我很高兴了解到是否有更简单的方法可以做到这一点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”列分组的数据框,并返回weights列的权重之和。其他列要么是加权平均值,要么是非平均值的min()函数用于聚合。


4

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

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和删除非数字列中所有数字列的加权平均值。


这太快了!很好!
谢伊·本·萨森

如果您有多个列,这真的很不错。真好!
克里斯,

@santon,谢谢您的回答。您能举个解决方案的例子吗?尝试使用您的解决方案时出现错误“ KeyError:'COUNT”。
艾伦(Allen)

@Allen您应该使用具有要用于加权平均值的计数的列名称。
桑顿

4

通过via完成此操作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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.