ทางเลือกที่รวดเร็วสำหรับ 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ข้อเสนอแนะในคำตอบที่เชื่อมโยง? สำหรับฉันมันไม่ได้ดูเหมือนว่าจะต้องมีองค์ประกอบจำนวนเท่ากันต่อฉลาก หรือฉันคิดถึงอะไรบางอย่าง?
Andras Deak

ดังนั้นเมื่อคุณพูดหลายล้านแถวชุดข้อมูลจริงของคุณคืออาร์เรย์ 2 มิติและคุณกำลังดำเนินการนี้กับแต่ละแถวเหล่านั้นหรือไม่
Divakar

@Divakar ดูการแก้ไขคำถามเพื่อทดสอบข้อมูล
Jean-Paul

คุณให้เกณฑ์มาตรฐานในข้อมูลเริ่มต้นแล้วฉันขยายมันเพื่อให้รูปแบบเหมือนเดิม ทุกอย่างถูกเปรียบเทียบกับชุดข้อมูลที่สูงเกินจริงของฉัน มันไม่สมเหตุสมผลที่จะเปลี่ยนตอนนี้
roganjosh

คำตอบ:


7

บางครั้งคุณต้องเขียนโค้ด numpy ไม่ใช่สำนวนถ้าคุณจริงๆต้องการที่จะเพิ่มความเร็วในการคำนวณของคุณที่คุณไม่สามารถทำอะไรกับ numpy พื้นเมือง

numbaรวบรวมไพ ธ อนโค้ดของคุณไปยัง C ระดับต่ำเนื่องจากจำนวนของตัวเองจำนวนมากมักจะเร็วเท่ากับ C ซึ่งส่วนใหญ่แล้วจะมีประโยชน์หากปัญหาของคุณไม่ได้ทำให้ตัวเองกลายเป็น vectorization ดั้งเดิมที่มีจำนวนมาก นี่คือตัวอย่างหนึ่ง (โดยที่ฉันสันนิษฐานว่าดัชนีนั้นต่อเนื่องกันและเรียงลำดับซึ่งสะท้อนออกมาในข้อมูลตัวอย่างด้วย):

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

และนี่คือเวลาที่ใช้%timeitเวทมนต์ของ IPython :

>>> %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)

การใช้ข้อมูลตัวอย่างที่อัปเดตในคำถามตัวเลขเหล่านี้ (เช่นรันไทม์ของฟังก์ชัน python เทียบกับรันไทม์ของฟังก์ชัน 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 speedup ในเคสขนาดเล็กและ 26x speedup ในเคสที่ใหญ่กว่า (เมื่อเทียบกับโค้ด loopy แบบช้า) โดยใช้โค้ดเร่งความเร็ว ข้อดีอีกอย่างคือ (ไม่เหมือนกับ vectorization ทั่วไปกับ native numpy) เราไม่ต้องการหน่วยความจำเพิ่มเติมเพื่อให้ได้ความเร็วนี้มันคือทั้งหมดที่เกี่ยวกับการปรับปรุงและคอมไพล์โค้ดระดับต่ำที่จบลงด้วยการทำงาน


ฟังก์ชั่นดังกล่าวข้างต้นถือว่าอาร์เรย์ numpy int เป็นint64ค่าเริ่มต้นซึ่งไม่ได้เป็นกรณีใน Windows ทางเลือกอื่นคือการลบลายเซ็นออกจากการเรียกไปยังnumba.njitทำให้การคอมไพล์แบบทันเวลาพอดี แต่นี่หมายความว่าฟังก์ชั่นจะถูกรวบรวมระหว่างการประมวลผลครั้งแรกซึ่งสามารถเข้าไปยุ่งกับผลลัพธ์เวลา (เราสามารถดำเนินการฟังก์ชั่นครั้งเดียวด้วยตนเองโดยใช้ประเภทข้อมูลตัวแทนหรือเพียงยอมรับว่าการดำเนินการเวลาแรกจะช้ากว่ามาก ถูกละเว้น) นี่คือสิ่งที่ฉันพยายามป้องกันโดยการระบุลายเซ็นซึ่งเป็นต้นเหตุของการรวบรวมล่วงหน้า

อย่างไรก็ตามในกรณี JIT อย่างถูกต้องมัณฑนากรที่เราต้องการนั้นเป็นเพียง

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

โปรดทราบว่าการกำหนดเวลาด้านบนที่ฉันแสดงสำหรับฟังก์ชั่นการคอมไพล์ jit จะใช้งานได้เมื่อฟังก์ชั่นได้รับการรวบรวม สิ่งนี้อาจเกิดขึ้นตามคำจำกัดความ (ด้วยการรวบรวมความกระตือรือร้นเมื่อมีการส่งผ่านลายเซ็นอย่างชัดเจนไปยังnumba.njit) หรือระหว่างการเรียกใช้ฟังก์ชันแรก (ด้วยการรวบรวมขี้เกียจเมื่อไม่มีการส่งผ่านลายเซ็นไปยังnumba.njit) หากฟังก์ชั่นจะถูกดำเนินการเพียงครั้งเดียวแล้วเวลาในการรวบรวมควรพิจารณาด้วยความเร็วของวิธีนี้ โดยทั่วไปแล้วมันเป็นเพียงฟังก์ชั่นการรวบรวมที่คุ้มค่าถ้าเวลารวมของการคอมไพล์ + การประมวลผลน้อยกว่ารันไทม์ที่ไม่ได้คอมไพล์ (ซึ่งจริงในกรณีข้างต้นซึ่งฟังก์ชันไพ ธ อนช้ามาก) สิ่งนี้ส่วนใหญ่เกิดขึ้นเมื่อคุณเรียกใช้ฟังก์ชันที่คอมไพล์แล้วหลายครั้ง

ในฐานะที่เป็นmax9111ตั้งข้อสังเกตในความคิดเห็นหนึ่งคุณลักษณะที่สำคัญของnumbaเป็นcacheคำหลักjitที่จะ การส่งผ่านcache=Trueไปยังnumba.jitจะเก็บฟังก์ชั่นที่คอมไพล์ไปยังดิสก์ดังนั้นในระหว่างการดำเนินการครั้งต่อไปของโมดูลหลามที่กำหนดฟังก์ชั่นจะถูกโหลดจากที่นั่นแทนที่จะทำการคอมไพล์ซ้ำอีกครั้ง


@Divakar จริง ๆ แล้วมันถือว่าดัชนีนั้นต่อเนื่องและเรียงลำดับซึ่งดูเหมือนว่าเป็นข้อสันนิษฐานในข้อมูลของ OP และยังรวมอยู่ในindexข้อมูลของ roganjosh โดยอัตโนมัติ ฉันจะทิ้งข้อความนี้ไว้ขอบคุณ :)
Andras Deak

ตกลงความต่อเนื่องไม่รวมอยู่โดยอัตโนมัติ ... แต่ฉันค่อนข้างแน่ใจว่ามันจะต้องต่อเนื่องกันอยู่ดี อืม ...
Andras Deak

1
@AndrasDeak มันก็ดีที่จะถือว่าฉลากติดกันและเรียงลำดับ (แก้ไขได้ถ้าไม่ใช่เรื่องง่ายอยู่แล้ว)
Jean-Paul

1
@AndrasDeak ดูการแก้ไขคำถามเพื่อทดสอบข้อมูล (เช่นการเปรียบเทียบประสิทธิภาพของคำถามต่าง ๆ มีความสอดคล้องกัน)
Jean-Paul

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)

สำหรับขนาดตัวอย่างเดียวกันฉันได้วิธี dict ของ Aryerezเป็น:

%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)

อย่างไรก็ตามด้วยค่าใช้จ่ายของความเสถียรบางคำตอบโดย Divakar ที่ใช้ numpy ล้วนมาใน:

%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)

ขอบคุณสำหรับคำตอบนี้! เพื่อความสอดคล้องกับคำตอบอื่น ๆ คุณสามารถทดสอบโซลูชันของคุณกับข้อมูลตัวอย่างที่ให้ไว้ในการแก้ไขคำถามของฉันได้หรือไม่
Jean-Paul

@ Jean-Paul การกำหนดเวลาสอดคล้องกันแล้วใช่ไหม? พวกเขาใช้ข้อมูลมาตรฐานเริ่มต้นของฉันและในกรณีที่ไม่เป็นเช่นนั้นฉันได้กำหนดเวลาสำหรับพวกเขาด้วยมาตรฐานเดียวกัน
roganjosh

ฉันมองข้ามคุณยังได้เพิ่มการอ้างอิงถึงคำตอบของ Divakar อยู่แล้วดังนั้นคำตอบของคุณได้ทำการเปรียบเทียบที่ดีระหว่างวิธีการต่าง ๆ แล้วขอบคุณมาก!
Jean-Paul

1
@ Jean-Paul ฉันได้เพิ่มการกำหนดเวลาล่าสุดที่ด้านล่างเนื่องจากจริง ๆ แล้วมันเปลี่ยนสิ่งต่าง ๆ ค่อนข้างมาก
4256

1
ขออภัยที่ไม่เพิ่มชุดทดสอบเมื่อโพสต์คำถามชื่นชมอย่างสูงว่าคุณยังเพิ่มผลการทดสอบทันที! ขอบคุณ !!!
Jean-Paul

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เป็นมากเสื้อคลุมบาง ๆ สำหรับวงดังนั้นฉันจะไม่คาดหวังวิธีการนี้โดยเฉพาะอย่างยิ่งที่จะเป็นไปอย่างรวดเร็ว
Andras Deak

1
@AndrasDeak ฉันไม่เห็นด้วย :) ฉันจะติดตามต่อไปและหากมีคนโพสต์วิธีแก้ปัญหาที่ดีกว่าฉันจะลบมัน
Aryerez

1
ฉันไม่คิดว่าคุณจะต้องลบมันแม้ว่าจะมีวิธีการที่รวดเร็วกว่าปรากฏขึ้น :)
Andras Deak

@roganjosh นั่นอาจเป็นเพราะคุณไม่ได้กำหนดdataและindexเป็นnp.arrayในฐานะที่เป็นปัญหา
Aryerez

1
@ Jean-Paul roganjosh ทำการเปรียบเทียบเวลาระหว่างฉันกับวิธีการของเขาและคนอื่น ๆ ที่นี่เปรียบเทียบพวกเขา มันขึ้นอยู่กับฮาร์ดแวร์ของคอมพิวเตอร์ดังนั้นทุกคนจึงไม่มีวิธีการตรวจสอบวิธีการของตัวเอง แต่ดูเหมือนว่าฉันคิดวิธีแก้ปัญหาที่ช้าที่สุดที่นี่
Aryerez

4

นี่คือวิธีการของ NumPy ในการรับ binned-median สำหรับค่า bins / index ที่เป็นบวก -

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')?
Jean-Paul

@ Jean-Paul คุณสามารถทดสอบชุดข้อมูลจริงของคุณนับล้านได้หรือไม่
Divakar

ดูการแก้ไขคำถามเพื่อทดสอบข้อมูล
Jean-Paul

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