内存有效的方式来创建一个列,该列指示一组列中值的唯一组合

时间:2019-12-13 23:05:00

标签: python pandas numpy dataframe dask

我想找到一种更有效的方法(就峰值内存使用量和可能的时间而言)来完成熊猫groupby.ngroup的工作,以便在处理大型数据集时不遇到内存问题(我请提供以下原因,说明为什么本专栏对我有用。以小数据集为例。我可以使用groupby.ngroup轻松完成此任务。

import pandas as pd
import numpy as np


df = pd.DataFrame(np.array(
        [[0, 1, 92],
        [0, 0, 39],
        [0, 0, 32],
        [1, 0, 44],
        [1, 1, 50],
        [0, 1, 11],
        [0, 0, 14]]), columns=['male', 'edu', 'wage'])

df['group_id'] = df.groupby(['male', 'edu']).ngroup()
df
   male  edu  wage  group_id
0     0    1    92         1
1     0    0    39         0
2     0    0    32         0
3     1    0    44         2
4     1    1    50         3
5     0    1    11         1
6     0    0    14         0

但是,当我开始使用更大的数据集时,N=100,000,000的内存使用量和计算时间便爆炸了,并且groupby中的内存使用量与数据帧的内存使用量之比几乎是{的三倍{1}}。见下文。

N=100,000

我为什么对这个组标识符感兴趣?因为我想使用from memory_profiler import memory_usage import time N_values = [10**k for k in range(4, 9)] stats = pd.DataFrame(index=N_values, dtype=float, columns=['time', 'basemem', 'groupby_mem']) for N in N_values: df = pd.DataFrame( np.hstack([np.random.randint(0, 2, (N, 2)), np.random.normal(5, 1, (N, 1))]), columns=['male', 'edu', 'wage'] ) def groupby_ngroup(): df.groupby(['male', 'edu']).ngroup() def foo(): pass basemem = max(memory_usage(proc=foo)) tic = time.time() mem = max(memory_usage(proc=groupby_ngroup)) toc = time.time() - tic stats.loc[N, 'basemem'] = basemem stats.loc[N, 'groupby_mem'] = mem stats.loc[N, 'time'] = toc stats['mem_ratio'] = stats.eval('groupby_mem/basemem') stats time basemem groupby_mem mem_ratio 10000 0.037834 104.781250 105.359375 1.005517 100000 0.051785 108.187500 113.125000 1.045638 1000000 0.143642 128.156250 182.437500 1.423555 10000000 0.644650 334.148438 820.183594 2.454549 100000000 6.074531 2422.585938 7095.437500 2.928869 方法创建利用熊猫的groupby函数(例如groupby.mean)的列,而不是.map,这需要大量的内存和时间。此外,groupby.transform方法可以与.map数据帧一起使用,因为dask当前不支持dask。有了.transform的专栏,我可以简单地执行"group_id"means = df.groupby(['group_id'])['wage'].mean()来完成df['mean_wage'] = df['group_id'].map(means)的工作。

3 个答案:

答案 0 :(得分:1)

不使用ngroup,而是编写我们自己的函数来创建group_id列怎么样?

这是一个似乎具有更好性能的代码段:

from memory_profiler import memory_usage
import time
import pandas as pd
import numpy as np

N_values = [10**k for k in range(4, 9)]

stats = pd.DataFrame(index=N_values, dtype=float, columns=['time', 'basemem', 'groupby_mem'])

for N in N_values:
    df = pd.DataFrame(
        np.hstack([np.random.randint(0, 2, (N, 2)), np.random.normal(5, 1, (N, 1))]),
        columns=['male', 'edu', 'wage']        
    )

    def groupby_ngroup():
        #df.groupby(['male', 'edu']).ngroup()
        df['group_id'] = 2*df.male + df.edu

    def foo():
        pass

    basemem = max(memory_usage(proc=foo))

    tic = time.time()
    mem = max(memory_usage(proc=groupby_ngroup))
    toc = time.time() - tic

    stats.loc[N, 'basemem'] = basemem
    stats.loc[N, 'groupby_mem'] = mem
    stats.loc[N, 'time'] = toc

stats['mem_ratio'] = stats.eval('groupby_mem/basemem')
stats


            time        basemem     groupby_mem mem_ratio
10000       0.117921    2370.792969 79.761719   0.033643
100000      0.026921    84.265625   84.324219   1.000695
1000000     0.067960    130.101562  130.101562  1.000000
10000000    0.220024    308.378906  536.140625  1.738577
100000000   0.751135    2367.187500 3651.171875 1.542409

从本质上讲,我们使用列为数字的事实并将其视为二进制数。 group_id是十进制等效项。

将其缩放为三列可获得相似的结果。为此,将数据帧初始化替换为以下内容:

df = pd.DataFrame(
        np.hstack([np.random.randint(0, 2, (N, 3)), np.random.normal(5, 1, (N, 1))]),
        columns=['male', 'edu','random1', 'wage']        
    )

和group_id函数用于:

def groupby_ngroup():
        df['group_id'] = 4*df.male + 2*df.edu + df.random1

以下是该测试的结果:

            time        basemem     groupby_mem mem_ratio
10000       0.050006    78.906250   78.980469   1.000941
100000      0.033699    85.007812   86.339844   1.015670
1000000     0.066184    147.378906  147.378906  1.000000
10000000    0.322198    422.039062  691.179688  1.637715
100000000   1.233054    3167.921875 5183.183594 1.636146

答案 1 :(得分:0)

让我们尝试使用hash

list(map(hash,df.to_records().tolist()))
[4686582722376372986, 3632587615391525059, 2578593961740479157, -48845846747569345, 2044051356115000853, -583388452461625474, -1637380652526859201]

答案 2 :(得分:0)

对于groupby变量具有未知模式的groupby,看来groupby.ngroup可能会和它一样好。但是,如果您的groupby变量都是类别变量,例如采用值0,1,2,3....,那么我们可以从@saurjog给出的解决方案中获得启发。

要生成组ID,我们可以构建一个数值表达式,用于计算groupby变量的特殊总和。考虑以下功能

def gen_groupby_numexpr(cols, numcats):
    txt = [cols[0]]

    k = numcats[0]

    for c,k_ in zip(cols[1:], numcats[1:]):

        txt.append('{}*{}'.format(k, c))

        k = k*k_

    return ' + '.join(txt)

def ngroup_cat(df, by, numcats):
    '''
    by : list
        the categorical (0,1,2,3...) groupby column names
    numcats : list
        the number of unique values for each column in "by"
    '''
    expr = gen_groupby_numexpr(by, numcats)

    return df.eval(expr)

函数gen_groupby_numexpr生成数值表达式,ngroup_cat生成by中具有唯一值计数numcats的groupby变量的组ID。因此,请考虑以下符合我们用例的数据集。它包含3个分类变量,我们将使用它们来构成分组依据,其中两个在{0,1}中使用值,而另一个在{0,1,2}中使用值。

df2 = pd.DataFrame(np.hstack([np.random.randint(0, 2, (100, 2)), 
                              np.random.randint(0, 3, (100, 1)), 
                              np.random.randint(0, 20, (100, 1))]), 
    columns=['male', 'mar', 'edu', 'wage'])

如果我们生成数字表达式,我们将得到:

'male + 2*mar + 4*edu'

总而言之,我们可以使用

生成组ID。
df2['group_id'] = ngroup_cat(df2, ['male', 'mar', 'edu'], [2, 2, 3])

从中我们获得2*2*3=12个唯一的组ID:

df2[['male', 'mar', 'edu', 'group_id']].drop_duplicates().sort_values(['group_id'])
    male  mar  edu  group_id
1      0    0    0         0
13     1    0    0         1
8      0    1    0         2
10     1    1    0         3
4      0    0    1         4
12     1    0    1         5
2      0    1    1         6
6      1    1    1         7
7      0    0    2         8
5      1    0    2         9
44     0    1    2        10
0      1    1    2        11

当我将上述解决方案与groupby.ngroup进行比较时,它在N=10,000,000的数据集上运行的速度快将近3倍,并且使用的额外内存大大减少。

现在,我们可以估算这些groupby平均值,然后将它们映射回整个数据帧以完成变换工作。对于使用transform还是groupby而言,我计算出一些混合结果的基准,然后map更快,内存占用更少。如果您正在计算包​​含多个变量的组的均值,那么我认为后者会更有效。此外,后者也可以在尚不支持dask的{​​{1}}中完成。