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 thread 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:
- Get a list of items from a user.
- Generate all combinations of pairs from the input list.
- Give the pairs to the user one at a time and let the user make their selections.
- Compute a histogram to simulate a tier list.
- 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:
- Write a Python Script to Autogenerate Google Form Responses
- Plex Organizer Script: Manage Files on a Plex Server
- How to Make a Python Script Shortcut with Arguments: Batch, Bash, and More
In addition, feel free to check out some of these other resources (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
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!
Recent Posts
Canvas is a learning management system that I use under the name Carmen. It mostly does its job correctly, but it often boggles my mind how little it seems to map to the education environment. So, I...
Okay, you caught me. This is a bit of clickbait, but it's in reaction to some real conversations folks are having about the role of the GRE in admissions. Let me vent a minute.