You could use np.unique to get the unique values in x
, as well as an array of indices (called inverse
). The inverse
can be thought of as "labels" for the elements in x
. Unlike x
itself, the labels are always integers, starting at 0.
Then you can take a bincount of the labels. Since the labels start at 0, the bincount won't be filled with a lot of zeros that you don't care about.
Finally, column_stack will join y
and the bincount into a 2D array:
In [84]: x = np.array([1,2,2,3])
In [85]: y, inverse = np.unique(x, return_inverse=True)
In [86]: y
Out[86]: array([1, 2, 3])
In [87]: inverse
Out[87]: array([0, 1, 1, 2])
In [88]: np.bincount(inverse)
Out[88]: array([1, 2, 1])
In [89]: np.column_stack((y,np.bincount(inverse)))
Out[89]:
array([[1, 1],
[2, 2],
[3, 1]])
Sometimes when an array is small, it turns out that using plain Python methods are faster than NumPy functions. I wanted to check if that was the case here, and, if so, how large x
would have to be before NumPy methods are faster.
Here is a graph of the performance of various methods as a function of the size of x
:
In [173]: x = np.random.random(1000)
In [174]: x.sort()
In [156]: %timeit using_unique(x)
10000 loops, best of 3: 99.7 us per loop
In [180]: %timeit using_groupby(x)
100 loops, best of 3: 3.64 ms per loop
In [157]: %timeit using_counter(x)
100 loops, best of 3: 4.31 ms per loop
In [158]: %timeit using_ordered_dict(x)
100 loops, best of 3: 4.7 ms per loop
For len(x)
of 1000, using_unique
is over 35x faster than any of the plain Python methods tested.
So it looks like using_unique
is fastest, even for very small len(x)
.
Here is the program used to generate the graph:
import numpy as np
import collections
import itertools as IT
import matplotlib.pyplot as plt
import timeit
def using_unique(x):
y, inverse = np.unique(x, return_inverse=True)
return np.column_stack((y, np.bincount(inverse)))
def using_counter(x):
result = collections.Counter(x)
return np.array(sorted(result.items()))
def using_ordered_dict(x):
result = collections.OrderedDict()
for item in x:
result[item] = result.get(item,0)+1
return np.array(result.items())
def using_groupby(x):
return np.array([(k, sum(1 for i in g)) for k, g in IT.groupby(x)])
fig, ax = plt.subplots()
timing = collections.defaultdict(list)
Ns = [int(round(n)) for n in np.logspace(0, 3, 10)]
for n in Ns:
x = np.random.random(n)
x.sort()
timing['unique'].append(
timeit.timeit('m.using_unique(m.x)', 'import __main__ as m', number=1000))
timing['counter'].append(
timeit.timeit('m.using_counter(m.x)', 'import __main__ as m', number=1000))
timing['ordered_dict'].append(
timeit.timeit('m.using_ordered_dict(m.x)', 'import __main__ as m', number=1000))
timing['groupby'].append(
timeit.timeit('m.using_groupby(m.x)', 'import __main__ as m', number=1000))
ax.plot(Ns, timing['unique'], label='using_unique')
ax.plot(Ns, timing['counter'], label='using_counter')
ax.plot(Ns, timing['ordered_dict'], label='using_ordered_dict')
ax.plot(Ns, timing['groupby'], label='using_groupby')
plt.legend(loc='best')
plt.ylabel('milliseconds')
plt.xlabel('size of x')
plt.show()