Search code examples
pythonalgorithmranking

Drop-in replacement for enumerate that ranks the elements being passed so that ties don't increase rank?


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

Solution

  • 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