Зробити Pandas DataFrame apply () використовувати всі ядра?


105

Починаючи з серпня 2017 року, Pandas DataFame.apply () , на жаль, все ще обмежений роботою з одним ядром, що означає, що багатоядерна машина витратить більшу частину свого обчислювального часу під час роботи df.apply(myfunc, axis=1).

Як ви можете використовувати всі свої ядра для паралельного запуску застосування у фреймі даних?

Відповіді:


79

Ви можете використовувати swifterпакет:

pip install swifter

Він працює як плагін для панд, що дозволяє повторно використовувати applyфункцію:

import swifter

def some_function(data):
    return data * 10

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

Він автоматично визначить найефективніший спосіб розпаралелювати функцію, незалежно від того, векторизована вона (як у наведеному вище прикладі) чи ні.

Інші приклади та порівняння продуктивності доступні на GitHub. Зверніть увагу, що пакет знаходиться в стадії активної розробки, тому API може змінюватися.

Також зауважте, що це не буде працювати автоматично для стовпців рядків. При використанні рядків Swifter повернеться до "простих" панд apply, які не будуть паралельними. У цьому випадку навіть примушування до використання daskне призведе до покращення продуктивності, і вам було б краще просто розділити свій набір даних вручну та розпаралелювати використанняmultiprocessing .


1
Ми з чистої цікавості, чи є спосіб обмежити кількість ядер, які він використовує при паралельному застосуванні? У мене є загальний сервер, тому, якщо я захоплю всі 32 ядра, ніхто не буде задоволений.
Максим Хайтович

1
@MaximHaytovich Я не знаю. Swifter використовує dask у фоновому режимі, тому, можливо, він поважає ці налаштування: stackoverflow.com/a/40633117/435093 - інакше я рекомендую відкрити випуск на GitHub. Автор дуже чуйний.
slhck

@slhck дякую! Викопаю ще трохи. Здається, це все одно не працює на сервері Windows - просто зависає, нічого не роблячи щодо іграшкового завдання
Максим Хайтович

Ви можете , будь ласка , допоможіть мені відповісти на цей: - 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
Суміт Сідана

103

Найпростіший спосіб - використовувати map_partitions Dask . Вам потрібні такі імпорти (вам потрібно 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)  

(Я вважаю, що 30 - це відповідна кількість розділів, якщо у вас 16 ядер). Просто для повноти, я приурочив різницю на моїй машині (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 прискорення, переходячи від панд, застосовується до тиску, що застосовується до розділів. Звичайно, якщо у вас є функція, яку ви можете векторизувати, вам слід - у цьому випадку функція ( y*(x**2+1)) тривіально векторизується, але є багато речей, які неможливо векторизувати.


2
Чудово знати, дякую за публікацію. Чи можете ви пояснити, чому обрали 30 розділів? Чи змінюється продуктивність при зміні цього значення?
Andrew L

4
@AndrewL Я припускаю, що кожен розділ обслуговується окремим процесом, і з 16 ядрами я припускаю, що 16 або 32 процеси можуть працювати одночасно. Я спробував, і продуктивність, здається, покращує до 32 розділів, але подальше збільшення не робить корисного ефекту. Я припускаю, що з чотирьохядерною машиною ви хотіли б 8 розділів і т. Д. Зверніть увагу, що я помітив певне покращення між 16 і 32, тому я думаю, що ви дійсно хочете 2x $ NUM_PROCESSORS
Роко Міїч

9
Єдине, що цеThe get= keyword has been deprecated. Please use the scheduler= keyword instead with the name of the desired scheduler like 'threads' or 'processes'
речі,

6
Для dask v0.20.0 і далі використовуйте ddata.map_partitions (лямбда df: df.apply ((лямбда-рядок: myfunc (* рядок)), вісь = 1)). Обчислення (планувальник = 'процеси') або один із інші варіанти планувальника. Поточний код видає "TypeError: Ключове слово get = було видалено. Будь ласка, використовуйте замість цього ключове слово scheduler = з назвою потрібного планувальника, наприклад" потоки "або" процеси ""
mork,

1
Переконайтеся, що перед тим, як це зробити, фрейм даних не має повторюваних індексів під час видачі ValueError: cannot reindex from a duplicate axis. Щоб обійти це, вам слід видалити дубльовані індекси за допомогою df = df[~df.index.duplicated()]або скинути їх за допомогою df.reset_index(inplace=True).
Хабіб Карбасян

24

Ви можете спробувати pandarallelзамість цього: простий та ефективний інструмент для розпаралелювання ваших операцій панд на всіх процесорах (у 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: Не вдається замаринувати локальний об'єкт 'Prepa_worker. <locals> .closure. <locals> .wrapper'. Чи можете ви допомогти мені в цьому?
Alex Cam

@Alex Sry Я не розробник цього модуля. Як виглядають ваші коди? Ви можете спробувати оголосити свої "внутрішні функції" глобальними? (тільки вгадайте)
G_KOBELIEF

@AlexCam Ваша функція повинна бути визначена поза іншою функцією, щоб python міг її замаринувати для багатопроцесорної обробки
Кенан,

1
@G_KOBELIEF З Python> 3.6 ми можемо використовувати лямбда-функцію з pandaparallel
user110244

18

Якщо ви хочете залишитися в рідному python:

import multiprocessing as mp

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

застосовуватиме функцію fпаралельно до стовпця colфрейму данихdf


Слідуючи такому підходу, я отримав ValueError: Length of values does not match length of indexвід __setitem__в pandas/core/frame.py. Не впевнений, чи зробив я щось неправильно, чи присвоєння не df['newcol']є безпечним.
Тріскачка

2
Ви можете записати pool.map до посередницького списку temp_result, щоб дозволити перевірити, чи довжина збігається з df, а потім зробити df ['newcol'] = temp_result?
Олів'є Крушан,

ви маєте на увазі створення нової колонки? що б ти використав?
Олів’є Крушант,

так, присвоєння результату карти новому стовпцю кадру даних. Чи карта не повертає список результатів кожного шматка, надісланого функції f? То що трапляється, коли ви призначаєте це стовпцю 'newcol? Використання Pandas та Python 3
Міна

Це насправді працює дуже гладко! Ви спробували? Він створює список тієї ж довжини df, того самого замовлення, що і надісланий. Це буквально робить c2 = f (c1) паралельно. Не існує більш простого способу багатопроцесорності в python. З
точки зору

2

Ось приклад базового трансформатора 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як альтернативу swifterі pandarallel.

Ви можете встановити кількість ядер (і поведінку порцій) на init:

import pandas as pd
import mapply

mapply.init(n_workers=-1)

...

df.mapply(myfunc, axis=1)

За замовчуванням ( n_workers=-1), пакет використовує всі фізичні процесори, доступні в системі. Якщо у вашій системі використовується гіперпотік (зазвичай у два рази перевищує кількість фізичних процесорів), mapplyз’явиться один зайвий працівник , який надасть пріоритет багатопроцесорному пулу над іншими процесами в системі.

Залежно від вашого визначення all your coresви можете замість цього використовувати всі логічні ядра (будьте уважні, що подібні процеси, пов'язані з процесором, будуть боротися за фізичні процесори, що може уповільнити вашу роботу):

import multiprocessing
n_workers = multiprocessing.cpu_count()

# or more explicit
import psutil
n_workers = psutil.cpu_count(logical=True)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.