按组对DataFrame进行归一化

40

假设我有一些数据,它是按照以下方式生成的:

N = 20
m = 3
data = np.random.normal(size=(N,m)) + np.random.normal(size=(N,m))**3

然后我创建了一些分类变量:

indx = np.random.randint(0,3,size=N).astype(np.int32)

并生成一个DataFrame:

import pandas as pd
df = pd.DataFrame(np.hstack((data, indx[:,None])), 
             columns=['a%s' % k for k in range(m)] + [ 'indx'])

我可以按组别获取均值,方法如下:

df.groubpy('indx').mean()

我不确定如何对原始数据中每列分组的数据减去该组的均值,以便使每列中的数据都按组内均值标准化。欢迎提出任何建议。

4个回答

86
In [10]: df.groupby('indx').transform(lambda x: (x - x.mean()) / x.std())

应该做。


34

如果数据包含许多组(数千个或更多),使用 lambda 的已接受的答案可能需要很长时间才能计算。一种快速解决方案是:

groups = df.groupby("indx")
mean, std = groups.transform("mean"), groups.transform("std")
normalized = (df[mean.columns] - mean) / std

解释和基准测试

被接受的答案使用apply和lambda存在性能问题。尽管groupby.transform本身很快,lambda函数中已经向量化的调用(.mean().std()和减法)也都很快,但是对于每个组而言,纯Python lambda函数的调用会创建相当大的开销。

可以通过使用纯向量化的Pandas/Numpy调用而不编写任何Python方法来避免这种情况,正如ErnestScribbler的答案所示。

我们可以利用.transform的广播功能来避免合并和命名列的麻烦。让我们将上面的解决方案放入一个用于基准测试的方法中:

def normalize_by_group(df, by):
    groups = df.groupby(by)
    # computes group-wise mean/std,
    # then auto broadcasts to size of group chunk
    mean = groups.transform("mean")
    std = groups.transform("std")
    normalized = (df[mean.columns] - mean) / std
    return normalized

我将原始问题中的数据生成方式更改,以允许更多的组:

def gen_data(N, num_groups):
    m = 3
    data = np.random.normal(size=(N,m)) + np.random.normal(size=(N,m))**3
    indx = np.random.randint(0,num_groups,size=N).astype(np.int32)

    df = pd.DataFrame(np.hstack((data, indx[:,None])), 
                      columns=['a%s' % k for k in range(m)] + [ 'indx'])
    return df

使用仅两个组(因此仅两个Python函数调用),lambda版本的速度仅比numpy代码慢大约1.8倍:

In: df2g = gen_data(10000, 2)  # 3 cols, 10000 rows, 2 groups

In: %timeit normalize_by_group(df2g, "indx")
6.61 ms ± 72.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In: %timeit df2g.groupby('indx').transform(lambda x: (x - x.mean()) / x.std())
12.3 ms ± 130 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

将组数增加到1000,运行时间问题就会变得明显。Lambda版本比Numpy代码慢370倍:

In: df1000g = gen_data(10000, 1000)  # 3 cols, 10000 rows, 1000 groups

In: %timeit normalize_by_group(df1000g, "indx")
7.5 ms ± 87.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In: %timeit df1000g.groupby('indx').transform(lambda x: (x - x.mean()) / x.std())
2.78 s ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

有没有任何情况下会因为额外的功能调用导致延迟真正关键?我对这些操作不太熟悉,所以实际上很好奇是否存在这样的场景,其中仅由额外的函数调用造成操作之间十秒钟的差异。在实际操作中,13毫秒几乎不会被注意到,而且这是我每次为了代码整洁而愿意支付的代价。我想如果您将这些操作串联在一起并重复执行它们,那么时间会累计,但这是否是极端或正常的情况呢? - Vince W.
1
是的,有一些情况下会很明显!当然这取决于数据。我的数据集大约包含160万个组。使用Lambda方法需要运行一个多小时。这就是为什么我写了这个程序,即使对于160万个组也只需要几秒钟就能完成。我完全同意你的观点 - 如果你只有少数几个组,那么选择简洁明了的版本,几毫秒并不重要。 - w-m
很有意思,谢谢回复。我能理解为什么160万个群组会成为寻找更快的方法的动力。 - Vince W.

4

接受的答案可行且优雅。但是,对于大型数据集,我认为在性能上使用 .transform() 要比使用不太优雅的如下方式慢得多(以单列 'a0' 为例):

means_stds = df.groupby('indx')['a0'].agg(['mean','std']).reset_index()
df = df.merge(means_stds,on='indx')
df['a0_normalized'] = (df['a0'] - df['mean']) / df['std']

要对多个列进行操作,您需要找出如何合并它们。我的建议是按照这个回答的方法从聚合中展平多索引列,然后分别对每一列进行合并和标准化:

means_stds = df.groupby('indx')[['a0','a1']].agg(['mean','std']).reset_index()
means_stds.columns = ['%s%s' % (a, '|%s' % b if b else '') for a, b in means_stds.columns]
df = df.merge(means_stds,on='indx')
for col in ['a0','a1']:
    df[col+'_normalized'] = ( df[col] - df[col+'|mean'] ) / df[col+'|std']

1
尽管这不是最美观的解决方案,但你可以像这样做:
indx = df['indx'].copy()
for indices in df.groupby('indx').groups.values():
    df.loc[indices] -= df.loc[indices].mean()
df['indx'] = indx

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