Pandas数据框中的按组加权平均和求和

89
我有一个数据框:
    Out[78]: 
   contract month year  buys  adjusted_lots    price
0         W     Z    5  Sell             -5   554.85
1         C     Z    5  Sell             -3   424.50
2         C     Z    5  Sell             -2   424.00
3         C     Z    5  Sell             -2   423.75
4         C     Z    5  Sell             -3   423.50
5         C     Z    5  Sell             -2   425.50
6         C     Z    5  Sell             -3   425.25
7         C     Z    5  Sell             -2   426.00
8         C     Z    5  Sell             -2   426.75
9        CC     U    5   Buy              5  3328.00
10       SB     V    5   Buy              5    11.65
11       SB     V    5   Buy              5    11.64
12       SB     V    5   Buy              2    11.60

我需要计算“adjusted_lots”总和,以价格加权平均值的方式,按照其他所有列分组,即按(合同,月份,年份和购买者)分组。

在R上使用dplyr可以实现类似的解决方案,但是在pandas中无法做到相同。

> newdf = df %>%
  select ( contract , month , year , buys , adjusted_lots , price ) %>%
  group_by( contract , month , year ,  buys) %>%
  summarise(qty = sum( adjusted_lots) , avgpx = weighted.mean(x = price , w = adjusted_lots) , comdty = "Comdty" )

> newdf
Source: local data frame [4 x 6]

  contract month year comdty qty     avgpx
1        C     Z    5 Comdty -19  424.8289
2       CC     U    5 Comdty   5 3328.0000
3       SB     V    5 Comdty  12   11.6375
4        W     Z    5 Comdty  -5  554.8500

是否可以通过groupby或其他解决方案实现相同的功能?

8个回答

174

编辑: 更新聚合函数以使其与最新版的pandas兼容

要将多个函数传递给groupby对象,您需要传递一个元组,其中包含聚合函数和应用该函数的列:

# Define a lambda function to compute the weighted mean:
wm = lambda x: np.average(x, weights=df.loc[x.index, "adjusted_lots"])

# Define a dictionary with the functions to apply for a given column:
# the following is deprecated since pandas 0.20:
# f = {'adjusted_lots': ['sum'], 'price': {'weighted_mean' : wm} }
# df.groupby(["contract", "month", "year", "buys"]).agg(f)

# Groupby and aggregate with namedAgg [1]:
df.groupby(["contract", "month", "year", "buys"]).agg(adjusted_lots=("adjusted_lots", "sum"),  
                                                      price_weighted_mean=("price", wm))

                          adjusted_lots  price_weighted_mean
contract month year buys                                    
C        Z     5    Sell            -19           424.828947
CC       U     5    Buy               5          3328.000000
SB       V     5    Buy              12            11.637500
W        Z     5    Sell             -5           554.850000

你可以在这里查看更多内容:

以及一个类似的问题:

[1] : https://pandas.pydata.org/pandas-docs/stable/whatsnew/v0.25.0.html#groupby-aggregation-with-relabeling


我尝试了这个方法,结果得到了我想要的东西,但是前几列变成了多重索引。请问您能否告诉我们如何解决这个问题?我需要它成为一个数据框,所有的索引都作为列,就像您的例子中(contract,month,year.buys)变成了索引一样。 - Saurabh Bade
你尝试过 df.reset_index() 吗?否则我建议你开一个新的问题,说明你有什么,你尝试了什么以及你想要什么。 - jrjc
2
我能澄清一下wm = lambda x: np.average(x, weights=df.loc[x.index, "adjusted_lots"])中的x.index代表什么吗?@jrjc - Lko
很棒的解决方案!它救了我一次。有没有一种方法可以在 expanding().mean() 的原理下实现它? - Daniel Arges
大家好,我想推荐下面的答案,因为它们不仅易于记忆,而且矢量化,速度更快。 - W. Ding
太好了,谢谢!除了过滤掉adjusted_lots = 0之外,还有其他方法可以防止ZeroDivisionError吗? - Niv Cohen

17

使用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

2
你的解决方案没有包括原始问题中需要存在的“adjusted_lot sums”。 - antonio_zeus
1
如果有人需要帮助理解,加权平均通常是:val_0 * weight_0 + val_1 * weight_1 + ... + val_n * weight_n,其中所有权重总和为1.0。当sum(weight) != 0时,您需要通过将其除以总权重来对其进行归一化。在此方法中,我们计算val_i * non-normalized_weight_i (_data_times_weight)和单独的non_normalized_weight_i (当数据不为空时,_weight_where_notnull)。然后,我们分组并汇总每个组的val_i*non_normalized_weight_i,并除以该组的总非标准化权重,以便对其进行归一化。 - ajoseps

14

这样做难道不会更简单吗?

  1. 将(adjusted_lots * price_weighted_mean)相乘,创建一个名为“X”的新列
  2. 使用groupby().sum()对“X”和“adjusted_lots”列进行分组,得到分组后的df df_grouped
  3. 在df_grouped上计算加权平均值,即df_grouped['X']/df_grouped['adjusted_lots']

这种方式更容易记忆,不需要每次查找语法。 - pitcheverlasting
1
而且这种方式更快。在处理十亿级别的数据时,得到最高赞的答案非常慢。 - W. Ding
第一步中的 "price_weighted_mean" 实际上应该是 "price" 吗? - Jossy

8

在未来的pandas版本(0.22)中,使用聚合函数字典的解决方案将被弃用:

FutureWarning: using a dict with renaming is deprecated and will be removed in a future 
version return super(DataFrameGroupBy, self).aggregate(arg, *args, **kwargs)

使用groupby apply并返回一个Series来重命名列,如以下讨论所述: 重命名Pandas聚合结果列(“FutureWarning: using a dict with renaming is deprecated”)

def my_agg(x):
    names = {'weighted_ave_price': (x['adjusted_lots'] * x['price']).sum()/x['adjusted_lots'].sum()}
    return pd.Series(names, index=['weighted_ave_price'])

产生相同的结果:

>df.groupby(["contract", "month", "year", "buys"]).apply(my_agg)

                          weighted_ave_price
contract month year buys                    
C        Z     5    Sell          424.828947
CC       U     5    Buy          3328.000000
SB       V     5    Buy            11.637500
W        Z     5    Sell          554.850000

我的数据框中加权列的值为零。在这种情况下,它将是adjusted_lots。我想避免这个警告:RuntimeWarning: invalid value encountered in double_scalars. 有什么有效的方法可以使用这个设置来做到这一点?一个可能的方法是:链接,但我不知道如何将其与您的解决方案结合起来。希望得到指导。 - Inder Jalli
2
你回答的措辞可能会被误读。将被弃用的是同时聚合、重命名和添加多级索引的能力。未来仍然可以使用字典在每列中使用不同和多个聚合函数。 - Buggy
@InderJalli 或许这个答案能帮助你处理零值。 - Asclepius

3

使用 datar,你不必学习pandas API就可以将你的R代码转换:

>>> from datar.all import f, tibble, c, rep, select, summarise, sum, weighted_mean, group_by
>>> df = tibble(
...     contract=c('W', rep('C', 8), 'CC', rep('SB', 3)),
...     month=c(rep('Z', 9), 'U', rep('V', 3)),
...     year=5,
...     buys=c(rep('Sell', 9), rep('Buy', 4)),
...     adjusted_lots=[-5, -3, -2, -2, -3, -2, -3, -2, -2, 5, 5, 5, 2],
...     price=[554.85, 424.50, 424.00, 423.75, 423.50, 425.50, 425.25, 426.00, 426.75,3328.00, 11.65, 11.64, 1
1.60]
... )
>>> df
   contract month  year  buys  adjusted_lots    price
0         W     Z     5  Sell             -5   554.85
1         C     Z     5  Sell             -3   424.50
2         C     Z     5  Sell             -2   424.00
3         C     Z     5  Sell             -2   423.75
4         C     Z     5  Sell             -3   423.50
5         C     Z     5  Sell             -2   425.50
6         C     Z     5  Sell             -3   425.25
7         C     Z     5  Sell             -2   426.00
8         C     Z     5  Sell             -2   426.75
9        CC     U     5   Buy              5  3328.00
10       SB     V     5   Buy              5    11.65
11       SB     V     5   Buy              5    11.64
12       SB     V     5   Buy              2    11.60
>>> newdf = df >> \
...   select(f.contract, f.month, f.year, f.buys, f.adjusted_lots, f.price) >> \
...   group_by(f.contract, f.month, f.year, f.buys) >> \
...   summarise(
...       qty = sum(f.adjusted_lots), 
...       avgpx = weighted_mean(x = f.price , w = f.adjusted_lots), 
...       comdty = "Comdty"
...   )
[2021-05-24 13:11:03][datar][   INFO] `summarise()` has grouped output by ['contract', 'month', 'year'] (overr
ide with `_groups` argument)
>>> 
>>> newdf
  contract month  year  buys  qty        avgpx  comdty
0        C     Z     5  Sell  -19   424.828947  Comdty
1       CC     U     5   Buy    5  3328.000000  Comdty
2       SB     V     5   Buy   12    11.637500  Comdty
3        W     Z     5  Sell   -5   554.850000  Comdty
[Groups: ['contract', 'month', 'year'] (n=4)]

我是这个软件包的作者。如果您有任何问题,请随时提交问题。


3
Pandas为什么没有添加加权平均函数? - safex

1

这将 jrjc 的原始方法 MB 的闭包方法结合起来。它的优点是能够重复使用闭包函数。

import pandas as pd

def group_weighted_mean_factory(df: pd.DataFrame, weight_col_name: str):
    # Ref: https://dev59.com/kFwZ5IYBdhLWcg3wYvck#69787938/
    def group_weighted_mean(x):
        try:
            return np.average(x, weights=df.loc[x.index, weight_col_name])
        except ZeroDivisionError:
            return np.average(x)
    return group_weighted_mean

df = ...  # Define
group_weighted_mean = group_weighted_mean_factory(df, "adjusted_lots")
g = df.groupby(...)  # Define
agg_df = g.agg({'price': group_weighted_mean})

然而,这种方法比Rohit P的答案需要更多不必要的复杂性。回过头来看,我会选择使用Rohit P的答案。


0

我在面对类似问题时发现了这个帖子。在我的情况下,我想生成一个加权平均数,以计算在某个NFL比赛中多名四分卫尝试传球的情况。

如果我在扩展过程中遇到了重大性能问题,我可能会更改代码。目前为止,我倾向于将我的解决方案压缩到.agg函数中并与其他转换一起使用。如果有人有更简单的解决方案以达到相同的目的,我很高兴看到。最终,我采用了闭包模式。

闭包方法的神奇之处,如果这对未来读者来说是一个陌生的模式,那么我仍然可以返回一个简单的函数给pandas的.agg()方法,但我可以在顶层的工厂函数中预配置一些额外的信息。

def weighted_mean_factory(*args, **kwargs):
    weights = kwargs.get('w').copy()
    
    def weighted_mean(x):
        x_mask = ~np.isnan(x)
        w = weights.loc[x.index]
        
        if all(v is False for v in x_mask):
            raise ValueError('there are no non-missing x variable values')

        return np.average(x[x_mask], weights=w[x_mask])
    
    return weighted_mean

res_df = df.groupby(['game_id', 'team'])\
    .agg(pass_player_cnt=('attempts', count_is_not_zero),
         completions=('completions', 'sum'), 
         attempts=('attempts', 'sum'),
         pass_yds=('pass_yards', 'sum'),
         pass_tds=('pass_tds', 'sum'), 
         pass_int=('pass_int', 'sum'), 
         sack_taken=('sacks_taken', 'sum'), 
         sack_yds_loss=('sack_yds_loss', 'sum'), 
         longest_completion=('longest_completion', 'max'),
         qbr_w_avg=('qb_rating', weighted_mean_factory(x='qb_rating', w=df['attempts']))
         )

以下是一个形状为(5436,31)的DataFrame的一些基本基准统计数据,就性能而言,在这个阶段我并不担心:

149 ms ± 4.75 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

0

ErnestScribbler的答案比被接受的解决方案快得多。这里是一个多元模拟:

def weighted_average(df,data_col,weight_col,by_col):
    ''' Now data_col can be a list of variables '''
    df_data = df[data_col].multiply(df[weight_col], axis='index')
    df_weight = pd.notnull(df[data_col]).multiply(df[weight_col], axis='index')
    df_data[by_col] = df[by_col]
    df_weight[by_col] = df[by_col]    
    result = df_data.groupby(by_col).sum() / df_weight.groupby(by_col).sum()
    return result

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