如果数据包含许多组(数千或更多),则使用 lambda 的 accepted answer 可能需要很长时间来计算。一个快速的解决方案是:
groups = df.groupby("indx")
mean, std = groups.transform("mean"), groups.transform("std")
normalized = (df[mean.columns] - mean) / std
解释和基准测试
接受的答案在使用带有 lambda 的 apply 时会遇到性能问题。尽管 groupby.transform 本身很快,就像 lambda 函数中已经矢量化的调用(.mean()、.std() 和减法)一样,对每个组的纯 Python lambda 函数本身的调用也会产生相当大的开销。
这可以通过使用纯矢量化 Pandas/Numpy 调用而不编写任何 Python 方法来避免,如 ErnestScribbler's answer 所示。
利用.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)