I've been hoping someone would ask this specific question on Stackoverflow, but since no-one has yet: How do I make an iterator like enumerate that ranks the scores so that ties don't increase the rank number?
For example, I want to give the rank for a sorted list of scores and names in the form, score
, name
.
scores = [(42, 'Adams'), (42, 'Douglas'), (41, 'Aaron'),
(41, 'Hall'), (39, 'Python')]
I could use enumerate to get the place for each name,
for i, (s, name) in enumerate(scores, 1):
print i, s, name
But each line would decrease ranking even if two or more were tied for the same score. This is ordinal ranking as described in Wikipedia.
1 42 Adams
2 42 Douglas
3 41 Aaron
4 41 Hall
5 39 Python
How do I accomplish this without incrementing if they're tied for score, knowing that I need the same API for the function so that it's a drop-in replacement for enumerate? This is described as standard competition ranking in Wikipedia.
Here's the enumerate
equivalency written in Python from the documentation:
def enumerate(sequence, start=0):
n = start
for elem in sequence:
yield n, elem
n += 1
I came across this specific problem when I first started where I'm currently working and was trucking through the training material, but I was frustrated as I climbed the rankings that I was given a lower ranking than others I was tied with. This was my first contribution to the code base, but tightened up a bit (because, embarrassingly, I didn't use enumerate
the first time around!)
My solution, is to use a generator I originally modeled on the enumerate equivalency code in the documentation (see question above).
def rank(sequence, start=0): #as in enumerate()
'''
rank generator function handles score ties,
same API as enumerate for drop-in replacement
assumes given a sorted sequence of two-tuples
of scores and names (or some other description)
'''
rank = start
previous_score = None # need to initialize
for n, (score, name) in enumerate(sequence, start):
if score != previous_score: #rank up if not tie
rank = n
previous_score = score
yield rank, (score, name) #rank unmodified if a tie.
And usage:
for i, (s, name) in rank(scores, 1):
print i, s, name
would print:
1 42 Adams
1 42 Douglas
3 41 Aaron
3 41 Hall
5 39 Python