Группировка с использованием itertools.groupby
У меня есть много больших ( > 35 000 000) списков целых чисел, которые будут содержать дубликаты. Мне нужно получить подсчет для каждого целого в списке. Следующий код работает, но кажется медленным. Может ли кто-нибудь еще лучше тестировать Python и желательно Numpy?
def group():
import numpy as np
from itertools import groupby
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
groups = ((k,len(list(g))) for k,g in groupby(values))
index = np.fromiter(groups,dtype='u4,u2')
if __name__=='__main__':
from timeit import Timer
t = Timer("group()","from __main__ import group")
print t.timeit(number=1)
который возвращает:
$ python bench.py
111.377498865
Ура!
Изменить на основе ответов:
def group_original():
import numpy as np
from itertools import groupby
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
groups = ((k,len(list(g))) for k,g in groupby(values))
index = np.fromiter(groups,dtype='u4,u2')
def group_gnibbler():
import numpy as np
from itertools import groupby
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
groups = ((k,sum(1 for i in g)) for k,g in groupby(values))
index = np.fromiter(groups,dtype='u4,u2')
def group_christophe():
import numpy as np
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
counts=values.searchsorted(values, side='right') - values.searchsorted(values, side='left')
index = np.zeros(len(values),dtype='u4,u2')
index['f0']=values
index['f1']=counts
#Erroneous result!
def group_paul():
import numpy as np
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
diff = np.concatenate(([1],np.diff(values)))
idx = np.concatenate((np.where(diff)[0],[len(values)]))
index = np.empty(len(idx)-1,dtype='u4,u2')
index['f0']=values[idx[:-1]]
index['f1']=np.diff(idx)
if __name__=='__main__':
from timeit import Timer
timings=[
("group_original","Original"),
("group_gnibbler","Gnibbler"),
("group_christophe","Christophe"),
("group_paul","Paul"),
]
for method,title in timings:
t = Timer("%s()"%method,"from __main__ import %s"%method)
print "%s: %s secs"%(title,t.timeit(number=1))
который возвращает:
$ python bench.py
Original: 113.385262966 secs
Gnibbler: 71.7464978695 secs
Christophe: 27.1690568924 secs
Paul: 9.06268405914 secs
Хотя Кристоф дает неверные результаты в настоящее время
Ответы
Ответ 1
Я получаю 3-кратное улучшение, делая что-то вроде этого:
def group():
import numpy as np
values = np.array(np.random.randint(0,3298,size=35000000),dtype='u4')
values.sort()
dif = np.ones(values.shape,values.dtype)
dif[1:] = np.diff(values)
idx = np.where(dif>0)
vals = values[idx]
count = np.diff(idx)
Ответ 2
Прошло более 5 лет с тех пор, как был принят ответ Павла. Интересно,
sort()
все еще является узким местом в принятом решении.
Line # Hits Time Per Hit % Time Line Contents
==============================================================
3 @profile
4 def group_paul():
5 1 99040 99040.0 2.4 import numpy as np
6 1 305651 305651.0 7.4 values = np.array(np.random.randint(0, 2**32,size=35000000),dtype='u4')
7 1 2928204 2928204.0 71.3 values.sort()
8 1 78268 78268.0 1.9 diff = np.concatenate(([1],np.diff(values)))
9 1 215774 215774.0 5.3 idx = np.concatenate((np.where(diff)[0],[len(values)]))
10 1 95 95.0 0.0 index = np.empty(len(idx)-1,dtype='u4,u2')
11 1 386673 386673.0 9.4 index['f0'] = values[idx[:-1]]
12 1 91492 91492.0 2.2 index['f1'] = np.diff(idx)
Принятое решение работает на 4.0 с на моем компьютере, с его сортировкой по методу radix
падает до 1,7 с.
Просто переключившись на сортировку radix, я получаю ускорение в 2,35 раза.. В этом случае сортировка radix более чем в 4 раза быстрее, чем quicksort.
Смотрите Как отсортировать массив целых чисел быстрее, чем quicksort?, который был мотивирован вашим вопросом.
Ответ 3
По просьбе здесь представлена версия Cython. Я сделал два прохода через массив. Первый узнает, сколько уникальных элементов есть, чтобы мои массивы имели уникальные значения и подсчеты соответствующего размера.
import numpy as np
cimport numpy as np
cimport cython
@cython.boundscheck(False)
def dogroup():
cdef unsigned long tot = 1
cdef np.ndarray[np.uint32_t, ndim=1] values = np.array(np.random.randint(35000000,size=35000000),dtype=np.uint32)
cdef unsigned long i, ind, lastval
values.sort()
for i in xrange(1,len(values)):
if values[i] != values[i-1]:
tot += 1
cdef np.ndarray[np.uint32_t, ndim=1] vals = np.empty(tot,dtype=np.uint32)
cdef np.ndarray[np.uint32_t, ndim=1] count = np.empty(tot,dtype=np.uint32)
vals[0] = values[0]
ind = 1
lastval = 0
for i in xrange(1,len(values)):
if values[i] != values[i-1]:
vals[ind] = values[i]
count[ind-1] = i - lastval
lastval = i
ind += 1
count[ind-1] = len(values) - lastval
Сортировка на самом деле занимает больше времени здесь. Используя массив значений, указанный в моем коде, сортировка занимает 4,75 секунды, и фактическое обнаружение уникальных значений и отсчетов занимает 0,67 секунды. С чистым кодом Numpy с использованием кода Paul (но с той же формой массива значений) с исправлением, которое я предложил в комментарии, поиск уникальных значений и отсчетов занимает 1,9 секунды (сортировка по-прежнему занимает такое же количество времени, конечно).
В большинстве случаев имеет смысл использовать сортировку, потому что это O (N log N), а подсчет - O (N). Вы можете немного ускорить сортировку по Numpy (который использует C qsort, если я правильно помню), но вы должны действительно знать, что делаете, и это, вероятно, не стоит. Кроме того, может быть некоторый способ ускорить мой Cython код немного больше, но это, вероятно, не стоит.
Ответ 4
Это многоуровневое решение:
def group():
import numpy as np
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
# we sort in place
values.sort()
# when sorted the number of occurences for a unique element is the index of
# the first occurence when searching from the right - the index of the first
# occurence when searching from the left.
#
# np.dstack() is the numpy equivalent to Python zip()
l = np.dstack((values, values.searchsorted(values, side='right') - \
values.searchsorted(values, side='left')))
index = np.fromiter(l, dtype='u4,u2')
if __name__=='__main__':
from timeit import Timer
t = Timer("group()","from __main__ import group")
print t.timeit(number=1)
Выполняется около 25 секунд на моей машине по сравнению с 96 для вашего первоначального решения (что является хорошим улучшением).
Там может быть место для улучшения, я часто не использую numpy.
Изменить: добавлены комментарии в код.
Ответ 5
Это довольно старый поток, но я подумал, что хочу упомянуть, что в текущем решении есть небольшое улучшение:
def group_by_edge():
import numpy as np
values = np.array(np.random.randint(0,1<<32,size=35000000),dtype='u4')
values.sort()
edges = (values[1:] != values[:-1]).nonzero()[0] - 1
idx = np.concatenate(([0], edges, [len(values)]))
index = np.empty(len(idx) - 1, dtype= 'u4, u2')
index['f0'] = values[idx[:-1]]
index['f1'] = np.diff(idx)
Это было протестировано примерно на полсекунды быстрее на моей машине; не огромное улучшение, но что-то стоит. Кроме того, я думаю, что яснее, что происходит здесь; двухступенчатый подход diff
на первый взгляд немного непрозрачен.
Ответ 6
Я думаю, что самый очевидный и до сих пор не упомянутый подход - просто использовать collections.Counter
. Вместо того, чтобы создавать огромное количество временно используемых списков с groupby, он просто увеличивает число целых чисел. Это oneliner и 2-кратное ускорение, но все еще медленнее, чем чистые решения numpy.
def group():
import sys
import numpy as np
from collections import Counter
values = np.array(np.random.randint(0,sys.maxint,size=35000000),dtype='u4')
c = Counter(values)
if __name__=='__main__':
from timeit import Timer
t = Timer("group()","from __main__ import group")
print t.timeit(number=1)
Я получаю ускорение от 136 с до 62 с для моей машины по сравнению с исходным решением.
Ответ 7
Замена len(list(g))
на sum(1 for i in g)
дает 2x ускорение
Ответ 8
Сортировка - это theta (NlogN), я бы пошел на амортизацию O (N), предоставляемую реализацией хэш-таблицы Python. Просто используйте defaultdict(int)
для хранения отсчетов целых чисел и просто итерации по массиву один раз:
counts = collections.defaultdict(int)
for v in values:
counts[v] += 1
Это теоретически быстрее, к сожалению, я не могу проверить сейчас. Выделение дополнительной памяти может сделать ее на самом деле медленнее, чем ваше решение, которое на месте.
Изменить: если вам нужно сохранить память, попробуйте сортировку radix, которая намного быстрее целых чисел, чем quicksort (что, по моему мнению, используется для использования numpy).
Ответ 9
Вы можете попробовать следующее (ab) использование scipy.sparse
:
from scipy import sparse
def sparse_bincount(values):
M = sparse.csr_matrix((np.ones(len(values)), values.astype(int), [0, len(values)]))
M.sum_duplicates()
index = np.empty(len(M.indices),dtype='u4,u2')
index['f0'] = M.indices
index['f1']= M.data
return index
Это медленнее, чем выигрышный ответ, возможно потому, что scipy
в настоящее время не поддерживает unsigned как типы индексов...
Ответ 10
В последней версии numpy у нас есть это.
import numpy as np
frequency = np.unique(values, return_counts=True)