如果数据包含许多组(数千个或更多),使用 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)
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)
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)
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)