在Python Pandas中对千万元素的分块文件进行groupby时遇到问题

4 投票
1 回答
1773 浏览
提问于 2025-04-18 18:16

我有一个非常大的CSV文件(几十个GB),里面包含了网页日志,列有:user_id(用户ID)、time_stamp(时间戳)、category_clicked(点击的类别)。我需要建立一个评分系统,来识别用户喜欢和不喜欢的类别。需要注意的是,我有超过1000万的用户。

我首先把文件分成小块,存储在一个叫input.h5HDFStore中,然后我使用groupby根据user_id进行分组,按照Jeff的方法

我的数据大约有2亿行,1000万个独特的用户ID。

user id | timestamp | category_clicked
20140512081646222000004-927168801|20140722|7
20140512081714121000004-383009763|20140727|4
201405011348508050000041009490586|20140728|1
20140512081646222000004-927168801|20140724|1
20140501135024818000004-1623130763|20140728|3

这是我的pandas版本:

INSTALLED VERSIONS
------------------
commit: None
python: 2.7.6.final.0
python-bits: 64
OS: Windows
OS-release: 8
machine: AMD64
processor: AMD64 Family 21 Model 2 Stepping 0, AuthenticAMD
byteorder: little
LC_ALL: None
LANG: fr_FR

pandas: 0.13.1
Cython: 0.20.1
numpy: 1.8.1
scipy: 0.13.3
statsmodels: 0.5.0
IPython: 2.0.0
sphinx: 1.2.2
patsy: 0.2.1
scikits.timeseries: None
dateutil: 2.2
pytz: 2013.9
bottleneck: None
tables: 3.1.1
numexpr: 2.3.1
matplotlib: 1.3.1
openpyxl: None
xlrd: 0.9.3
xlwt: 0.7.5
xlsxwriter: None
sqlalchemy: 0.9.4
lxml: None
bs4: None
html5lib: None
bq: None
apiclient: None

我想要的输出是这样的:

对于每个用户ID,生成一个列表[0.1,0.45,0.89,1.45,5.12,0.,0.,0.45,0.12,2.36,7.8],这个列表代表用户在每个类别上的得分,还有一个总得分。我不能告诉你更多关于得分的细节,但计算得分需要所有的时间戳和点击的类别。你不能事后再去加总或做其他处理。

这是我的代码:

clean_input_reader = read_csv(work_path + '/input/input.csv', chunksize=500000)
with get_store(work_path+'/input/input.h5') as store:
    for chunk in clean_input_reader:
        store.append('clean_input', chunk,
                     data_columns=['user_id','timestamp','category_clicked'],
                     min_itemsize=15)

    groups = store.select_column('clean_input','user_id').unique()
    for user in groups:
        group_user = store.select('clean_input',where=['user_id==%s' %user])
        <<<<TREATMENT returns a list user_cat_score>>>>
        store.append(user, Series(user_cat_score))

我的问题是:我觉得这一行代码:

group_user=store.select('clean_input',where=['user_id==%s' %user])

在时间复杂度上太重,因为我有很多组,我相信在执行store.select时会有很多冗余的排序,如果我执行1000万次的话。

给你一个估算,我用这种方法处理1000个键需要250秒,而如果用普通的groupby,直接读取完整的内存CSV文件,使用read_csv而不分块,只需要1秒

**********更新***********

在应用了Jeff的哈希方法后,我可以在1秒内处理1000个键(和完整内存方法的时间一样),并且大大减少了内存使用。唯一的时间开销是我之前没有考虑到的,就是分块、保存100个哈希组,以及从哈希组中获取真实组的时间。但这个操作不会超过几分钟。

1 个回答

6

这里有一个解决方案,可以让这个问题的规模变得更灵活。实际上,这就像是这个问题的高密度版本,具体可以参考这里

首先,定义一个函数,把特定的组值转换成更少的组。我的设计思路是把你的数据集分成可以在内存中轻松处理的小块。

def sub_group_hash(x):
    # x is a dataframe with the 'user id' field given above
    # return the last 2 characters of the input
    # if these are number like, then you will be sub-grouping into 100 sub-groups
    return x['user id'].str[-2:]

使用上面提供的数据,这样就可以在输入数据上创建一个分组框架:

In [199]: [ (grp, grouped) for grp, grouped in df.groupby(sub_group_hash) ][0][1]
Out[199]: 
                             user id  timestamp  category
0  20140512081646222000004-927168801   20140722         7
3  20140512081646222000004-927168801   20140724         1

其中,grp是组的名称,grouped是结果框架。

# read in the input in a chunked way
clean_input_reader = read_csv('input.csv', chunksize=500000)
with get_store('output.h5') as store:
    for chunk in clean_input_reader:

        # create a grouper for each chunk using the sub_group_hash
        g = chunk.groupby(sub_group_hash)

        # append each of the subgroups to a separate group in the resulting hdf file
        # this will be a loop around the sub_groups (100 max in this case)
        for grp, grouped in g:

            store.append('group_%s' % grp, grouped,
                         data_columns=['user_id','timestamp','category_clicked'],
                         min_itemsize=15)

现在你有一个包含100个子组的hdf文件(如果不是所有组都有数据,可能会更少),每个子组都包含进行操作所需的所有数据。

with get_store('output.h5') as store:

    # all of the groups are now the keys of the store
    for grp in store.keys():

        # this is a complete group that will fit in memory
        grouped = store.select(grp)

        # perform the operation on grouped and write the new output
        grouped.groupby(......).apply(your_cool_function)

这样一来,这个问题的复杂度就减少了100倍。如果这样还不够,那就简单地增加sub_group_hash,来创建更多的组。

你应该尽量让组的数量保持较小,因为HDF5在处理较少的组时效果更好(比如不要创建1000万个子组,这样就失去了意义,100、1000甚至1万都是可以的)。不过我觉得100个应该对你有帮助,除非你的组密度非常高(比如某个组的数据量巨大,而其他组的数据量很少)。

注意,这个问题的规模扩展起来很简单;如果需要,你可以把子组存储在不同的文件中,或者单独处理它们(并行处理)。

这样一来,你的解决方案的时间复杂度大约是O(子组数量)

撰写回答