Pandas DataFrame apply ()가 모든 코어를 사용하게 하시겠습니까?


105

2017 년 8 월 현재 Pandas DataFame.apply () 는 여전히 단일 코어 작업으로 제한되어 있습니다. 즉, 멀티 코어 머신은 df.apply(myfunc, axis=1).

모든 코어를 사용하여 데이터 프레임에서 병렬로 적용하는 방법은 무엇입니까?

답변:


79

다음 swifter패키지를 사용할 수 있습니다 .

pip install swifter

pandas 용 플러그인으로 작동하여 apply함수 를 재사용 할 수 있습니다 .

import swifter

def some_function(data):
    return data * 10

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

벡터화 여부 (위의 예에서와 같이) 여부에 관계없이 함수를 병렬화하는 가장 효율적인 방법을 자동으로 알아냅니다.

GitHub에서 더 많은 예제성능 비교 를 확인할 수 있습니다. 패키지는 현재 개발 중이므로 API가 변경 될 수 있습니다.

또한 이것은 문자열 열에 대해 자동으로 작동하지 않습니다 . 문자열을 사용할 때 Swifter는 apply병렬이 아닌 "단순한"Pandas로 대체됩니다 . 이 경우 강제로 사용하더라도 dask성능이 향상되지는 않으며 데이터 세트를 수동으로 분할하고 .NET을 사용하여 병렬화하는 것이multiprocessing 좋습니다.


1
우리의 순수한 호기심은 병렬 적용을 할 때 사용하는 코어 수를 제한하는 방법이 있습니까? 공유 서버가 있으므로 32 코어를 모두 잡으면 아무도 기뻐하지 않을 것입니다.
Maksim Khaitovich

1
@MaximHaytovich 모르겠어요. Swifter는 백그라운드에서 dask를 사용하므로 다음 설정을 존중할 수 있습니다. stackoverflow.com/a/40633117/435093 — 그렇지 않으면 GitHub에서 문제를 여는 것이 좋습니다. 저자는 매우 반응이 좋습니다.
slhck

@slhck 감사합니다! 좀 더 파헤칠 것입니다. 어쨌든 Windows 서버에서 작동하지 않는 것 같습니다-그냥 장난감 작업에 아무것도하지 않습니다
Maksim Khaitovich

날이 대답에 도움을 기쁘게 할 수 있습니다 : - stackoverflow.com/questions/53561794/...을
ak3191

2
문자열의 경우 다음 allow_dask_on_strings(enable=True)과 같이 추가 하십시오. df.swifter.allow_dask_on_strings(enable=True).apply(some_function) 출처 : github.com/jmcarpenter2/swifter/issues/45
Sumit Sidana

103

가장 간단한 방법은 Dask의 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

주기 (10)의 단축의 요인을 판다에서 진행 파티션에 적용 DASK에 적용됩니다. 물론, 벡터화 할 수있는 함수가 있다면,이 경우 함수 ( y*(x**2+1))는 사소하게 벡터화되지만 벡터화가 불가능한 것들이 많이 있습니다.


2
게시 해 주셔서 감사합니다. 30 개의 파티션을 선택한 이유를 설명해 주시겠습니까? 이 값을 변경하면 성능이 변경됩니까?
Andrew L

4
@AndrewL 각 파티션이 별도의 프로세스에 의해 서비스된다고 가정하고 16 코어를 사용하면 16 개 또는 32 개의 프로세스가 동시에 실행될 수 있다고 가정합니다. 시도해 보았고 성능이 최대 32 개의 파티션까지 향상되는 것 같지만 추가로 증가해도 유익한 효과는 없습니다. 쿼드 코어 머신을 사용하는 경우 8 개의 파티션 등이 필요하다고 가정합니다. 16과 32 사이에 약간의 개선이 있음을 확인 했으므로 2x $ NUM_PROCESSORS를 원한다고 생각합니다
Roko Mijic 2017-08-07

9
한가지입니다The get= keyword has been deprecated. Please use the scheduler= keyword instead with the name of the desired scheduler like 'threads' or 'processes'
wordsforthewise

6
dask v0.20.0 이상의 경우 ddata.map_partitions (lambda df : df.apply ((lambda row : myfunc (* row)), axis = 1)). compute (scheduler = 'processes') 또는 다른 스케줄러 옵션. 현재 코드에서 "TypeError : get = 키워드가 제거되었습니다. 대신 'threads'또는 'processes'와 같은 원하는 스케줄러의 이름과 함께 scheduler = 키워드를 사용하십시오."
mork

1
이 작업을 수행하기 전에 데이터 프레임이을 throw 할 때 중복 인덱스가 없는지 확인하십시오 ValueError: cannot reindex from a duplicate axis. 이 문제를 해결하려면으로 중복 된 색인을 제거 df = df[~df.index.duplicated()]하거나으로 색인을 재설정해야합니다 df.reset_index(inplace=True).
Habib Karbasian

24

pandarallel대신 시도해 볼 수 있습니다 . 모든 CPU에서 Pandas 작업을 병렬화하는 간단하고 효율적인 도구 (Linux 및 macOS에서)

  • 병렬화에는 비용이 발생하므로 (새 프로세스 인스턴스화, 공유 메모리를 통해 데이터 전송 등) 병렬화 할 계산량이 충분히 높은 경우에만 병렬화가 효율적입니다. 아주 적은 양의 데이터에 대해 항상 그만한 가치가있는 것은 아닙니다.
  • 적용된 함수는 람다 함수가 아니어야합니다.
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를


안녕하세요, pandarallel을 사용하여 한 가지 문제를 해결할 수 없습니다. 오류 : AttributeError : Ca n't pickle local object 'prepare_worker. <locals> .closure. <locals> .wrapper'가 있습니다. 이걸 도와 주 시겠어요?
Alex Cam

@Alex Sry 저는 해당 모듈의 개발자가 아닙니다. 코드는 어떻게 생겼습니까? "내부 함수"를 전역으로 선언 할 수 있습니까? (추측)
G_KOBELIEF

@AlexCam 당신의 함수는 파이썬이 멀티 프로세싱을 위해 피클 할 수 있도록 다른 함수 외부에서 정의되어야합니다
Kenan

1
@G_KOBELIEF Python> 3.6에서는 pandaparallel과 함께 람다 함수를 사용할 수 있습니다.
user110244

18

네이티브 파이썬에 머물고 싶다면 :

import multiprocessing as mp

with mp.Pool(mp.cpu_count()) as pool:
    df['newcol'] = pool.map(f, df['col'])

데이터 프레임의 f열에 병렬 방식으로 기능 을 적용합니다 .coldf


이 I 같은 접근 방식을 따라하는 것은있어 ValueError: Length of values does not match length of index에서 __setitem__에서 pandas/core/frame.py. 내가 뭔가 잘못했는지 또는 할당 df['newcol']이 스레드 안전 하지 않은지 확실 하지 않습니다.
Rattle

2
pool.map을 중간 temp_result 목록에 작성하여 길이가 df와 일치하는지 확인한 다음 df [ 'newcol'] = temp_result?
Olivier Cruchant

새 열을 만드는 건가요? 무엇을 사용 하시겠습니까?
Olivier Cruchant

예, 맵의 결과를 데이터 프레임의 새 열에 할당합니다. map은 함수 f에 전송 된 각 청크의 결과 목록을 반환하지 않습니까? 그래서 그것을 'newcol'열에 할당하면 어떻게 될까요? Pandas 및 Python 3 사용
Mina

실제로 정말 부드럽게 작동합니다! 해봤 어? 전송 된 것과 동일한 순서로 동일한 길이의 df 목록을 생성합니다. 말 그대로 병렬 방식으로 c2 = f (c1)을 수행합니다. 파이썬에서 다중 처리하는 더 간단한 방법은 없습니다. 성능면에서 Ray도 좋은 일을 할 수있는 것 같지만 ( againstdatascience.com/… ) 아직 성숙하지 않았고 내 경험상 설치가 항상 원활하게 진행되는 것은 아닙니다
Olivier Cruchant

2

다음은 pandas 적용이 병렬화 된 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을 참조 하십시오.


0

모든 (물리적 또는 논리적) 코어를 사용하려면 및 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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.