并行化在pandas groupby之后应用

时间:2014-10-03 22:43:28

标签: python pandas parallel-processing rosetta

我已经使用rosetta.parallel.pandas_easy来并行化分组后应用,例如:

from rosetta.parallel.pandas_easy import groupby_to_series_to_frame
df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
groupby_to_series_to_frame(df, np.mean, n_jobs=8, use_apply=True, by=df.index)

但是,是否有人想出如何并行化返回数据帧的函数?正如预期的那样,此代码对rosetta失败。

def tmpFunc(df):
    df['c'] = df.a + df.b
    return df

df.groupby(df.index).apply(tmpFunc)
groupby_to_series_to_frame(df, tmpFunc, n_jobs=1, use_apply=True, by=df.index)

6 个答案:

答案 0 :(得分:74)

这似乎有效,虽然它确实应该内置于pandas

import pandas as pd
from joblib import Parallel, delayed
import multiprocessing

def tmpFunc(df):
    df['c'] = df.a + df.b
    return df

def applyParallel(dfGrouped, func):
    retLst = Parallel(n_jobs=multiprocessing.cpu_count())(delayed(func)(group) for name, group in dfGrouped)
    return pd.concat(retLst)

if __name__ == '__main__':
    df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
    print 'parallel version: '
    print applyParallel(df.groupby(df.index), tmpFunc)

    print 'regular version: '
    print df.groupby(df.index).apply(tmpFunc)

    print 'ideal version (does not work): '
    print df.groupby(df.index).applyParallel(tmpFunc)

答案 1 :(得分:39)

Ivan的答案很棒,但看起来可以略微简化,也不需要依赖joblib:

from multiprocessing import Pool, cpu_count

def applyParallel(dfGrouped, func):
    with Pool(cpu_count()) as p:
        ret_list = p.map(func, [group for name, group in dfGrouped])
    return pandas.concat(ret_list)

顺便说一下:这不能取代任何 groupby.apply(),但它将涵盖典型案例:例如:它应该涵盖案例2和3 in the documentation,而您应该通过将参数axis=1提供给最终的pandas.concat()来获取案例1的行为。

答案 2 :(得分:10)

我有一个hack用于在Pandas中获得并行化。我将数据帧分成块,将每个块放入列表元素中,然后使用ipython的并行位对数据帧列表进行并行应用。然后我使用pandas concat函数将列表重新组合在一起。

然而,这通常不适用。它适用于我,因为我想要应用于数据帧的每个块的功能大约需要一分钟。将我的数据拆分并整理在一起并不需要那么长时间。所以这显然是一个障碍。话虽如此,这是一个例子。我使用的是Ipython笔记本,因此您可以在我的代码中看到%%time魔法:

## make some example data
import pandas as pd

np.random.seed(1)
n=10000
df = pd.DataFrame({'mygroup' : np.random.randint(1000, size=n), 
                   'data' : np.random.rand(n)})
grouped = df.groupby('mygroup')

对于这个例子,我要制作' chunks'基于上面的groupby,但这不一定是数据如何分块。虽然这是一种非常常见的模式。

dflist = []
for name, group in grouped:
    dflist.append(group)

设置并行位

from IPython.parallel import Client
rc = Client()
lview = rc.load_balanced_view()
lview.block = True

写一个愚蠢的函数来应用我们的数据

def myFunc(inDf):
    inDf['newCol'] = inDf.data ** 10
    return inDf

现在让我们以串行方式然后并行运行代码。 序列第一:

%%time
serial_list = map(myFunc, dflist)
CPU times: user 14 s, sys: 19.9 ms, total: 14 s
Wall time: 14 s

现在并行

%%time
parallel_list = lview.map(myFunc, dflist)

CPU times: user 1.46 s, sys: 86.9 ms, total: 1.54 s
Wall time: 1.56 s

然后只需几毫秒就可以将它们合并回一个数据帧

%%time
combinedDf = pd.concat(parallel_list)
 CPU times: user 296 ms, sys: 5.27 ms, total: 301 ms
Wall time: 300 ms

我在MacBook上运行了6个IPython引擎,但你可以看到它将执行时间从14秒降低到2秒。

对于真正长时间运行的随机模拟,我可以通过使用StarCluster启动集群来使用AWS后端。但是,在很多时候,我只在我的MBP上的8个CPU上并行化。

答案 3 :(得分:3)

陪伴JD Long回答的简短评论。我发现,如果组的数量非常大(比如数十万),并且你的apply函数正在做一些相当简单和快速的事情,那么就将你的数据框分解成块并将每个块分配给一个工人来携带outby-apply(串行)可以比执行并行groupby-apply并让worker读取包含多个组的队列快得多。例如:

import pandas as pd
import numpy as np
import time
from concurrent.futures import ProcessPoolExecutor, as_completed

nrows = 15000
np.random.seed(1980)
df = pd.DataFrame({'a': np.random.permutation(np.arange(nrows))})

所以我们的数据框架如下:

    a
0   3425
1   1016
2   8141
3   9263
4   8018

请注意列' a'有很多小组(想想客户ID):

len(df.a.unique())
15000

对我们的小组进行操作的功能:

def f1(group):
    time.sleep(0.0001)
    return group

启动游泳池:

ppe = ProcessPoolExecutor(12)
futures = []
results = []

执行并行groupby-apply:

%%time

for name, group in df.groupby('a'):
    p = ppe.submit(f1, group)
    futures.append(p)

for future in as_completed(futures):
    r = future.result()
    results.append(r)

df_output = pd.concat(results)
del ppe

CPU times: user 18.8 s, sys: 2.15 s, total: 21 s
Wall time: 17.9 s

现在让我们添加一个列,将df分成更少的组:

df['b'] = np.random.randint(0, 12, nrows)

现在代替15000组,只有12个:

len(df.b.unique())
12

我们将我们的df分区并对每个块执行groupby-apply。

ppe = ProcessPoolExecutor(12)

包装乐趣:

def f2(df):
    df.groupby('a').apply(f1)
    return df

以串行方式发送要操作的每个块:

%%time

for i in df.b.unique():
    p = ppe.submit(f2, df[df.b==i])
    futures.append(p)

for future in as_completed(futures):
    r = future.result()
    results.append(r)

df_output = pd.concat(results) 

CPU times: user 11.4 s, sys: 176 ms, total: 11.5 s
Wall time: 12.4 s

请注意,每组花费的时间量没有变化。而改变的是工作人员从中读取的队列的长度。我怀疑发生的事情是工作人员不能同时访问共享内存,并且不断返回以读取队列,因此踩到彼此的脚趾。随着更大的块运行,工作人员返回的频率降低,因此这个问题得到改善,整体执行速度更快。

答案 4 :(得分:0)

我个人建议按照this thread使用dask。

正如@chrisb指出的那样,在python中使用熊猫进行多处理可能会产生不必要的开销。它也可能表现好于多线程甚至是单线程。

Dask是专门为多进程创建的。

答案 5 :(得分:0)

您可以使用multiprocessingjoblib来实现并行化。但是,如果组的数量很大而每个组的DataFrame很大,则运行时间会更糟,因为您需要将这些组多次转移到CPU中。为了减少开销,我们可以先将数据分成大块,然后对这些块进行并行计算。

例如,假设您正在处理库存数据,您需要在其中按库存代码对库存进行分组,然后计算一些统计数据。您可以先按代码的第一个字符(大块)进行分组,然后在此虚拟组中执行操作:

import pandas as pd
from joblib import Parallel, delayed

def group_func(dummy_group):
    # Do something to the group just like doing to the original dataframe.
    #     Example: calculate daily return.
    res = []
    for _, g in dummy_group.groupby('code'):
        g['daily_return']  = g.close / g.close.shift(1)
        res.append(g)
    return pd.concat(res)

stock_data = stock_data.assign(dummy=stock_data['code'].str[0])

Parallel(n_jobs=-1)(delayed(group_func)(group) for _, group in stock_data.groupby('dummy'))