按组规范化 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

按行规范化 pandas DataFrame

pandas DataFrame:规范化一个 JSON 列并与其他列合并

Python Pandas Dataframe:规范化 0.01 到 0.99 之间的数据?

在大熊猫DataFrame中按组删除异常值的更快方法[重复]

Julia:将 DataFrame 传递给函数会创建指向 DataFrame 的指针?