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

따라서 수백만 행을 말했을 때 실제 데이터 세트가 2D 배열입니까? 각 행에서이 작업을 수행하고 있습니까?
Divakar

테스트 데이터에 대한 질문에 @Divakar 참조 편집
장 폴

당신은 이미 초기 데이터에서 벤치 마크를 받았고, 형식을 동일하게 유지하기 위해 팽창했습니다. 팽창 된 데이터 세트에 대해 모든 것이 벤치 마크됩니다. 지금 변경하는 것은 합리적이지 않습니다
roganjosh

답변:


7

때때로 당신은 당신이 경우 비 관용적 NumPy와 코드를 작성할 필요가 정말 당신이 기본 NumPy와 함께 할 수없는 당신의 계산 속도를합니다.

numba파이썬 코드를 저수준 C로 컴파일합니다. 많은 numpy 자체는 일반적으로 C만큼 빠르기 때문에 문제가 numpy를 사용하여 기본 벡터화에 적합하지 않은 경우에 유용합니다. 이것은 하나의 예입니다 (인덱스가 연속적이고 정렬되어 있다고 가정하고 예제 데이터에도 반영됨).

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)

문제의 업데이트 된 예제 데이터를 사용하면 이러한 숫자 (예 : 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)

이것은 가속 코드를 사용하여 작은 경우에는 65 배, 큰 경우에는 26 배의 속도 향상 (물론 느린 루프 코드와 비교)에 해당합니다. 또 다른 단점은 (기본 numpy를 사용하는 일반적인 벡터화와 달리)이 속도를 달성하기 위해 추가 메모리가 필요하지 않다는 것입니다. 이것은 결국 실행되고 결국 최적화되고 컴파일 된 저수준 코드에 관한 것입니다.


위 함수는 numpy int 배열이 int64기본적으로 가정하지만 실제로는 Windows에서는 그렇지 않습니다. 대안은에 대한 호출에서 서명을 제거하여 numba.njit적절한 적시 컴파일을 트리거하는 것입니다. 그러나 이것은 첫 번째 실행 중에 함수가 컴파일되어 타이밍 결과와 충돌 할 수 있음을 의미합니다 (대표 데이터 유형을 사용하여 수동으로 한 번 함수를 한 번 실행하거나 첫 번째 타이밍 실행이 훨씬 느리다는 것을 받아 들일 수 있음) 무시하십시오). 이것은 미리 컴파일을 트리거하는 서명을 지정하여 방지하려고했던 것입니다.

어쨌든, 적절한 JIT 케이스에서 우리가 필요로하는 데코레이터는

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

jit 컴파일 된 함수에 대해 위에서 보여준 타이밍은 함수가 컴파일 된 후에 만 ​​적용됩니다. 이는 정의 (열심 한 컴파일, 명시 적 서명이 전달 될 때 numba.njit) 또는 첫 번째 함수 호출 (지연이없는 서명이 전달 될 때 numba.njit)에서 발생합니다. 함수가 한 번만 실행될 예정이라면이 방법의 속도에 대해 컴파일 시간도 고려해야합니다. 일반적으로 컴파일 + 실행의 총 시간이 컴파일되지 않은 런타임 (기본 파이썬 함수가 매우 느린 위의 경우에 해당)보다 짧은 경우에만 함수를 컴파일하는 것이 좋습니다. 컴파일 된 함수를 여러 번 호출 할 때 주로 발생합니다.

으로 max9111이 코멘트에 언급, 하나 개 중요한 기능 numba은 IS cache키워드jit. 전달 cache=True하기 위해 numba.jit, 디스크에 컴파일 된 기능을 저장합니다 그래서 당신은 장기적으로이 아닌 재 컴파일을 다시 마련 할 수있는 런타임에서 주어진 파이썬 모듈의 다음 실행시에 기능이로드됩니다.


@Divakar는 실제로 인덱스가 연속적이고 정렬되어 있다고 가정합니다. 이는 OP의 데이터에서 가정 된 것처럼 보였으며 자동으로 roganjosh의 index데이터에 포함되었습니다 . 감사합니다 :)
Andras Deak

OK, 연속성은 자동으로 포함되지 않지만 어쨌든 인접해야합니다. 흠 ...
Andras Deak

1
@AndrasDeak 레이블이 연속적이고 정렬되어 있다고 가정하는 것이 좋습니다 (어쨌든 쉽지 않은 경우 고정)
Jean-Paul

1
@AndrasDeak 데이터 테스트를 위해 편집 질문을 참조하십시오 (질문 간 성능 비교가 일관되도록)
Jean-Paul

1
cache=True운영체제를 다시 시작할 때마다 재 컴파일을 피하기 위해 키워드 를 언급 할 수 있습니다.
max9111

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)

동일한 표본 크기 에 대해 Aryerezdict 접근 방식은 다음 과 같습니다.

%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 는 순수한 숫자를 사용하여 대답합니다 .

%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
@ 장 폴 나는 그것이 사실은 꽤 과감하게 일을 변경하기 때문에 하단에있는 최신 타이밍을 추가
roganjosh

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.vectorizeA는 매우 루프 얇은 래퍼, 그래서 나는이 방법은 특히 빨리 기대하지 않을 것이다.
Andras Deak

1
@AndrasDeak 나는 동의하지 않는다 :) 나는 계속 따라갈 것이고, 누군가가 더 나은 해결책을 게시한다면 그것을 삭제할 것이다.
Aryerez

1
나는 더 빠른 접근 방식이 팝업 되더라도 그것을 삭제해야한다고 생각하지 않습니다. :)
Andras Deak

@roganjosh 아마도 질문에서 dataindex같이 정의하지 않았기 때문일 수 있습니다 np.array.
Aryerez

1
@ Jean-Paul roganjosh는 광산과 그의 방법을 시간 비교하고 다른 사람들은 그들의 방법을 비교했습니다. 그것은 컴퓨터 하드웨어에 달려 있으므로 모든 사람이 자신의 방법을 확인하는 것은 중요하지 않지만 여기서 가장 느린 해결책을 찾은 것으로 보입니다.
Aryerez

4

양수 bin / 인덱스 값에 대해 binned-median을 얻는 NumPy 기반 접근법은 다음과 같습니다.

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 더 간단한 솔루션을 위해 솔루션을 편집했습니다. 당신이 있다면, 테스트를 위해 이것을 사용하십시오.
Divakar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.