Workshopping a Tier List Generator

A photo of a workshop with the title of the article overlayed.

The tier list trope has been beaten to death by this point, but I couldn’t help myself. I had to put together a script to generate one.

Table of Contents

Inspiration

As an educator, I keep tabs on a variety of sites where educators are evaluated. That includes sites like Rate My Professors and even Reddit. In fact, where I work, there are a whole host of sites that my students use to decide which classes to take:

Well, recently, I stumbled upon a Reddit threadOpens in a new tab. where someone had ranked every professor they ever had. Obviously, this is a nontrivial process. After all, I make a list post annually covering topics like the best programming languages and the best video games, where I force myself to order the items from best to worst.

What I found really interesting was that in this Reddit thread, the author talked about their methodology for ordering all the professors. To do it, they created a Python script to pit each professor in a head-to-head battle, and it was the author’s job to pick the better of the two professors. The script generated all combinations of professors and each win counted as a single point. The professors were then ordered by their scores.

Naturally, I was obsessed with this idea, and I wanted to see if I could replicate the idea myself. Even better though, I wanted to see if I could get the script to output a tier list, rather than a regular list, because I like the idea of clustering similar items. As it turns out, it’s actually not too hard to get a script to do this!

Sharing the Code

In the spirit of workshopping, I wanted to share the proof of concept:

from itertools import combinations
from random import shuffle

test = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']

# Generate list of pairs
pairs = list(combinations(test, 2))
shuffle(pairs)

# Run the comparisons
print(f"Welcome to pytier! Today, you will be asked to make {len(pairs)} comparisons.")
print("For each comparison, type 1 for the first choice or 2 for the second choice.")
scores = {item: 0 for item in test}
for pair in pairs:
    first, second = pair
    choice: int = int(input(f">> Which is better? 1) {first} or 2) {second}: "))
    winner = first if choice == 1 else second
    scores[winner] += 1

# Generate histogram from results
sorted_pairs = sorted(scores.items(), key=lambda item: item[1], reverse=True)
max_vote, min_vote = sorted_pairs[0][1], sorted_pairs[-1][1]
range_of_data = max_vote - min_vote
class_width = range_of_data / 5
categories = ["S", "A", "B", "C", "F"]
dividers = [num * class_width + min_vote for num in range(0, len(categories) + 1)]
buckets = dict(zip(categories, tuple(zip(dividers, dividers[1:]))[::-1]))
tiers = {k: [] for k in categories}
for pair in sorted_pairs:
    for tier, bucket in buckets.items():
        if bucket[0] <= pair[1] <= bucket[1]:
            tiers[tier].append(pair)

# Show histogram
print("Thanks for making your selections! Your tierlist is as follows:")
for tier, values in tiers.items():
    items = [value[0] for value in values]
    print(f"{tier}: {items}")

Breaking It Down

Broadly speaking, it takes about five steps to generate a tier list from some input list:

  1. Get a list of items from a user.
  2. Generate all combinations of pairs from the input list.
  3. Give the pairs to the user one at a time and let the user make their selections.
  4. Compute a histogram to simulate a tier list.
  5. Output the tier list to the user.

In the rest of this section, I’ll share how each of these steps are accomplished in Python.

Getting an Input List

If you take a peek at my code, you’ll see that I don’t actually prompt the user for anything. Instead, I provide a simple list of colors:

test = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']

Though, it wouldn’t be too much of a stretch to prompt the user for strings, get the list via command line arguments, or require the user pass in a file.

Generating Combinations of Pairs

While it’s possible to generate all the pairs by hand using a pair of nested loops, there are a variety of problems. For example, the pairs (red, blue) and (blue, red) are identical, so we don’t want to generate them. Instead, we make use of itertools, which has a class for generating combinations:

# Generate list of pairs
pairs = list(combinations(test, 2))
shuffle(pairs)

In this example, the combinations constructor asks for two inputs, an iterable and the size of the combinations. In this case, because we’re looking for pairs, the size of the combinations should be two.

Also, because combinations is actually a class, the output is a special object. To get it into a more useful form, we can throw the object at the list constructor. This is a pretty common idiom in Python as many of the useful built-in “functions”, such as zip and range, output special objects.

For good measure, I also shuffle the pairs using the shuffle function of the random module. That way, if you rerun the program, the pairs are not presented in the same order.

Presenting the Pairs to the User

The logic for presenting the pairs to the user is pretty straightforward. For each pair in the list of pairs, ask the user which one is better. To keep things simple, I number the choices, so the user only has to reply with 1 or 2.

# Run the comparisons
print(f"Welcome to pytier! Today, you will be asked to make {len(pairs)} comparisons.")
print("For each comparison, type 1 for the first choice or 2 for the second choice.")
scores = {item: 0 for item in test}
for pair in pairs:
    first, second = pair
    choice: int = int(input(f">> Which is better? 1) {first} or 2) {second}: "))
    winner = first if choice == 1 else second
    scores[winner] += 1

To determine the winner, I use a basic ternary to track the winner string. That string is then used as a key into a dictionary where we track points.

To ensure this code works every time, I initialize a dictionary using a dictionary comprehension. That way, every possible option is preloaded into the dictionary with a value of zero. Certainly, there are other options like defaultdict, which provide a similar functionality.

Computing a Histogram

At this point, I wasn’t entirely sure how I was going to present the results to the user. It would be fairly painless to sort the map by value and display it, but I wanted to cluster the values together into tiers.

To do that, I considered a few ideas. For example, I thought maybe I would force the structure to be a bell curve, where only the best and worst items are in the top and bottom tiers. Meanwhile, the middle of the distribution would contain the most items. Looking back, as long as you define the buckets as holding a certain percentage of the elements, it should be too hard to bin them appropriately.

However, what I decided to do instead was construct a histogram where the buckets are made from point ranges. For example, if the range of the points is 50, then I would divide that 50 up into five buckets of 10 points each. As a result, it would be possible to not have normal distribution, and I think that’s okay. When you make a tier list, I think it’s reasonable to have several “heroes” and “villains”. Ultimately, this allows for any variety of distributions to emerge.

At any rate, here’s the code that does that:

# Generate histogram from results
sorted_pairs = sorted(scores.items(), key=lambda item: item[1], reverse=True)
max_vote, min_vote = sorted_pairs[0][1], sorted_pairs[-1][1]
range_of_data = max_vote - min_vote
class_width = range_of_data / 5
categories = ["S", "A", "B", "C", "F"]
dividers = [num * class_width + min_vote for num in range(0, len(categories) + 1)]
buckets = dict(zip(categories, tuple(zip(dividers, dividers[1:]))[::-1]))
tiers = {k: [] for k in categories}
for pair in sorted_pairs:
    for tier, bucket in buckets.items():
        if bucket[0] <= pair[1] <= bucket[1]:
            tiers[tier].append(pair)

As you can see, I sort the pairs by value. Then, I get the min and max points to calculate the range of points. After that, I generate a list of dividers, where consecutive values define a bucket. Buckets are then formed by zipping the dividers to create tuples of pairs, which are then mapped to the tiers. The output map might look something like the following:

  • S: (50, 40)
  • A: (40, 30)
  • B: (30, 20)
  • C: (20, 10)
  • F: (10, 0)

After that, it’s just a matter of iterating over the items and their scores and placing them in the appropriate bucket.

Outputting the Tier List

With our dictionary already holding the results we want, it’s just a matter of looping over it:

# Show histogram
print("Thanks for making your selections! Your tierlist is as follows:")
for tier, values in tiers.items():
    items = [value[0] for value in values]
    print(f"{tier}: {items}")

After personally running this, here’s the output:

Thanks for making your selections! Your tierlist is as follows:
S: ['violet', 'indigo']
A: ['blue']
B: ['red']
C: ['green']
F: ['yellow', 'orange']

And just as predicted, there are colors I really like and colors I really don’t like. This would look a lot more weird if I forced it into a normal distribution.

What’s Next?

Something I would love to do is put together an actual UI for this. Perhaps I’ll dabble with Django to see if I can visually generate a tier list and let people make their own. As always though, this is a tool I made for myself because I turn 31 in a couple of months, and I haven’t started my annual list post. Surely, this will go to good use for that!

Anyway, it’s the holidays, and I need to pack before I head back home. So, let’s call this one for the day! As usual, you’re free to check out some of these related posts:

In addition, feel free to check out some of these other resources (#ad):

And if you want to take your support even further, check out my list of ways to grow the site. Otherwise, we’ll see you next time!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife and kid, playing Overwatch 2, Lethal Company, and Baldur's Gate 3, reading manga, watching Penguins hockey, and traveling the world.

Recent Posts