6

This is almost the same question than here, except that I am asking about the most efficient solution for a sorted result.

I have a list (about 10 integers randomly between 0 and 12), for example:

the_list = [5, 7, 6, 5, 5, 4, 4, 7, 5, 4]

I want to create a function that returns a list of tuples (item, counts) ordered by the first element, for example

output = [(4, 3), (5, 4), (6, 1), (7, 2)]

So far I have used:

def dupli(the_list):
    return [(item, the_list.count(item)) for item in sorted(set(the_list))]

But I call this function almost a millon time and I need to make it as fast as I (python) can. Therefore my question: How to make this function less time comsuming? (what about memory?)

I have played around a bit, but nothing obvious came up:

from timeit import Timer as T
number=10000
setup = "the_list=[5, 7, 6, 5, 5, 4, 4, 7, 5, 4]"

stmt = "[(item, the_list.count(item)) for item in sorted(set(the_list))]"
T(stmt=stmt, setup=setup).timeit(number=number)

Out[230]: 0.058799982070922852

stmt = "L = []; \nfor item in sorted(set(the_list)): \n    L.append((item, the_list.count(item)))"
T(stmt=stmt, setup=setup).timeit(number=number)

Out[233]: 0.065041065216064453

stmt = "[(item, the_list.count(item)) for item in set(sorted(the_list))]"
T(stmt=stmt, setup=setup).timeit(number=number)

Out[236]: 0.098351955413818359

Thanks
Christophe

Community
  • 1
  • 1
Christophe
  • 517
  • 2
  • 5
  • 9
  • Which python version are you using? – Felix Kling Dec 16 '10 at 01:44
  • 6
    As a programmer, I would be asking myself not "How can I make this thing take less time?" but "How can I avoid doing it a million times?" Are you certain that your algorithm that requires this function is optimal on the larger scale to begin with? – DGH Dec 16 '10 at 01:46
  • If you call your function "almost a million times", this will take about 5 seconds -- is this really a problem? – Sven Marnach Dec 16 '10 at 02:36
  • to DGH: I'm simulating poker hands. According to the complexity of the code within the loop (which runs almost a millions time), I don't think I could vectorize it or that I could avoid calling dupli at least once per loop. – Christophe Dec 16 '10 at 03:33
  • to Sven Marnach: It is not a problem since this poker-program is only for fun, I'm just taking this as an opportunity to learn more python. However, it is very possible that I will run these 1-million-hands many times or that I want an instant answer (if I play online in parallel for example). – Christophe Dec 16 '10 at 03:35
  • Since you know that the number of items is quite small, it is not out of the question to use a O(n^2) algorithm as you have seen. Usually this type of approach loses before n gets very large at all, but that is irrelevant here. You may get a good speed up by using cython for this function, but much of the time will still be taken up manipulating the python objects – John La Rooy Dec 16 '10 at 04:50

6 Answers6

4

Change where you sort for a savings of about 20%.

Change this:

def dupli(the_list):
    return [(item, the_list.count(item)) for item in sorted(set(the_list))]

To this:

def dupli(the_list):
    count = the_list.count # this optimization added courtesy of Sven's comment
    result = [(item, count(item)) for item in set(the_list)]
    result.sort()
    return result

The reason this is faster is that the sorted iterator must create a temporary list, whereas sorting the result sorts in place.

edit: Here's another approach that is 35% faster than your original:

def dupli(the_list):
    counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    for n in the_list:
        counts[n] += 1
    return [(i, counts[i]) for i in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) if counts[i]]

Note: You may want to randomize the values for the_list. My final version of dupli tests even faster with other random data sets (import random; the_list=[random.randint(0,12) for i in xrange(10)])

Steven Rumbalski
  • 44,786
  • 9
  • 89
  • 119
  • This is the fastest approach I have seen so far (using plain CPython 2.6.6). It can be slightly improved by looking up `.count()` outside of the list comprehension (i.e. `count = the_list.count` before `result = ...`, and then use `(item, count(item))` inside the list comprehension). – Sven Marnach Dec 16 '10 at 05:42
  • @Sven Marnach: Nice optimization. I've updated my answer to include it. I've also added another approach which was based on John Machin's answer, but it tests much faster due to the elimination of `enumerate` and because it expands `[0] * 13` to its result. – Steven Rumbalski Dec 16 '10 at 06:03
3

I would try:

from collections import defaultdict
output = defaultdict(lambda: 0)
for item in the_list: output[item] += 1
return sorted(output.items())
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
2

Taking advantage of the qualification "between 0 and 12":

>>> the_list = [5, 7, 6, 5, 5, 4, 4, 7, 5, 4]
>>> answer1 = [0] * 13
>>> for i in the_list:
...    answer1[i] += 1
...
>>> answer1
[0, 0, 0, 0, 3, 4, 1, 2, 0, 0, 0, 0, 0]
>>> # You might be able to use that as-is:
...
>>> for i, v in enumerate(answer1):
...     if v: print i, v
...
4 3
5 4
6 1
7 2
>>> # Otherwise you can build the list that you specified:
...
>>> answer2 = [(i, v) for i, v in enumerate(answer1) if v]
>>> answer2
[(4, 3), (5, 4), (6, 1), (7, 2)]
>>>
John Machin
  • 81,303
  • 11
  • 141
  • 189
  • This is the first thing I tried. On my machine, it is on average the same speed as the original `dupli()` -- at least if the output is converted to the requested format (`answer2`). – Sven Marnach Dec 16 '10 at 02:22
0

It might be faster to write your own function that counts the numbers in one pass through the list. You're calling the count function for every number in the set, and each of those calls requires a pass through the list.

counts = {}
for n in the_list:
    if n not in counts:
        counts[n] = 0
    counts[n] += 1
sorted(counts.items())
Colin
  • 10,447
  • 11
  • 46
  • 54
  • This is slower than the function in the OP on my machine, though by a smaller margin than all other suggestions so far. – Sven Marnach Dec 16 '10 at 02:00
0

This seems fairly optimal in terms of space and speed:

def dupli2(list_):                                    
    dict_ = {}                                       
    for item in list_:                               
        dict_[item] = dict_.get(item, 0) + 1         
    return sorted(dict_.items())                    

Or this:

def dupli3(list_):                                            
    last = None                                               
    list_ = sorted(list_)                                     

    i = 0                                                     
    for item in list_:                                        
        if item != last and last is not None:                 
            yield last, i                                     
            i = 0                                             
        i += 1                                                
        last = item                                           

    yield last, i 

Not sure about the speed though. For that I'd recommend that you either do it in C or use Psyco ;)

With Psyco:

In [33]: %timeit list(dupli3(test.the_list))
100000 loops, best of 3: 6.46 us per loop

In [34]: %timeit list(dupli2(test.the_list))
100000 loops, best of 3: 2.37 us per loop

In [35]: %timeit list(dupli(test.the_list))
100000 loops, best of 3: 2.7 us per loop
Wolph
  • 78,177
  • 11
  • 137
  • 148
  • Both these functions are significantly slower on my machine than the function in the OP. Additionally, the second function returns a wrong result. – Sven Marnach Dec 16 '10 at 01:57
  • @Sven Marnach: that depends, if you use `psyco` than the `dupli2` method is faster over here. You're right about the `dupli3` method, I made a stupid mistake there and accidently posted an earlier version here. I'll update it :) – Wolph Dec 16 '10 at 02:43
  • OK, I used CPython 2.6.6. It gets slightly faster if you move the attribute lookup for `.get()` out of the loop. – Sven Marnach Dec 16 '10 at 02:53
  • 1
    When timing `dupli()` and `dupli2()` you copied the returned list using `list()`. This seems a bit pointless. – Sven Marnach Dec 16 '10 at 02:55
  • you should use setdefault instead of the shenanigans with get – John La Rooy Dec 16 '10 at 04:53
  • 1
    @gnibbler: what is the point of using `setdefault` with an integer? You can't increment it anyway so you'll be doing pretty much the same. Only a `defaultdict` would make it shorter/better. – Wolph Dec 16 '10 at 10:34
  • @Sven Marnach: ofcourse, it was the only way to make somewhat of a fair comparison of the 3 methods. – Wolph Dec 16 '10 at 10:35
0

itertools.groupby is perfect for this:

>>> from itertools import groupby
>>> the_list = [5, 7, 6, 5, 5, 4, 4, 7, 5, 4]
>>> gb = groupby(sorted(the_list))
>>> print [(i,len(list(j))) for i,j in gb]
[(4, 3), (5, 4), (6, 1), (7, 2)]
PaulMcG
  • 62,419
  • 16
  • 94
  • 130