Numpy.median.reduceat के लिए तेज़ विकल्प


12

इस उत्तर से संबंधित , क्या मध्यस्थों की गणना करने का एक तेज़ तरीका है, जिसमें असमान तत्वों की संख्या वाले समूह हैं?

उदाहरण के लिए:

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67, ... ]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3,    ... ]

और फिर मैं प्रति समूह और माध्यिका के बीच के अंतर की गणना करना चाहता हूं (जैसे समूह 0का माध्य 1.025इसलिए पहला परिणाम है 1.00 - 1.025 = -0.025) तो ऊपर दिए गए सरणी के लिए, परिणाम इस प्रकार दिखाई देंगे:

result = [-0.025, 0.025, 0.05, -0.05, -0.19, 0.29, 0.00, 0.10, -0.10, ...]

चूंकि np.median.reduceat(अभी तक) मौजूद नहीं है, क्या इसे प्राप्त करने का एक और तेज़ तरीका है? मेरी सरणी में लाखों पंक्तियाँ होंगी ताकि गति महत्वपूर्ण हो!

सूचक को सन्निहित और आदेशित माना जा सकता है (यदि वे नहीं हैं तो उन्हें बदलना आसान है)।


प्रदर्शन तुलना के लिए उदाहरण डेटा:

import numpy as np

np.random.seed(0)
rows = 10000
cols = 500
ngroup = 100

# Create random data and groups (unique per column)
data = np.random.rand(rows,cols)
groups = np.random.randint(ngroup, size=(rows,cols)) + 10*np.tile(np.arange(cols),(rows,1))

# Flatten
data = data.ravel()
groups = groups.ravel()

# Sort by group
idx_sort = groups.argsort()
data = data[idx_sort]
groups = groups[idx_sort]

क्या आपने scipy.ndimage.medianलिंक किए गए उत्तर में सुझाव दिया था? यह मुझे नहीं लगता है कि इसे प्रति लेबल समान संख्या में तत्वों की आवश्यकता है। या किसी को याद किया था?
आंद्रा डीक

तो, जब आपने लाखों पंक्ति कहा था, क्या आपका वास्तविक डेटासेट एक 2D सरणी है और आप उन पंक्तियों में से प्रत्येक पर यह ऑपरेशन कर रहे हैं?
दिवाकर

@Divakar डेटा के परीक्षण के लिए प्रश्न संपादित करें देखें
जीन-पॉल

आपने प्रारंभिक डेटा में पहले से ही बेंचमार्क दिया था, मैंने प्रारूप को समान रखने के लिए इसे फुलाया। मेरे फुले हुए डेटासेट के खिलाफ सब कुछ निर्धारित है। अब इसे बदलना उचित नहीं है
roganjosh

जवाबों:


7

कभी-कभी आपको गैर-मुहावरेदार संख्याओं को लिखने की आवश्यकता होती है, यदि आप वास्तव में अपनी गणना को गति देना चाहते हैं जो आप देशी अंकों के साथ नहीं कर सकते हैं।

numbaअपने अजगर कोड को निम्न-स्तरीय सी के लिए संकलित करता है क्योंकि बहुत सारे खाँसी आमतौर पर सी के रूप में तेजी से होती है, यह ज्यादातर तब उपयोगी होता है जब आपकी समस्या स्वयं खतना के साथ देशी वैश्वीकरण को उधार नहीं देती है। यह एक उदाहरण है (जहां मैंने माना कि सूचकांक सन्निहित और क्रमबद्ध हैं, जो उदाहरण डेटा में भी परिलक्षित होता है):

import numpy as np
import numba

# use the inflated example of roganjosh https://stackoverflow.com/a/58788534
data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3] 

data = np.array(data * 500) # using arrays is important for numba!
index = np.sort(np.random.randint(0, 30, 4500))               

# jit-decorate; original is available as .py_func attribute
@numba.njit('f8[:](f8[:], i8[:])') # explicit signature implies ahead-of-time compile
def diffmedian_jit(data, index): 
    res = np.empty_like(data) 
    i_start = 0 
    for i in range(1, index.size): 
        if index[i] == index[i_start]: 
            continue 

        # here: i is the first _next_ index 
        inds = slice(i_start, i)  # i_start:i slice 
        res[inds] = data[inds] - np.median(data[inds]) 

        i_start = i 

    # also fix last label 
    res[i_start:] = data[i_start:] - np.median(data[i_start:])

    return res

और यहाँ कुछ समय IPython के %timeitजादू का उपयोग कर रहे हैं :

>>> %timeit diffmedian_jit.py_func(data, index)  # non-jitted function
... %timeit diffmedian_jit(data, index)  # jitted function
...
4.27 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
65.2 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

इन नंबरों में अपडेटेड उदाहरण डेटा का उपयोग करना (जैसे कि पायथन फ़ंक्शन का रनटाइम बनाम JIT- त्वरित फंक्शनलियो का रनटाइम) हैं

>>> %timeit diffmedian_jit.py_func(data, groups) 
... %timeit diffmedian_jit(data, groups)
2.45 s ± 34.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93.6 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

यह छोटे केस में 65x स्पीडअप और बड़े केस में 26x स्पीडअप (तुलना में धीमी गति कोड की तुलना में) त्वरित कोड का उपयोग करता है। एक और उल्टा यह है कि (सामान्य संख्या के साथ विशिष्ट वैश्वीकरण के विपरीत) हमें इस गति को प्राप्त करने के लिए अतिरिक्त मेमोरी की आवश्यकता नहीं थी, यह सभी अनुकूलित और संकलित निम्न-स्तरीय कोड के बारे में है जो कि समाप्त हो रहा है।


उपरोक्त फ़ंक्शन मानता है कि सुन्न int arrays हैं int64 डिफ़ॉल्ट रूप से , जो वास्तव में विंडोज पर मामला नहीं है। तो एक विकल्प कॉल से हस्ताक्षर को हटाने के लिए हैnumba.njit , उचित जस्ट-इन-टाइम संकलन को ट्रिगर किया जाए। लेकिन इसका मतलब यह है कि फ़ंक्शन को पहले निष्पादन के दौरान संकलित किया जाएगा, जो समय परिणामों के साथ मध्यस्थता कर सकता है (हम या तो मैन्युअल रूप से फ़ंक्शन को निष्पादित कर सकते हैं, प्रतिनिधि डेटा प्रकारों का उपयोग कर सकते हैं, या बस स्वीकार कर सकते हैं कि पहली बार निष्पादन बहुत धीमा होगा, जिसे करना चाहिए अनदेखा किया जाए)। यह वही है जो मैंने एक हस्ताक्षर को निर्दिष्ट करके रोकने की कोशिश की थी, जो समय-समय पर संकलन से आगे बढ़ता है।

वैसे भी, ठीक से JIT मामले में डेकोरेटर की हमें जरूरत है

@numba.njit
def diffmedian_jit(...):

ध्यान दें कि उपर्युक्त समय मैंने जो जीट-संकलित फ़ंक्शन के लिए दिखाया था, केवल एक बार फ़ंक्शन संकलित होने के बाद लागू होता है। यह या तो परिभाषा पर होता है (उत्सुक संकलन के साथ, जब एक स्पष्ट हस्ताक्षर पारित हो जाता है numba.njit), या पहले फ़ंक्शन कॉल के दौरान (आलसी संकलन के साथ, जब कोई हस्ताक्षर पारित नहीं होता हैnumba.njit )। यदि फ़ंक्शन केवल एक बार निष्पादित होने जा रहा है, तो इस विधि की गति के लिए संकलन समय पर भी विचार किया जाना चाहिए। यह आम तौर पर केवल संकलित कार्यों के लायक है यदि संकलन + निष्पादन का कुल समय अप्रयुक्त रनटाइम से कम है (जो वास्तव में उपरोक्त मामले में सच है, जहां देशी पायथन फ़ंक्शन बहुत धीमा है)। यह ज्यादातर तब होता है जब आप अपने संकलित फ़ंक्शन को बहुत बार कॉल कर रहे होते हैं।

के रूप में max9111 एक टिप्पणी में बताया गया है, में से एक महत्वपूर्ण विशेषता numbaहै cacheकीवर्ड के लिए jit। पासिंग cache=Trueके लिए numba.jit, डिस्क के लिए संकलित समारोह का संग्रह किया जायेगा ताकि दिया अजगर मॉड्यूल के अगले निष्पादन के दौरान समारोह से वहाँ के बजाय कंपाइल है, जो फिर से छोड़ कर सकते हैं लंबे समय के रनटाइम लोड किया जाएगा।


@Divakar वास्तव में, यह मानता है कि सूचकांक सन्निहित और क्रमबद्ध हैं, जो ओपी के डेटा में एक धारणा की तरह लग रहा था, और स्वचालित रूप से रंजोशोश के indexडेटा में भी शामिल है । मैं इस बारे में एक टिप्पणी छोड़ दूंगा, धन्यवाद :)
एंड्रास डीक

ठीक है, सन्निहित स्वचालित रूप से शामिल नहीं है ... लेकिन मुझे पूरा यकीन है कि इसे वैसे भी सन्निहित होना चाहिए। हम्म ...
डीक

1
@AndrasDeak यह वास्तव में ठीक है कि लेबल सन्निहित हैं और क्रमबद्ध हैं (उन्हें ठीक करना अगर किसी भी तरह से आसान नहीं है)
जीन-पॉल

1
@AndrasDeak परीक्षण डेटा के लिए प्रश्न करने के लिए संपादन देखें (जैसे कि प्रश्नों में प्रदर्शन की तुलना सुसंगत हो)
जीन-पॉल

1
आप cache=Trueदुभाषिया के प्रत्येक पुनरारंभ पर पुन: प्राप्ति से बचने के लिए कीवर्ड का उल्लेख कर सकते हैं ।
अधिकतम 9111

5

Pandasशुद्ध रूप से उपयोग करने के लिए यहां एक दृष्टिकोण का उपयोग किया जाएगा groupby। मैंने टाइमिंग की बेहतर समझ देने के लिए इनपुट साइज़ को थोड़ा बढ़ाया है (क्योंकि DF बनाने में ओवरहेड है)।

import numpy as np
import pandas as pd

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3]

data = data * 500
index = np.sort(np.random.randint(0, 30, 4500))

def df_approach(data, index):
    df = pd.DataFrame({'data': data, 'label': index})
    df['median'] = df.groupby('label')['data'].transform('median')
    df['result'] = df['data'] - df['median']

निम्नलिखित देता है timeit:

%timeit df_approach(data, index)
5.38 ms ± 50.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

समान नमूने के आकार के लिए, मुझे आर्यसेर का तानाशाही दृष्टिकोण प्राप्त करना चाहिए:

%timeit dict_approach(data, index)
8.12 ms ± 3.47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

हालांकि, अगर हम 10 के अन्य कारक द्वारा इनपुट बढ़ाते हैं, तो समय बन जाता है:

%timeit df_approach(data, index)
7.72 ms ± 85 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit dict_approach(data, index)
30.2 ms ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

हालांकि, कुछ पुनरावृत्ति की कीमत पर, दिवाकर द्वारा शुद्ध खस का उपयोग करने का उत्तर आता है:

%timeit bin_median_subtract(data, index)
573 µs ± 7.48 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

नए डेटासेट के प्रकाश में (जो वास्तव में शुरुआत में सेट होना चाहिए):

%timeit df_approach(data, groups)
472 ms ± 2.52 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit bin_median_subtract(data, groups) #https://stackoverflow.com/a/58788623/4799172
3.02 s ± 31.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dict_approach(data, groups) #https://stackoverflow.com/a/58788199/4799172
<I gave up after 1 minute>

# jitted (using @numba.njit('f8[:](f8[:], i4[:]') on Windows) from  https://stackoverflow.com/a/58788635/4799172
%timeit diffmedian_jit(data, groups)
132 ms ± 3.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

इस उत्तर के लिए धन्यवाद! अन्य उत्तरों के साथ संगति के लिए, क्या आप मेरे प्रश्न को संपादित करने में प्रदान किए गए उदाहरण डेटा पर अपने समाधानों का परीक्षण कर सकते हैं?
जीन-पॉल

@ जीन पॉल समय पहले से ही संगत कर रहे हैं, नहीं? उन्होंने मेरे शुरुआती बेंचमार्क डेटा का उपयोग किया, और उन मामलों में जो उन्होंने नहीं किया, मैंने उनके लिए समान बेंचमार्क के साथ समय प्रदान किया है
roganjosh

मैंने अनदेखा किया कि आपने दिवाकर के जवाब का एक संदर्भ पहले ही जोड़ दिया है इसलिए आपका उत्तर वास्तव में पहले से ही विभिन्न दृष्टिकोणों के बीच एक अच्छी तुलना करता है, इसके लिए धन्यवाद!
जीन पॉल

1
@ जीन-पॉल मैंने सबसे निचले स्तर पर नवीनतम समय को जोड़ा क्योंकि यह वास्तव में चीजों को काफी बदल देता है
रंजोशोश

1
प्रश्न पोस्ट करते समय परीक्षण सेट को नहीं जोड़ने के लिए माफी, अत्यधिक सराहना की गई कि आपने अभी भी परीक्षा परिणाम को जोड़ा! धन्यवाद!!!
जीन-पॉल

4

हो सकता है कि आपने पहले से ही ऐसा किया हो, लेकिन यदि नहीं, तो देखें कि क्या यह काफी तेज है:

median_dict = {i: np.median(data[index == i]) for i in np.unique(index)}
def myFunc(my_dict, a): 
    return my_dict[a]
vect_func = np.vectorize(myFunc)
median_diff = data - vect_func(median_dict, index)
median_diff

आउटपुट:

array([-0.025,  0.025,  0.05 , -0.05 , -0.19 ,  0.29 ,  0.   ,  0.1  ,
   -0.1  ])

स्पष्ट करते हुए कहा के जोखिम में np.vectorizeएक है बहुत एक पाश के लिए पतली आवरण है, इसलिए मैं यह उम्मीद नहीं है कि इस दृष्टिकोण विशेष रूप से तेजी से किया जाना है।
डीक

1
@AndrasDeak मैं असहमत नहीं हूं :) मैं आगे भी बना रहूंगा, और अगर कोई बेहतर समाधान पोस्ट करेगा, तो मैं इसे हटा दूंगा।
आर्येज़र

1
मुझे नहीं लगता कि अगर तेजी से पॉप अप होता है तो भी आपको इसे हटाना पड़ेगा :)
एंड्रास डीक

@roganjosh शायद ऐसा इसलिए है क्योंकि आपने परिभाषित नहीं किया है dataऔर indexजैसा np.arrayकि प्रश्न में है।
आर्यसेर

1
@ जीन-पॉल रोजगोश ने खान और उनके तरीकों के बीच एक समय की तुलना की, और यहाँ दूसरों ने उनकी तुलना की। यह कंप्यूटर हार्डवेयर पर निर्भर करता है, इसलिए हर किसी के अपने तरीकों की जाँच करने का कोई मतलब नहीं है, लेकिन ऐसा प्रतीत होता है कि मैं यहाँ धीमे समाधान के साथ आया हूं।
आर्यसेर

4

सकारात्मक बिन्स / इंडेक्स मानों के लिए बीनड-मेडियन प्राप्त करने के लिए न्यूमपी आधारित दृष्टिकोण यहां दिया गया है -

def bin_median(a, i):
    sidx = np.lexsort((a,i))

    a = a[sidx]
    i = i[sidx]

    c = np.bincount(i)
    c = c[c!=0]

    s1 = c//2

    e = c.cumsum()
    s1[1:] += e[:-1]

    firstval = a[s1-1]
    secondval = a[s1]
    out = np.where(c%2,secondval,(firstval+secondval)/2.0)
    return out

घटाए गए हमारे विशिष्ट मामले को हल करने के लिए -

def bin_median_subtract(a, i):
    sidx = np.lexsort((a,i))

    c = np.bincount(i)

    valid_mask = c!=0
    c = c[valid_mask]    

    e = c.cumsum()
    s1 = c//2
    s1[1:] += e[:-1]
    ssidx = sidx.argsort()
    starts = c%2+s1-1
    ends = s1

    starts_orgindx = sidx[np.searchsorted(sidx,starts,sorter=ssidx)]
    ends_orgindx  = sidx[np.searchsorted(sidx,ends,sorter=ssidx)]
    val = (a[starts_orgindx] + a[ends_orgindx])/2.
    out = a-np.repeat(val,c)
    return out

बहुत अच्छा जवाब! क्या आपके पास गति में सुधार के रूप में कोई संकेत है जैसे df.groupby('index').transform('median')?
जीन-पॉल

@ जीन-पॉल क्या आप लाखों लोगों के वास्तविक डेटासेट का परीक्षण कर सकते हैं?
दिवाकर 12

डेटा के परीक्षण के लिए प्रश्न संपादित करें देखें
जीन-पॉल

@ जीन-पॉल ने सरल समाधान के लिए मेरे समाधान का संपादन किया। यदि आप हैं, तो परीक्षण के लिए इसका उपयोग करना सुनिश्चित करें।
दिवाकर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.