पंडों का प्रदर्शन मौजूदा स्तंभों से नया स्तंभ बनाने के लिए np.vectorize पर लागू होता है


81

मैं पंडों के डेटाफ्रेम का उपयोग कर रहा हूं और मौजूदा कॉलम के एक फ़ंक्शन के रूप में एक नया कॉलम बनाना चाहता हूं। मैंने गति के अंतर के बारे में अच्छी चर्चा नहीं देखी है df.apply()और np.vectorize()इसलिए मुझे लगा कि मैं यहां पूछूंगा।

पंडों का apply()कार्य धीमा है। जो मैंने मापा (कुछ प्रयोगों में नीचे दिखाया गया है np.vectorize()) से, डेटाफ़्रेम फ़ंक्शन का उपयोग करने की तुलना में 25x तेज़ (या अधिक) का उपयोग कर रहा है apply(), कम से कम मेरे 2016 मैकबुक प्रो पर। क्या यह अपेक्षित परिणाम है, और क्यों?

उदाहरण के लिए, मान लें कि मेरे पास Nपंक्तियों के साथ निम्नलिखित डेटाफ़्रेम हैं:

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

आगे मान लीजिए कि मैं दो कॉलमों के एक फ़ंक्शन के रूप में एक नया कॉलम बनाना चाहता हूं Aऔर B। नीचे दिए गए उदाहरण में, मैं एक साधारण फ़ंक्शन का उपयोग करूंगा divide()। फ़ंक्शन को लागू करने के लिए, मैं df.apply()या तो उपयोग कर सकता हूं या np.vectorize():

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

अगर मैं Nवास्तविक दुनिया के आकार में 1 मिलियन या उससे अधिक की वृद्धि करता हूं , तो मैं निरीक्षण करता हूं कि np.vectorize()25x तेज या उससे अधिक है df.apply()

नीचे कुछ पूर्ण बेंचमार्किंग कोड दिए गए हैं:

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

परिणाम नीचे दर्शाए गए है:

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

यदि np.vectorize()सामान्य रूप से हमेशा तेजी से होता है df.apply(), तो np.vectorize()अधिक उल्लेख क्यों नहीं किया जाता है ? मैं केवल कभी भी StackOverflow से संबंधित पोस्ट देखता हूं df.apply(), जैसे:

पांडा अन्य स्तंभों से मानों के आधार पर नए कॉलम बनाते हैं

मैं कई कॉलमों में पंडों के 'फंक्शन' फ़ंक्शन का उपयोग कैसे करूं?

पंडों डेटाफ्रेम के दो स्तंभों के लिए एक फ़ंक्शन कैसे लागू करें


मैंने आपके प्रश्न के विवरण में खुदाई नहीं की है, लेकिन np.vectorizeमूल रूप से एक अजगर forपाश है (यह एक सुविधा विधि है) और applyएक
लंबो के

"यदि np.vectorize () सामान्य रूप से हमेशा df.apply () से अधिक तेज़ होता है, तो np.vectorize () का अधिक उल्लेख क्यों नहीं किया जाता है?" applyजब तक आपको पंक्ति-दर-पंक्ति के आधार पर उपयोग नहीं करना चाहिए , जब तक कि आपके पास न हो, और जाहिर है कि एक वेक्टरकृत फ़ंक्शन एक गैर-वेक्टरीकृत प्रदर्शन करेगा।
21

1
@PMende लेकिन सदिश np.vectorizeनहीं है। यह एक प्रसिद्ध मिथ्या नाम है
roganjosh

1
@Pendende, ज़रूर, मैं अन्यथा नहीं था। आपको समय से कार्यान्वयन पर अपनी राय प्राप्त नहीं करनी चाहिए। हाँ, वे आनंदमय हैं। लेकिन वे आपको ऐसी चीजें बना सकते हैं जो सच नहीं हैं।
जेपी

3
@PMende के पास पांडा एक्सेसर्स के साथ एक नाटक है .str। वे बहुत सारे मामलों में सूची की समझ से धीमी हैं। हम बहुत ज्यादा मान लेते हैं।
roganjosh

जवाबों:


115

मैं यह कहकर शुरू करूंगा कि पंडों और न्यूमपी सरणियों की शक्ति संख्यात्मक सरणियों पर उच्च-प्रदर्शन वेक्टर की गई गणनाओं से ली गई है । 1 वेक्टर की गई गणनाओं का पूरा बिंदु अत्यधिक अनुकूलित सी कोड की गणना करके और सन्निहित मेमोरी ब्लॉकों का उपयोग करके पायथन-स्तर के छोरों से बचने के लिए है।

पायथन-स्तर के छोरों

अब हम कुछ समय देख सकते हैं। नीचे सभी पायथन-स्तरीय लूप हैं जो या तो उत्पादन करते हैं pd.Series, np.ndarrayया listसमान मान वाले ऑब्जेक्ट। किसी डेटाफ़्रेम के भीतर एक श्रृंखला को असाइन करने के प्रयोजनों के लिए, परिणाम तुलनीय हैं।

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

कुछ takeaways:

  1. tupleआधारित विधियों (पहले 4) की तुलना में एक कारक अधिक कुशल हैं pd.Seriesआधारित विधियों (पिछले 3)।
  2. np.vectorize, सूची समझ + zipऔर mapविधियाँ, अर्थात शीर्ष 3, सभी में लगभग समान प्रदर्शन है। इसका कारण यह है कि वे उपयोग करते हैं tuple और कुछ पंडों को उपर से बाईपास करते हैं pd.DataFrame.itertuples
  3. बनाम बिना उपयोग raw=Trueकरने से एक महत्वपूर्ण गति सुधार है pd.DataFrame.apply। यह विकल्प pd.Seriesऑब्जेक्ट के बजाय कस्टम फ़ंक्शन को NumPy सरणियों को खिलाता है ।

pd.DataFrame.apply: बस एक और लूप

देखने के लिए वास्तव में पांडा के आसपास गुजरता वस्तुओं, आप अपने समारोह तुच्छता में संशोधन कर सकते हैं:

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

आउटपुट: <class 'pandas.core.series.Series'>। पंडों सीरीज़ ऑब्जेक्ट को बनाना, पास करना और क्वेरी करना, NumPy सरणियों के सापेक्ष महत्वपूर्ण ओवरहेड्स को वहन करता है। यह आश्चर्यचकित नहीं होना चाहिए: पंडों की श्रृंखला में एक सूचकांक, मूल्यों, विशेषताओं आदि को रखने के लिए मचान की एक सभ्य राशि शामिल है।

उसी व्यायाम को फिर से करें raw=Trueऔर आप देखेंगे <class 'numpy.ndarray'>। यह सब डॉक्स में वर्णित है, लेकिन इसे देखकर और अधिक आश्वस्त है।

np.vectorize: नकली वैश्यावृत्ति

np.vectorizeनिम्नलिखित नोट में डॉक्स हैं :

pyfuncसदिश फ़ंक्शन, इनपुट सरणियों के क्रमिक ट्यूपल्स पर मूल्यांकन करता है जैसे कि पायथन मैप फ़ंक्शन, इसके अलावा यह खस्ता के प्रसारण नियमों का उपयोग करता है।

"प्रसारण नियम" यहां अप्रासंगिक हैं, क्योंकि इनपुट सरणियों के आयाम समान हैं। समांतर उपदेशात्मक mapहै, क्योंकि mapउपरोक्त संस्करण में लगभग समान प्रदर्शन है। स्रोत कोड से पता चलता है क्या हो रहा है: np.vectorizeएक में अपने इनपुट समारोह धर्मान्तरित यूनिवर्सल समारोह के माध्यम से ( "ufunc") np.frompyfunc। कुछ अनुकूलन है, उदाहरण के लिए कैशिंग, जिससे कुछ प्रदर्शन में सुधार हो सकता है।

संक्षेप में, np.vectorizeपायथन-स्तरीय लूप को क्या करना चाहिए , लेकिन pd.DataFrame.applyएक चंकी ओवरहेड जोड़ता है। कोई जेआईटी-संकलन नहीं है जिसे आप देखें numba(नीचे देखें)। यह सिर्फ एक सुविधा है

सच्चा वैश्वीकरण: आपको क्या उपयोग करना चाहिए

कहीं भी उपरोक्त अंतर क्यों नहीं हैं? क्योंकि वास्तव में सदिश गणनाओं का प्रदर्शन उन्हें अप्रासंगिक बनाता है:

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

हां, यह उपरोक्त लूप सॉल्यूशन के सबसे तेज की तुलना में ~ 40x तेज है। या तो ये स्वीकार्य हैं। मेरी राय में, पहला आत्मघाती, पठनीय और कुशल है। केवल अन्य तरीकों को देखें, जैसे numbaनीचे, यदि प्रदर्शन महत्वपूर्ण है और यह आपकी अड़चन का हिस्सा है।

numba.njit: अधिक से अधिक कुशलता

जब छोरों को व्यवहार्य माना जाता है तो आमतौर पर numbaसी के लिए जितना संभव हो उतना स्थानांतरित करने के लिए अंतर्निहित NumPy सरणियों के माध्यम से अनुकूलित किया जाता है।

दरअसल, माइक्रोसेकंड केnumba लिए प्रदर्शन में सुधार । कुछ बोझिल काम के बिना, इससे अधिक कुशल प्राप्त करना मुश्किल होगा।

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

उपयोग करने से @njit(parallel=True)बड़ी सरणियों के लिए और अधिक बढ़ावा मिल सकता है।


1 संख्यात्मक प्रकार में शामिल हैं: int, float, datetime, bool, category। वे dtype को बाहर कर देते हैंobject और उन्हें सन्निहित मेमोरी ब्लॉकों में रखा जा सकता है।

2 ऐसे कम से कम 2 कारण हैं जिनके कारण NumPy ऑपरेशंस कुशल हैं बनाम पायथन:

  • अजगर में सब कुछ एक वस्तु है। इसमें C, संख्याओं के विपरीत शामिल हैं। अजगर प्रकार इसलिए एक उपरि है जो देशी सी प्रकार के साथ मौजूद नहीं है।
  • NumPy विधियां आमतौर पर C- आधारित होती हैं। इसके अलावा, अनुकूलित एल्गोरिदम का उपयोग किया जाता है जहां संभव हो।

1
@ जेपीपी: parallelतर्क के साथ डेकोरेटर का उपयोग करने से @njit(parallel=True)मुझे और अधिक सुधार होता है @njit। शायद आप उसे भी जोड़ सकते हैं।
शेल्डोर

1
आपके पास b [i] के लिए दोहरी जांच है! = 0. सामान्य पायथन और Numba व्यवहार को 0 की जांच करना और एक त्रुटि फेंकना है। यह संभावना किसी भी SIMD वेक्टरकरण को तोड़ती है और आमतौर पर निष्पादन की गति पर एक उच्च प्रभाव होता है। लेकिन आप यह बदल सकते हैं कि Numba से @njit (error_model = 'numpy') के भीतर एक डिवीजन के लिए इस दोहरी जाँच से बचने के लिए 0. यह np.empty के साथ मेमोरी आवंटित करने और परिणाम को 0 अन्य कथन में सेट करने के लिए भी अनुशंसित है।
13:

1
error_model numpy का उपयोग करता है जो प्रोसेसर 0 में विभाजन द्वारा देता है -> NaN। कम से कम Numba 0.41dev में दोनों संस्करण SIMD-वेक्टराइजेशन का उपयोग करते हैं। आप यहां बताए अनुसार इस जांच कर सकते हैं numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3। क्यों मेरी पाश vectorized नहीं है?) मैं बस (अपने कार्य करने के लिए एक और को बयान जोड़ना होगा रेस [ i] = 0।) और np.empty के साथ मेमोरी आवंटित करें। इसे एरर_मॉडल के साथ जोड़ा जाना चाहिए। पुराने नुम्बा संस्करणों पर प्रदर्शन पर अधिक प्रभाव था ...
मैक्स 9111

2
@ stackoverflowuser2010, "मनमाने कार्यों के लिए" कोई सार्वभौमिक जवाब नहीं है। आपको सही काम के लिए सही उपकरण चुनना होगा, जो प्रोग्रामिंग / एल्गोरिदम को समझने का हिस्सा है।
जेपी

1
छुट्टियां आनंददायक हों!
1995 पर cs95

5

आपके कार्य जितने जटिल होंगे (यानी, कम numpyअपने स्वयं के इंटर्ल्स पर जा सकते हैं), जितना अधिक आप देखेंगे कि प्रदर्शन उतना अलग नहीं होगा। उदाहरण के लिए:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    elif name.lower().startswith('e'):
        return 'E'
    elif name.lower().startswith('i'):
        return 'I'
    elif name.lower().startswith('o'):
        return 'O'
    elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

कुछ समय कर रहे हैं:

अप्लाई का उपयोग करना

%timeit name_series.apply(parse_name)

परिणाम:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

का उपयोग करते हुए np.vectorize

%timeit parse_name_vec(name_series)

परिणाम:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

ufuncजब आप कॉल करते हैं तो Numpy अजगर कार्यों को सुन्न वस्तुओं में बदलने की कोशिश करता है np.vectorize। यह कैसे होता है, मुझे वास्तव में नहीं पता है - आपको एटीएम के लिए तैयार होने के मुकाबले आपको अधिक संख्या में आंतरिक क्षेत्रों में खुदाई करनी होगी। यह कहा, यह इस स्ट्रिंग आधारित समारोह की तुलना में केवल संख्यात्मक कार्यों पर एक बेहतर काम करने लगता है।

1,000,000 तक के आकार को क्रैंक करना:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

परिणाम:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

परिणाम:

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

एक बेहतर ( वेक्टरकृत ) तरीका np.select:

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

समय:

%timeit np.select(cases, replacements, default=name_series)

परिणाम:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

क्या होगा यदि आप इसे size=1000000(1 मिलियन) तक क्रैंक करते हैं ?
stackoverflowuser2010

2
मुझे पूरा यकीन है कि यहां आपके दावे गलत हैं। मैं उस कथन को अभी के लिए कोड के साथ वापस नहीं कर सकता, उम्मीद है कि कोई और व्यक्ति
roganjosh

@ stackoverflowuser2010 मैंने इसे एक वास्तविक सदिश दृष्टिकोण के साथ अद्यतन किया है ।
PMende

0

मैं अजगर के लिए नया हूं। लेकिन नीचे दिए गए उदाहरण में 'लागू करें' 'वेक्टराइज़' की तुलना में तेज़ी से काम करता है, या क्या मुझे कुछ याद आ रहा है।

 import numpy as np
 import pandas as pd

 B = np.random.rand(1000,1000)
 fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
 print(fn(B))

 B = pd.DataFrame(np.random.rand(1000,1000))
 fn = lambda l: 1/(1-np.exp(-l))
 print(B.apply(fn))
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.