如何在一台机器上使用所有内核的Pandas Dataframes并行化apply()?

时间:2017-08-07 10:49:23

标签: pandas dask

截至2017年8月,不幸的是,Pandas DataFame.apply()仅限于使用单核,这意味着当您运行df.apply(myfunc, axis=1)时,多核机器将浪费大部分计算时间。

如何使用所有核心并行运行应用于数据帧?

10 个答案:

答案 0 :(得分:63)

最简单的方法是使用Dask's map_partitions。您需要这些导入(您需要pip install dask):

import pandas as pd
import dask.dataframe as dd
from dask.multiprocessing import get

,语法是

data = <your_pandas_dataframe>
ddata = dd.from_pandas(data, npartitions=30)

def myfunc(x,y,z, ...): return <whatever>

res = ddata.map_partitions(lambda df: df.apply((lambda row: myfunc(*row)), axis=1)).compute(get=get)  

(如果您有16个核心,我认为30是适当数量的分区)。为了完整起见,我在机器上计算了差异(16个核心):

data = pd.DataFrame()
data['col1'] = np.random.normal(size = 1500000)
data['col2'] = np.random.normal(size = 1500000)

ddata = dd.from_pandas(data, npartitions=30)
def myfunc(x,y): return y*(x**2+1)
def apply_myfunc_to_DF(df): return df.apply((lambda row: myfunc(*row)), axis=1)
def pandas_apply(): return apply_myfunc_to_DF(data)
def dask_apply(): return ddata.map_partitions(apply_myfunc_to_DF).compute(get=get)  
def vectorized(): return myfunc(data['col1'], data['col2']  )

t_pds = timeit.Timer(lambda: pandas_apply())
print(t_pds.timeit(number=1))
  

28.16970546543598

t_dsk = timeit.Timer(lambda: dask_apply())
print(t_dsk.timeit(number=1))
  

2.708152851089835

t_vec = timeit.Timer(lambda: vectorized())
print(t_vec.timeit(number=1))
  

0.010668013244867325

从pandas获得 10倍加速因子适用于分区上的dask应用。当然,如果你有一个可以向量化的函数,你应该 - 在这种情况下函数(y*(x**2+1))被简单地向量化,但是有很多东西是不可能矢量化的。

答案 1 :(得分:29)

您可以使用swifter软件包:

pip install swifter

它作为熊猫的插件,允许您重用apply函数:

import swifter

def some_function(data):
    return data * 10

data['out'] = data['in'].swifter.apply(some_function)

无论是否进行矢量化(如上例所示),它都会自动找出最有效的方法来并行化功能。

More examplesperformance comparison在GitHub上可用。请注意,该软件包正在积极开发中,因此API可能会更改。

答案 2 :(得分:8)

您可以尝试使用pandarallel来代替:一种简单高效的工具来并行化所有CPU(在Linux和macOS上)的熊猫操作

  • 并行化具有成本(初始化新进程,通过共享内存发送数据等),因此,只有在要并行化的计算量足够高的情况下,并行化才有效。对于很少的数据,使用并行化并不总是值得的。
  • 应用的函数不应是lambda函数。
from pandarallel import pandarallel
from math import sin

pandarallel.initialize()

# FORBIDDEN
df.parallel_apply(lambda x: sin(x**2), axis=1)

# ALLOWED
def func(x):
    return sin(x**2)

df.parallel_apply(func, axis=1)

请参阅https://github.com/nalepae/pandarallel

答案 3 :(得分:3)

只想给Dask一个更新的答案

import dask.dataframe as dd

def your_func(row):
  #do something
  return row

ddf = dd.from_pandas(df, npartitions=30) # find your own number of partitions
ddf_update = ddf.apply(your_func, axis=1).compute()

在我的 100,000 条记录中,没有 Dask:

CPU 时间:用户 6 分 32 秒,系统:100 毫秒,总计:6 分 32 秒 挂墙时间:6 分 32 秒

使用 Dask:

CPU 时间:用户 5.19 秒,系统:784 毫秒,总计:5.98 秒 挂墙时间:1分3秒

答案 4 :(得分:2)

要使用所有(物理或逻辑)内核,可以尝试使用mapply作为swifterpandarallel的替代方案。

您可以在初始化时设置核心数量(以及分块行为):

import pandas as pd
import mapply

mapply.init(n_workers=-1)

...

df.mapply(myfunc, axis=1)

默认情况下(n_workers=-1),程序包使用系统上所有可用的物理CPU。如果您的系统使用超线程(通常会显示两倍的物理CPU),mapply将产生一个额外的工作程序,以将多处理池的优先级设置为高于系统上的其他进程。

根据您对all your cores的定义,也可以改用所有逻辑核心(请注意,像这样的CPU绑定进程将在争夺物理CPU,这可能会降低您的运行速度):

import multiprocessing
n_workers = multiprocessing.cpu_count()

# or more explicit
import psutil
n_workers = psutil.cpu_count(logical=True)

答案 5 :(得分:1)

这里是sklearn基础变压器的一个示例,其中将熊猫应用并行化

import multiprocessing as mp
from sklearn.base import TransformerMixin, BaseEstimator

class ParllelTransformer(BaseEstimator, TransformerMixin):
    def __init__(self,
                 n_jobs=1):
        """
        n_jobs - parallel jobs to run
        """
        self.variety = variety
        self.user_abbrevs = user_abbrevs
        self.n_jobs = n_jobs
    def fit(self, X, y=None):
        return self
    def transform(self, X, *_):
        X_copy = X.copy()
        cores = mp.cpu_count()
        partitions = 1

        if self.n_jobs <= -1:
            partitions = cores
        elif self.n_jobs <= 0:
            partitions = 1
        else:
            partitions = min(self.n_jobs, cores)

        if partitions == 1:
            # transform sequentially
            return X_copy.apply(self._transform_one)

        # splitting data into batches
        data_split = np.array_split(X_copy, partitions)

        pool = mp.Pool(cores)

        # Here reduce function - concationation of transformed batches
        data = pd.concat(
            pool.map(self._preprocess_part, data_split)
        )

        pool.close()
        pool.join()
        return data
    def _transform_part(self, df_part):
        return df_part.apply(self._transform_one)
    def _transform_one(self, line):
        # some kind of transformations here
        return line

有关更多信息,请参见https://towardsdatascience.com/4-easy-steps-to-improve-your-machine-learning-code-performance-88a0b0eeffa8

答案 6 :(得分:1)

如果要保留在本机python中:

import multiprocessing as mp

pool = mp.Pool(mp.cpu_count())
df['newcol'] = pool.map(f, df['col'])
pool.terminate()
pool.join()

将以并行方式将功能f应用于数据帧col的列df

答案 7 :(得分:0)

由于问题是“ 如何使用所有内核并行运行数据帧上的应用程序?”,因此答案也可以是modin。您可以并行运行所有内核,尽管实时性更差。

请参见https://github.com/modin-project/modin。它在daskray的顶部运行。他们说:“ Modin是为1MB至1TB +的数据集设计的DataFrame。”我尝试过:pip3 install "modin"[ray]"。 Modin与Pandas的对比是-六个核心需要12秒,而核心需要6秒。

答案 8 :(得分:0)

这里还有一个使用 Joblib 和一些来自 scikit-learn 的帮助代码。轻量级(如果你已经有了 scikit-learn),如果你更喜欢更好地控制它正在做什么,那么很好,因为 joblib 很容易被破解。

from joblib import parallel_backend, Parallel, delayed, effective_n_jobs
from sklearn.utils import gen_even_slices
from sklearn.utils.validation import _num_samples


def parallel_apply(df, func, n_jobs= -1, **kwargs):
    """ Pandas apply in parallel using joblib. 
    Uses sklearn.utils to partition input evenly.
    
    Args:
        df: Pandas DataFrame, Series, or any other object that supports slicing and apply.
        func: Callable to apply
        n_jobs: Desired number of workers. Default value -1 means use all available cores.
        **kwargs: Any additional parameters will be supplied to the apply function
        
    Returns:
        Same as for normal Pandas DataFrame.apply()
        
    """
    
    if effective_n_jobs(n_jobs) == 1:
        return df.apply(func, **kwargs)
    else:
        ret = Parallel(n_jobs=n_jobs)(
            delayed(type(df).apply)(df[s], func, **kwargs)
            for s in gen_even_slices(_num_samples(df), effective_n_jobs(n_jobs)))
        return pd.concat(ret)

用法:result = parallel_apply(my_dataframe, my_func)

答案 9 :(得分:0)

代替

df["new"] = df["old"].map(fun)

from joblib import Parallel, delayed
df["new"] = Parallel(n_jobs=-1, verbose=10)(delayed(fun)(i) for i in df["old"])

对我来说这比

略有改进
import multiprocessing as mp
with mp.Pool(mp.cpu_count()) as pool:
    df["new"] = pool.map(fun, df["old"])

如果作业非常小,您会收到进度指示和自动批处理。