按组规范化 DataFrame
Posted
技术标签:
【中文标题】按组规范化 DataFrame【英文标题】:Normalize DataFrame by group 【发布时间】:2014-11-20 16:14:29 【问题描述】:假设我有一些数据生成如下:
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()
我不确定该怎么做,然后从原始数据中的每列减去每组的平均值,以便每列中的数据通过组内的平均值进行归一化。任何建议将不胜感激。
【问题讨论】:
【参考方案1】:In [10]: df.groupby('indx').transform(lambda x: (x - x.mean()) / x.std())
应该这样做。
【讨论】:
太棒了。我只是在学习 Pandas,还没有偶然发现transform
。这非常优雅,比@Mike 的解决方案要快一点。【参考方案2】:
如果数据包含许多组(数千或更多),则使用 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)
【讨论】:
在任何情况下这会转化为真正重要的延迟吗?我对这些操作不太熟悉,所以我真的很好奇是否存在这样一种情况,即仅由额外的函数调用引起的操作之间会有数十秒的差异。 13 毫秒在实际操作中几乎看不到,而且我每次都会为代码清洁度付出代价,我想如果你将这些操作菊花链在一起并重复执行它可能会加起来,但这会是极端的还是正常的?跨度> 是的,在某些情况下它很明显!当然,这取决于数据。我的数据集包含大约 160 万个组。 lambda 方法将运行一个多小时。这就是我写这篇文章的原因,即使是 160 万组,它也会在几秒钟内完成。我完全同意你的观点——如果你只有少数几个组,那就选择干净简单的版本,几毫秒无关紧要。 令人着迷,感谢您的回复。我明白为什么 160 万个群组会成为寻找更快方法的动力。【参考方案3】:接受的答案有效且优雅。 不幸的是,对于大型数据集,我认为在性能方面使用 .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']
要为多个列执行此操作,您必须弄清楚合并。我的建议是像this answer 那样从聚合中展平多索引列,然后分别对每一列进行合并和规范化:
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']
【讨论】:
【参考方案4】:虽然这不是最漂亮的解决方案,但您可以这样做:
indx = df['indx'].copy()
for indices in df.groupby('indx').groups.values():
df.loc[indices] -= df.loc[indices].mean()
df['indx'] = indx
【讨论】:
以上是关于按组规范化 DataFrame的主要内容,如果未能解决你的问题,请参考以下文章
pandas DataFrame:规范化一个 JSON 列并与其他列合并
Python Pandas Dataframe:规范化 0.01 到 0.99 之间的数据?