web-dev-qa-db-fra.com

Regroupement Numpy utilisant itertools.groupby performance

J'ai beaucoup de grandes listes d'entiers (> 35 000 000) qui contiendront des doublons. Je dois obtenir un compte pour chaque entier de la liste. Le code suivant fonctionne, mais semble lent. Quelqu'un d'autre peut-il améliorer la référence en utilisant Python et de préférence 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)

qui retourne:

$ python bench.py 
111.377498865

À votre santé!

Modifier basé sur les réponses:

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

qui retourne:

$ python bench.py 
Original: 113.385262966 secs
Gnibbler: 71.7464978695 secs
Christophe: 27.1690568924 secs
Paul: 9.06268405914 secs

Bien que Christophe donne actuellement des résultats incorrects

26
Donny

je reçois une amélioration 3x en faisant quelque chose comme ça:

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)
30
Paul

Plus de cinq ans se sont écoulés depuis que la réponse de Paul a été acceptée. Fait intéressant, La sort() est toujours le goulot d’étranglement de la solution acceptée. 

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)

La solution acceptée s’exécute pendant 4,0 s sur ma machine. Avec le tri radix, elle descend À 1,7 s. 

Il suffit de passer au tri de base pour obtenir une accélération globale de 2,35x. La sorte de base est plus de 4x plus rapide que quicksort dans ce cas.

Voir Comment trier un tableau d'entiers plus rapidement que quicksort? cela a été motivé par votre question.

10
Ali

Sur demande, voici une version Cython. J'ai fait deux passages à travers le tableau. Le premier découvre le nombre d'éléments uniques qui peuvent contenir mes tableaux pour les valeurs uniques et les comptes de taille appropriée.

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

Le tri prend de loin le plus de temps. En utilisant le tableau de valeurs donné dans mon code, le tri prend 4,75 secondes et la recherche réelle des valeurs uniques et des comptes prend 0,67 seconde. Avec le code pur Numpy utilisant le code de Paul (mais avec la même forme du tableau de valeurs) avec le correctif suggéré dans un commentaire, la recherche des valeurs uniques et des comptes prend 1,9 seconde (le tri prend toujours le même temps, bien sûr). 

Il est logique que le tri prenne la plupart du temps en compte car il s’agit de O (N log N) et le comptage est O (N). Vous pouvez accélérer le tri un peu plus que Numpy's (qui utilise qsort de C si je me souviens bien), mais vous devez vraiment savoir ce que vous faites et ce n'est probablement pas rentable. En outre, il y aurait peut-être moyen d'accélérer un peu plus mon code Cython, mais cela n'en vaut probablement pas la peine.

4
Justin Peel

J'imagine que l'approche la plus évidente et toujours non mentionnée est d'utiliser simplement collections.Counter. Au lieu de créer une quantité énorme de listes temporairement utilisées avec groupby, il ne fait que monter les nombres entiers. Il s’agit d’une solution double et rapide, mais toujours plus lente que les solutions numpy pures.

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)

J'obtiens une accélération de 136 à 62 s pour ma machine, par rapport à la solution initiale.

2
Michael

C'est un fil assez ancien, mais je pensais mentionner qu'il y avait une petite amélioration à apporter à la solution actuellement acceptée:

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)

Cela a été testé environ une demi-seconde plus vite sur ma machine; pas une amélioration énorme, mais vaut quelque chose. De plus, je pense que ce qui se passe ici est plus clair. L’approche diff en deux étapes est un peu opaque au premier abord. 

2
senderle

C'est une solution numpy:

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's 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)

S'exécute en environ 25 secondes sur ma machine par rapport à environ 96 pour votre solution initiale (ce qui constitue une amélioration intéressante).

Il y a peut-être encore place à amélioration, je n'utilise pas numpy aussi souvent.

Edit : ajout de quelques commentaires dans le code.

2
ChristopheD

Remplacer len(list(g)) par sum(1 for i in g) donne un gain de vitesse 2x

1
John La Rooy

Vous pouvez essayer l’utilisation suivante (ab) de 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

C'est plus lent que la réponse gagnante, peut-être parce que scipy ne prend actuellement pas en charge les types d'index non signés ...

0
joeln

Dans la dernière version de numpy, nous avons ceci.

import numpy as np
frequency = np.unique(values, return_counts=True)
0
Gabriel_F

Le tri est thêta (NlogN), je choisirais l'amortissement O(N) fourni par Python. Utilisez simplement defaultdict(int) pour conserver le nombre d'entiers et parcourez simplement le tableau une fois:

counts = collections.defaultdict(int)
for v in values:
    counts[v] += 1

C'est théoriquement plus rapide, malheureusement, je n'ai aucun moyen de vérifier maintenant. L'allocation de la mémoire supplémentaire peut en réalité la ralentir par rapport à votre solution, qui est en place.

Edit: Si vous avez besoin d’économiser de la mémoire, essayez la méthode Radix sort, qui est beaucoup plus rapide sur les entiers que sur quicksort (ce que Numpy utilise, je crois).

0
Rafał Dowgird