I would suggest a vectorized approach making heavy usage of NumPy functions which might be beneficial as we are focusing on performance here. The steps to solve our case could be put together as shown below -
# Get counts for each label in input array
counts = np.bincount(X)
# Setup ID array that will ultimately when cumulatively summed would give us
# such a ramp-like array corresponding to an otherwise sorted input array
id_arr = np.ones(len(X),dtype=int)
id_arr[0] = 0
id_arr[counts[:-1].cumsum()] = -counts[:-1]+1
# Perform cumlative summation and go back to original order using double argsorting
out = id_arr.cumsum()[np.argsort(X,kind='mergesort').argsort()]
Please note that the input expected would be a NumPy array.
Benchmarking
Let's test out all the approaches posted thus far to solve the problem. I have compiled all the solutions as functions with names attributed to their respective author names. Listed below are the compiled codes -
def times_so_far_hackerman(ls):
out = [0]*len(ls)
for i in xrange(len(ls)):
out[i] = ls[:i].count(ls[i])
return out
def times_so_far_JoeWallis(list_):
counted = defaultdict(int)
for v in list_:
counted[v] += 1
yield counted[v]-1
def times_so_far_generator(nums):
counter = Counter()
for num in nums:
counter[num] += 1
yield counter[num]-1
def times_so_far_mleyfman(nums):
return list(times_so_far_generator(nums))
def times_so_far_Caridorc(xs):
return [xs[:index].count(x) for index, x in enumerate(xs)]
def times_so_far_vectorized(X):
counts = np.bincount(X)
id_arr = np.ones(len(X),dtype=int)
id_arr[0] = 0
id_arr[counts[:-1].cumsum()] = -counts[:-1]+1
return id_arr.cumsum()[np.argsort(X,kind='mergesort').argsort()]
Now, let's start timings these. Let's start with a relatively decent sized list of numbers :
In [9]: %timeit times_so_far(x)
...: %timeit times_so_far_hackerman(x)
...: %timeit list(times_so_far_JoeWallis(x))
...: %timeit times_so_far_mleyfman(x)
...: %timeit times_so_far_Caridorc(x)
...: %timeit times_so_far_vectorized(np.array(x))
...:
100 loops, best of 3: 3.27 ms per loop
1 loops, best of 3: 1.3 s per loop
100 loops, best of 3: 3.03 ms per loop
100 loops, best of 3: 8.32 ms per loop
1 loops, best of 3: 1.36 s per loop
100 loops, best of 3: 2.44 ms per loop
So, the three fastest approaches are :
times_so_far()
times_so_far_JoeWallis()
times_so_far_vectorized()
Let's use a bigger sized array to test out those three :
In [10]: # Input array with random elements between 0 amnd 100
...: x = np.random.randint(0,500,(50000)).tolist()
In [11]: %timeit times_so_far(x)
...: %timeit list(times_so_far_JoeWallis(x))
...: %timeit times_so_far_vectorized(np.array(x))
...:
100 loops, best of 3: 19.7 ms per loop
100 loops, best of 3: 17.7 ms per loop
100 loops, best of 3: 15.6 ms per loop
Now, if the input is already a NumPy array, times_so_far_vectorized
can shave off the runtime spent on converting from a list to an array. Let's test out how that works out :
In [12]: %timeit times_so_far(x)
...: %timeit list(times_so_far_JoeWallis(x))
...: X = np.array(x)
...: %timeit times_so_far_vectorized(X)
...:
100 loops, best of 3: 19.5 ms per loop
100 loops, best of 3: 17.6 ms per loop
100 loops, best of 3: 11.3 ms per loop
So, there's a pretty good performance boost for times_so_far_vectorized
over the other approaches!