ทำให้ Pandas DataFrame ใช้ () ใช้คอร์ทั้งหมดหรือไม่


111

เมื่อวันที่สิงหาคม 2017, นุ่นDataFame.apply ()เป็นที่น่าเสียดายที่ยังมีข้อ จำกัด ในการทำงานร่วมกับแกนเดียวหมายความว่าเครื่องแบบ multi-core df.apply(myfunc, axis=1)จะเสียส่วนใหญ่ของการคำนวณเวลาเมื่อคุณเรียกใช้

คุณจะใช้คอร์ทั้งหมดของคุณเพื่อรันใช้กับดาต้าเฟรมแบบขนานได้อย่างไร

คำตอบ:


85

คุณสามารถใช้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จะไม่สร้างการปรับปรุงประสิทธิภาพการทำงานและคุณจะดีกว่าเพียงแค่การแยกชุดข้อมูลของคุณด้วยตนเองและparallelizing multiprocessingใช้


1
ความอยากรู้อยากเห็นของเรามีวิธี จำกัด จำนวนคอร์ที่ใช้เมื่อทำการขนานหรือไม่? ฉันมีเซิร์ฟเวอร์ที่ใช้ร่วมกันดังนั้นหากฉันคว้าทั้ง 32 คอร์ก็จะไม่มีใครพอใจ
Maksim Khaitovich

1
@MaximHaytovich ฉันไม่รู้ Swifter ใช้ dask ในพื้นหลังดังนั้นอาจจะเคารพการตั้งค่าเหล่านี้: stackoverflow.com/a/40633117/435093 - มิฉะนั้นฉันขอแนะนำให้เปิดปัญหาบน GitHub ผู้เขียนตอบสนองได้ดีมาก
slhck

@slhck ขอบคุณ! จะขุดมันอีกเล็กน้อย. ดูเหมือนว่าจะไม่ทำงานบนเซิร์ฟเวอร์ windows อยู่ดี - แค่แฮงค์ไม่ได้ทำอะไรกับงานของเล่น
Maksim Khaitovich

1
+1 สำหรับ Swifter ไม่เพียง แต่ขนานกันโดยใช้วิธีการที่ดีที่สุดเท่านั้น แต่ยังรวมแถบความคืบหน้าผ่าน tqdm
scribu

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

110

วิธีที่ง่ายที่สุดคือการใช้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 speedupจากแพนด้าใช้กับ dask ใช้กับพาร์ติชัน แน่นอนถ้าคุณมีฟังก์ชันที่คุณสามารถ vectorize ได้คุณควร - ในกรณีนี้ function ( y*(x**2+1)) เป็น vectorized เล็กน้อย แต่มีหลายสิ่งที่เป็นไปไม่ได้ที่จะ vectorize


2
ยินดีที่ได้ทราบขอบคุณสำหรับการโพสต์ คุณอธิบายได้ไหมว่าทำไมคุณถึงเลือกพาร์ติชั่น 30 ชิ้น ประสิทธิภาพจะเปลี่ยนไปไหมเมื่อเปลี่ยนค่านี้
Andrew L

4
@AndrewL ฉันคิดว่าแต่ละพาร์ติชันได้รับการบริการโดยกระบวนการแยกกันและด้วย 16 คอร์ฉันถือว่ากระบวนการ 16 หรือ 32 กระบวนการสามารถทำงานพร้อมกันได้ ฉันลองใช้แล้วและดูเหมือนว่าประสิทธิภาพจะดีขึ้นถึง 32 พาร์ติชั่น แต่การเพิ่มขึ้นต่อไปไม่มีผลประโยชน์ใด ๆ ฉันคิดว่าด้วยเครื่องควอดคอร์คุณต้องการ 8 พาร์ติชั่นเป็นต้นโปรดทราบว่าฉันสังเกตเห็นการปรับปรุงระหว่าง 16 ถึง 32 ดังนั้นฉันคิดว่าคุณต้องการ 2x $ NUM_PROCESSORS จริงๆ
Roko Mijic

11
สิ่งเดียวคือ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 (lambda df: df.apply ((lambda row: myfunc (* row)), axis = 1)) คำนวณ (ตัวกำหนดตารางเวลา = 'กระบวนการ') หรือหนึ่งใน ตัวเลือกตัวกำหนดตารางเวลาอื่น ๆ โค้ดปัจจุบันพ่น "TypeError: คีย์เวิร์ด get = ถูกลบออกแล้วโปรดใช้คีย์เวิร์ดตัวกำหนดตารางเวลา = แทนด้วยชื่อของตัวกำหนดตารางเวลาที่ต้องการเช่น 'เธรด' หรือ 'โปรเซส'
mork

1
ตรวจสอบให้แน่ใจว่าก่อนที่คุณจะดำเนินการนี้ดาต้าเฟรมไม่มีดัชนีซ้ำกันขณะที่มันพ่นValueError: cannot reindex from a duplicate axisออกมา จะไปรอบ ๆ ว่าทั้งคุณควรลบดัชนีซ้ำโดยหรือตั้งค่าดัชนีของคุณโดยdf = df[~df.index.duplicated()] df.reset_index(inplace=True)
Habib Karbasian

26

คุณสามารถลองpandarallelใช้แทน: เครื่องมือที่เรียบง่ายและมีประสิทธิภาพในการขนานการทำงานของแพนด้าบนซีพียูทั้งหมดของคุณ (บน Linux และ macOS)

  • Parallelization มีค่าใช้จ่าย (การสร้างกระบวนการใหม่การส่งข้อมูลผ่านหน่วยความจำที่ใช้ร่วมกัน ฯลฯ ... ) ดังนั้นการทำแบบขนานจะมีประสิทธิภาพก็ต่อเมื่อปริมาณการคำนวณเพื่อขนานนั้นสูงเพียงพอ สำหรับข้อมูลจำนวนน้อยมากการใช้ Parallezation ไม่คุ้มค่าเสมอไป
  • ฟังก์ชันที่ใช้ไม่ควรเป็นฟังก์ชันแลมบ์ดา
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: ไม่สามารถดองวัตถุในเครื่อง 'จัดเตรียม _worker. <locals> .closure. <locals> .wrapper' คุณช่วยฉันเรื่องนี้ได้ไหม
Alex Cam

@ Alex Sry ฉันไม่ใช่ผู้พัฒนาโมดูลนั้น รหัสของคุณมีลักษณะอย่างไร คุณสามารถลองประกาศ "ฟังก์ชันภายใน" ของคุณเป็นส่วนกลางได้ไหม (แค่เดา)
G_KOBELIEF

@AlexCam ฟังก์ชันของคุณควรกำหนดไว้ภายนอกฟังก์ชันอื่น ๆ เพื่อให้ python สามารถดองสำหรับการประมวลผลหลายขั้นตอนได้
Kenan

1
@G_KOBELIEF ด้วย Python> 3.6 เราสามารถใช้ฟังก์ชัน lambda กับ pandaparallel ได้
user110244

19

หากคุณต้องการอยู่ใน 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']ไม่ปลอดภัย
Rattle

2
คุณสามารถเขียน pool.map ลงในรายการ temp_result ตัวกลางเพื่ออนุญาตให้ตรวจสอบว่าความยาวตรงกับ df หรือไม่จากนั้นทำ df ['newcol'] = temp_result?
Olivier Cruchant

คุณหมายถึงการสร้างคอลัมน์ใหม่? คุณจะใช้อะไร
Olivier Cruchant

ใช่กำหนดผลลัพธ์ของแผนที่ให้กับคอลัมน์ใหม่ของดาต้าเฟรม แผนที่ไม่ส่งคืนรายการผลลัพธ์ของแต่ละกลุ่มที่ส่งไปยังฟังก์ชัน f หรือไม่? จะเกิดอะไรขึ้นเมื่อคุณกำหนดสิ่งนั้นให้กับคอลัมน์ 'newcol? การใช้ Pandas และ Python 3
Mina

ใช้งานได้จริงเนียนจริง! คุณลองหรือยัง? จะสร้างรายการที่มีความยาวเท่ากันของ df ลำดับเดียวกันกับสิ่งที่ส่งไป แท้จริงแล้ว c2 = f (c1) เป็นแบบคู่ขนาน ไม่มีวิธีที่ง่ายกว่าในการทำหลายกระบวนการใน python ดูเหมือนว่า Ray จะสามารถทำสิ่งดีๆได้เช่นกัน ( towardsdatascience.com/… ) แต่มันยังไม่เป็นผู้ใหญ่และการติดตั้งก็ไม่ราบรื่นเสมอไปจากประสบการณ์ของฉัน
Olivier Cruchant

2

นี่คือตัวอย่างของ sklearn base transformer ซึ่งแพนด้าใช้จะขนานกัน

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


2

ที่จะใช้ทั้งหมด (ทางร่างกายหรือตรรกะ) แกนคุณอาจจะลองmapplyเป็นทางเลือกให้กับและswifterpandarallel

คุณสามารถกำหนดจำนวนคอร์ (และพฤติกรรมการแยกชิ้นส่วน) เมื่อเริ่มต้น:

import pandas as pd
import mapply

mapply.init(n_workers=-1)

...

df.mapply(myfunc, axis=1)

ตามค่าเริ่มต้น ( n_workers=-1) แพ็กเกจจะใช้ซีพียูทางกายภาพทั้งหมดที่มีอยู่ในระบบ หากระบบของคุณใช้ไฮเปอร์เธรด (โดยปกติเป็นสองเท่าของจำนวนซีพียูทางกายภาพที่จะปรากฏขึ้น) mapplyจะสร้างผู้ปฏิบัติงานเพิ่มอีกหนึ่งคนเพื่อจัดลำดับความสำคัญของพูลการประมวลผลหลายขั้นตอนเหนือกระบวนการอื่น ๆ ในระบบ

ทั้งนี้ขึ้นอยู่กับคำจำกัดความของall your coresคุณคุณสามารถใช้คอร์ตรรกะทั้งหมดแทนได้ (โปรดระวังว่ากระบวนการที่เชื่อมโยงกับ CPU จะต่อสู้กับซีพียูทางกายภาพซึ่งอาจทำให้การทำงานของคุณช้าลง):

import multiprocessing
n_workers = multiprocessing.cpu_count()

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

0

ตั้งแต่คำถามคือ " วิธีการที่คุณสามารถใช้แกนของคุณทั้งหมดเรียกใช้บน dataframe ในขนาน? " modinคำตอบก็ยังสามารถอยู่กับ คุณสามารถรันคอร์ทั้งหมดแบบขนานได้แม้ว่าเวลาจริงจะแย่กว่าก็ตาม

ดูhttps://github.com/modin-project/modin มันวิ่งจากด้านบนของหรือdask rayพวกเขาพูดว่า "Modin คือ DataFrame ที่ออกแบบมาสำหรับชุดข้อมูลตั้งแต่ 1MB ถึง 1TB +" ฉันพยายาม: pip3 install "modin"[ray]". Modin vs pandas คือ - 12 วินาทีในหกคอร์เทียบกับ 6 วินาที

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.