2017-03-17 4 views
7

내가 예를 들어, 두 NumPy와 배열, 모양 (d, f)A0..n의 모양 (d,) 포함 인덱스의 I을 말해봐NumPy와 감소는

I = np.array([0, 0, 1, 0, 2, 1]) 
A = np.arange(12).reshape(6, 2) 

나는 특히 sum, meanmax에, 감소를 만들 수있는 빠른 방법을 찾고, 모든 조각 A[I == i, :]을 통해; 느린 버전은이 경우에 제공

results = np.zeros((I.max() + 1, A.shape[1])) 
for i in np.unique(I): 
    results[i, :] = np.mean(A[I == i, :], axis=0) 

results = [[ 2.66666667, 3.66666667], 
      [ 7.  , 8.  ], 
      [ 8.  , 9.  ]]) 

편집 : 나는 Divakar의 대답과 이전 포스터의 (삭제) pandas 기반의 답변에 따라 약간의 타이밍을했다 .

타이밍 코드 :

from __future__ import division, print_function 
import numpy as np, pandas as pd 
from time import time 

np.random.seed(0) 
d = 500000 
f = 500 
n = 500 
I = np.hstack((np.arange(n), np.random.randint(n, size=(d - n,)))) 
np.random.shuffle(I) 
A = np.random.rand(d, f) 

def reduce_naive(A, I, op="avg"): 
    target_dtype = (np.float if op=="avg" else A.dtype) 
    results = np.zeros((I.max() + 1, A.shape[1]), dtype=target_dtype) 
    npop = {"avg": np.mean, "sum": np.sum, "max": np.max}.get(op) 
    for i in np.unique(I): 
     results[i, :] = npop(A[I == i, :], axis=0) 
    return results 

def reduce_reduceat(A, I, op="avg"): 
    sidx = I.argsort() 
    sI = I[sidx] 
    sortedA = A[sidx] 
    idx = np.r_[ 0, np.flatnonzero(sI[1:] != sI[:-1])+1 ] 
    if op == "max": 
     return np.maximum.reduceat(sortedA, idx, axis=0) 
    sums = np.add.reduceat(sortedA, idx, axis=0) 
    if op == "sum": 
     return sums 
    if op == "avg": 
     count = np.r_[idx[1:] - idx[:-1], A.shape[0] - idx[-1]] 
     return sums/count.astype(float)[:,None] 

def reduce_bincount(A, I, op="avg"): 
    ids = (I[:,None] + (I.max()+1)*np.arange(A.shape[1])).ravel() 
    sums = np.bincount(ids, A.ravel()).reshape(A.shape[1],-1).T 
    if op == "sum": 
     return sums 
    if op == "avg": 
     return sums/np.bincount(ids).reshape(A.shape[1],-1).T 

def reduce_pandas(A, I, op="avg"): 
    group = pd.concat([pd.DataFrame(A), pd.DataFrame(I, columns=("i",)) 
        ], axis=1 
        ).groupby('i') 
    if op == "sum": 
     return group.sum().values 
    if op == "avg": 
     return group.mean().values 
    if op == "max": 
     return group.max().values 

def reduce_hybrid(A, I, op="avg"): 
    sidx = I.argsort() 
    sI = I[sidx] 
    sortedA = A[sidx] 

    idx = np.r_[ 0, np.flatnonzero(sI[1:] != sI[:-1])+1 ] 
    unq_sI = sI[idx]  

    m = I.max()+1 
    N = A.shape[1] 

    target_dtype = (np.float if op=="avg" else A.dtype) 
    out = np.zeros((m,N),dtype=target_dtype) 
    ss_idx = np.r_[idx,A.shape[0]] 

    npop = {"avg": np.mean, "sum": np.sum, "max": np.max}.get(op) 
    for i in range(len(idx)): 
     out[unq_sI[i]] = npop(sortedA[ss_idx[i]:ss_idx[i+1]], axis=0) 
    return out 

for op in ("sum", "avg", "max"): 
    for name, method in (("naive ", reduce_naive), 
         ("reduceat", reduce_reduceat), 
         ("pandas ", reduce_pandas), 
         ("bincount", reduce_bincount), 
         ("hybrid ", reduce_hybrid) 
         ("numba ", reduce_numba) 
         ):  
     if op == "max" and name == "bincount": 
      continue 
     # if name is not "naive": 
     #  assert np.allclose(method(A, I, op), reduce_naive(A, I, op)) 
     times = [] 
     for tries in range(3): 
      time0 = time(); method(A, I, op) 
      times.append(time() - time0); 
     print(name, op, "{:.2f}".format(np.min(times))) 
    print() 

타이밍는 :

naive sum 1.10 
reduceat sum 4.62 
pandas sum 5.29 
bincount sum 1.54 
hybrid sum 0.62 
numba sum 0.31 

naive avg 1.12 
reduceat avg 4.45 
pandas avg 5.23 
bincount avg 2.43 
hybrid avg 0.61 
numba avg 0.33 

naive max 1.19 
reduceat max 3.18 
pandas max 5.24 
hybrid max 0.72 
numba max 0.34 

(내 사용 사례에 대한 dn 전형적인 값을 선택했다 - 나는 numba-버전에 대한 코드를 추가 한 내 대답은).

+0

당신의'pandas' 시간은 저의 테스트와 비교해 보았을 때 낮습니다. 그러나 그것은 여러분이 데이터 프레임 생성과'groupby'를 시간 루프 밖으로 가져 왔기 때문입니다. – hpaulj

+0

@hpaulj'groupby'는 함수 내부에 있으므로 시간 루프 내부에 있습니다. 타이밍을 업데이트하고 씨앗을 고정하고 재현성을 높이기 위해 3 개를 가장 좋았습니다. 나는 판다 '0.19.2'를 가지고있다. – Maxim

+0

Divakar의 솔루션을 순수한 접근 방식으로 받아 들였지만 numba 또는 cython으로 컴파일 된 코드가 가장 빠른 솔루션을 제공해야합니다. – Maxim

답변

6

접근 방법 # 1 : NumPy와 ufunc를 사용하여 우리는 그 세 감소 작업 ufuncs을 가지고 다행히 우리는 또한 축으로 따라 특정 간격으로 제한하는 감축을 수행 할 수 ufunc.reduceat

을 reduceat. 그래서, 그 사용하여, 우리는 그 세 작업과 같이 계산 할 것이다 -

# Gives us sorted array based on input indices I and indices at which the 
# sorted array should be interval-limited for reduceat operations to be 
# applied later on using those results 
def sorted_array_intervals(A, I): 
    # Compute sort indices for I. To be later used for sorting A based on it. 
    sidx = I.argsort() 
    sI = I[sidx] 
    sortedA = A[sidx] 

    # Get indices at which intervals change. Also, get count in each interval 
    idx = np.r_[ 0, np.flatnonzero(sI[1:] != sI[:-1])+1 ] 
    return sortedA, idx 

# Groupby sum reduction using the interval indices 
# to perform interval-limited ufunc reductions 
def groupby_sum(A, I): 
    sortedA, idx = sorted_array_intervals(A,I) 
    return np.add.reduceat(sortedA, idx, axis=0) 

# Groupby mean reduction 
def groupby_mean(A, I): 
    sortedA, idx = sorted_array_intervals(A,I) 
    sums = np.add.reduceat(sortedA, idx, axis=0) 
    count = np.r_[idx[1:] - idx[:-1], A.shape[0] - idx[-1]] 
    return sums/count.astype(float)[:,None] 

# Groupby max reduction 
def groupby_max(A, I): 
    sortedA, idx = sorted_array_intervals(A,I) 
    return np.maximum.reduceat(sortedA, idx, axis=0) 

을 따라서, 우리는 모든 작업을 필요로하는 경우, 우리는 sorted_array_intervals의 한 인스턴스로 다시 사용할 수 있도록처럼 -

def groupby_sum_mean_max(A, I): 
    sortedA, idx = sorted_array_intervals(A,I) 
    sums = np.add.reduceat(sortedA, idx, axis=0) 
    count = np.r_[idx[1:] - idx[:-1], A.shape[0] - idx[-1]] 
    avgs = sums/count.astype(float)[:,None] 
    maxs = np.maximum.reduceat(sortedA, idx, axis=0) 
    return sums, avgs, maxs 

접근법 # 1-B : 하이브리드 버전 (정렬 + 슬라이스 + 감소) 여기서

가 정렬 된 배열과 간격을 t로 변경하는 인덱스를 얻을 sorted_array_intervals의 도움 걸립니다 않는 하이브리드 버전의 그 다음 그룹이지만 마지막 단계에서 슬라이싱을 사용하여 각 간격을 합산하고이를 각 그룹에 대해 반복적으로 수행합니다. 우리가 views과 함께 일할 때 여기에서 슬라이스가 도움이됩니다.

def reduce_hybrid(A, I, op="avg"): 
    sidx = I.argsort() 
    sI = I[sidx] 
    sortedA = A[sidx] 

    # Get indices at which intervals change. Also, get count in each interval 
    idx = np.r_[ 0, np.flatnonzero(sI[1:] != sI[:-1])+1 ] 
    unq_sI = sI[idx]  

    m = I.max()+1 
    N = A.shape[1] 

    target_dtype = (np.float if op=="avg" else A.dtype) 
    out = np.zeros((m,N),dtype=target_dtype) 
    ss_idx = np.r_[idx,A.shape[0]] 

    npop = {"avg": np.mean, "sum": np.sum, "max": np.max}.get(op) 
    for i in range(len(idx)): 
     out[unq_sI[i]] = npop(sortedA[ss_idx[i]:ss_idx[i+1]], axis=0) 
    return out 

런타임 테스트 (질문에 게시 벤치 마크에서 설정을 사용) - -

구현은 다음과 같이 보일 것이다

In [432]: d = 500000 
    ...: f = 500 
    ...: n = 500 
    ...: I = np.hstack((np.arange(n), np.random.randint(n, size=(d - n,)))) 
    ...: np.random.shuffle(I) 
    ...: A = np.random.rand(d, f) 
    ...: 

In [433]: %timeit reduce_naive(A, I, op="sum") 
    ...: %timeit reduce_hybrid(A, I, op="sum") 
    ...: 
1 loops, best of 3: 1.03 s per loop 
1 loops, best of 3: 549 ms per loop 

In [434]: %timeit reduce_naive(A, I, op="avg") 
    ...: %timeit reduce_hybrid(A, I, op="avg") 
    ...: 
1 loops, best of 3: 1.04 s per loop 
1 loops, best of 3: 550 ms per loop 

In [435]: %timeit reduce_naive(A, I, op="max") 
    ...: %timeit reduce_hybrid(A, I, op="max") 
    ...: 
1 loops, best of 3: 1.14 s per loop 
1 loops, best of 3: 631 ms per loop 

접근 방법 # 2 : 사용 숫자 한도

여기 있습니다. 빈 기반 합산을 수행하는 np.bincount을 사용하는 또 다른 접근법. 그래서, 그것으로 우리는 합계 및 평균 값을 계산 할 수 있으며 같은, 그 과정에서 정렬 피 - 난 그냥-에 함께 짧은 타이밍을 얻을 수 있었다 파이썬/NumPy와 JIT 컴파일러 Numba를 사용

ids = (I[:,None] + (I.max()+1)*np.arange(A.shape[1])).ravel() 
sums = np.bincount(ids, A.ravel()).reshape(A.shape[1],-1).T 
avgs = sums/np.bincount(ids).reshape(A.shape[1],-1).T 
+0

큰 일을하면서 나는 reduceat을 알아 차렸지만 연속적인 간격으로 만 작동하기 때문에 그것을 버렸다. – Maxim

+0

'_, idx, count = np.unique (sI, return_counts = True, return_index = True)'를 사용할 수도있다. 비록 일종의 일이긴하지만 실제로는 'r_' 표현식보다 조금 빠릅니다. – hpaulj

+0

정렬 기반 메서드에 대한 명백하게 불리한 타이밍을 기반으로 지금 질문을 다시 열기로 결정했습니다. – Maxim

2

직관적 인 선형 알고리즘의 -time 편집 : 벤치 마크 문제에

from numba import jit 

@jit 
def reducenb_avg(A, I): 
    d, f = A.shape 
    n = I.max() + 1 
    result = np.zeros((n, f), np.float) 
    count = np.zeros((n, 1), int) 
    for i in range(d): 
     result[I[i], :] += A[i] 
     count[I[i], 0] += 1 
    return result/count 

@jit 
def reducenb_sum(A, I): 
    d, f = A.shape 
    n = I.max() + 1 
    result = np.zeros((n, f), A.dtype) 
    for i in range(d): 
     result[I[i], :] += A[i] 
    return result 

@jit 
def reducenb_max(A, I): 
    d, f = A.shape 
    n = I.max() + 1 
    result = -np.inf * np.ones((n, f)) 
    count = np.zeros((n, f)) 
    for i in range(d): 
     result[I[i], :] = np.maximum(A[i], result[I[i], :]) 
    return result 

def reduce_numba(A, I, op="avg"): 
    return {"sum": reducenb_sum, "avg": reducenb_avg, "max": reducenb_max}.get(op)(A, I) 

~ 0.32s에서 이러한 마무리, 순수 NumPy와 정렬 기반 방법의 시간의 절반.

0

이 사용할 수있는 또 다른 도구는 버퍼링 add.at입니다 :

def add_at(I,A): 
    n = I.max() + 1 
    res = np.zeros((n,A.shape[1])) 
    cnt = np.zeros((n,1)) 
    np.add.at(res, I, A) 
    np.add.at(cnt, I, 1) 
    return res/cnt 

비교가 아니라 시험이 작은 문제에 대한

In [438]: add_at(I,A) 
Out[438]: 
array([[ 2.66666667, 3.66666667], 
     [ 7.  , 8.  ], 
     [ 8.  , 9.  ]]) 

(그것은 구조 numbareducenb_avg에 매우 가까이) 다른 사람에게,하지만 잘 확장되지 않습니다 (3 배속에서 12 배속으로 느리게 진행).