Overwatch is a 2016 team-based multiplayer first-person shooter game by Blizzard Entertainment. Released in May 2016, its open beta drew in nearly 10 million players. It has received universal acclaim from critics, who praised the game for its accessibility, the diverse appeal of its hero characters, its cartoonish art style, and enjoyable gameplay. The game featured several different game modes, primarily designed around squad-based combat with two opposing teams of six players. Players selected one of over two dozen pre-made hero characters from one of three class types: Damage heroes whose primary focus is dealing damage, tank heroes who can absorb damage, and support heroes who provide healing and buffs for their teammates. Players can change their hero during the course of a match, as the goal of Overwatch's design was to encourage dynamic team compositions to adapt to the situation.
As with most games, Overwatch developed a meta game. Certain characters simply outclassed other characters and had higher or lower winrates to prove it. That being said, certain characters also played better the more skilled the player controlling them was, thus their win rate may have been higher or lower depending on the rank. Using data provided from two websites, esportstales.com and overbuff, we can create a ranking of the heroes with the best win rates
Overwatch is played competitively with a skill rating system with 7 divisions ranging from Bronze (lowest) to Grandmaster (highest). Skill Rating (SR) has a broad range from 0 to 5000 and determines a players division placement. The formula for determining SR is mostly kept secret, but generally players lose or gain SR based on whether they win or lose a game.
On October 4, 2022, blizzard released Overwatch 2, the sequel to the original game which kept many of the same mechanics as its predecessor, but changed a handful of things such as many character reworks, and reducing the size of teams from 6 to 5. With the servers on the original Overwatch closed, players have been looking to optimize their gameplay and pick the best heroes for this new game.
In this tutorial, our goal is to tidy the in-game stats, analyze which heroes are the strongest for certain time periods, and develop a model that predicts based on Overwatch 1 and 2 statistics, which heroes were either buffed or nerfed following the transition to Overwatch 2.
# Overbuff data from GitHub CSV online (NOTE: Data is from May 2020).
import requests
import pandas as pd
# URL: https://github.com/jamie-ralph/overbuff-webscrape/blob/master/all_data.csv
overbuff = pd.read_csv("https://raw.githubusercontent.com/jamie-ralph/overbuff-webscrape/master/all_data.csv", sep = ',')
# Dropping the first column as it is not needed.
overbuff = overbuff.drop('Unnamed: 0', axis = 1)
# Creating a new dataframe for PC only.
overbuff_pc = overbuff.loc[overbuff['Platform'] == 'PC']
# Renaming columns to keep the same column-naming schema.
overbuff_pc = overbuff_pc.rename(columns = {'Pick_rate': 'Pick %', 'Win_rate': 'Win %', 'Tie_Rate': 'Tie %', 'On_fire': 'On Fire %'})
# Creating a unique list of heroes in Overwatch 1 and sorts them in alphabetical order.
hero_list = []
for i in range(256):
if overbuff_pc['Hero'][i] not in hero_list:
hero_list.append(overbuff_pc['Hero'][i])
hero_list.sort()
display(overbuff_pc) # NOTE: Displays data for PC. This contains each rank separately, including all ranks combined.
# Creating a new dataframe for PSN (Playstation) only.
overbuff_psn = overbuff.loc[overbuff['Platform'] == 'PSN']
# Creating a new dataframe for Xbox only.
overbuff_xbox = overbuff.loc[overbuff['Platform'] == 'XBox']
# Creating a new dataframe for PC by each rank.
overbuff_pc_all = overbuff_pc.loc[overbuff_pc['Rank'] == 'All']
overbuff_pc_bronze = overbuff_pc.loc[overbuff_pc['Rank'] == 'Bronze']
overbuff_pc_silver = overbuff_pc.loc[overbuff_pc['Rank'] == 'Silver']
overbuff_pc_gold = overbuff_pc.loc[overbuff_pc['Rank'] == 'Gold']
overbuff_pc_diamond = overbuff_pc.loc[overbuff_pc['Rank'] == 'Diamond']
overbuff_pc_master = overbuff_pc.loc[overbuff_pc['Rank'] == 'Master']
overbuff_pc_grandmaster = overbuff_pc.loc[overbuff_pc['Rank'] == 'Grandmaster']
display(overbuff_pc_all) # NOTE: Shows PC data for all ranks combined.
# The same dataframe of all ranks as above just sorted alphabetically by hero.
overbuff_pc_all_sorted = overbuff_pc_all.sort_values('Hero')
# Creating a new dataframe for PC by each role (NOTE: this combines every rank)
overbuff_pc_tank = overbuff_pc.loc[overbuff_pc['Role'] == 'TANK']
overbuff_pc_support = overbuff_pc.loc[overbuff_pc['Role'] == 'SUPPORT']
overbuff_pc_offense = overbuff_pc.loc[overbuff_pc['Role'] == 'OFFENSE']
overbuff_pc_defense = overbuff_pc.loc[overbuff_pc['Role'] == 'DEFENSE']
# Resetting indices for PC tank data (for cleaner display purposes).
overbuff_pc_tank = overbuff_pc_tank.reset_index()
# Dropping old index table.
overbuff_pc_tank = overbuff_pc_tank.drop('index', axis = 1)
display(overbuff_pc_tank) # NOTE: Shows PC data for the role 'TANK'.
Hero | Role | Pick % | Win % | Tie % | On Fire % | Rank | Date | Platform | |
---|---|---|---|---|---|---|---|---|---|
0 | Reinhardt | TANK | 12.08 | 52.10 | 2.61 | 7.15 | All | 2020-05-24 11:54:16.967151 | PC |
1 | Ana | SUPPORT | 10.80 | 49.92 | 2.36 | 9.81 | All | 2020-05-24 11:54:16.967151 | PC |
2 | Moira | SUPPORT | 8.28 | 49.95 | 2.45 | 10.52 | All | 2020-05-24 11:54:16.967151 | PC |
3 | Zarya | TANK | 7.51 | 52.90 | 2.47 | 12.39 | All | 2020-05-24 11:54:16.967151 | PC |
4 | Mercy | SUPPORT | 6.56 | 51.32 | 2.42 | 2.46 | All | 2020-05-24 11:54:16.967151 | PC |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
251 | Junkrat | DEFENSE | 0.72 | 57.72 | 0.73 | 7.28 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
252 | Symmetra | SUPPORT | 0.51 | 50.53 | 1.04 | 6.50 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
253 | Sombra | OFFENSE | 0.44 | 62.05 | 0.60 | 13.35 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
254 | Bastion | DEFENSE | 0.22 | 49.41 | 0.00 | 5.88 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
255 | Reaper | OFFENSE | 0.21 | 53.75 | 1.23 | 11.46 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
256 rows × 9 columns
Hero | Role | Pick % | Win % | Tie % | On Fire % | Rank | Date | Platform | |
---|---|---|---|---|---|---|---|---|---|
0 | Reinhardt | TANK | 12.08 | 52.10 | 2.61 | 7.15 | All | 2020-05-24 11:54:16.967151 | PC |
1 | Ana | SUPPORT | 10.80 | 49.92 | 2.36 | 9.81 | All | 2020-05-24 11:54:16.967151 | PC |
2 | Moira | SUPPORT | 8.28 | 49.95 | 2.45 | 10.52 | All | 2020-05-24 11:54:16.967151 | PC |
3 | Zarya | TANK | 7.51 | 52.90 | 2.47 | 12.39 | All | 2020-05-24 11:54:16.967151 | PC |
4 | Mercy | SUPPORT | 6.56 | 51.32 | 2.42 | 2.46 | All | 2020-05-24 11:54:16.967151 | PC |
5 | McCree | OFFENSE | 4.72 | 47.43 | 1.79 | 8.60 | All | 2020-05-24 11:54:16.967151 | PC |
6 | Lúcio | SUPPORT | 4.36 | 51.76 | 2.06 | 11.86 | All | 2020-05-24 11:54:16.967151 | PC |
7 | Ashe | OFFENSE | 3.83 | 53.39 | 2.20 | 9.85 | All | 2020-05-24 11:54:16.967151 | PC |
8 | Roadhog | TANK | 3.40 | 49.70 | 1.75 | 9.34 | All | 2020-05-24 11:54:16.967151 | PC |
9 | Sigma | TANK | 3.07 | 50.85 | 2.22 | 12.90 | All | 2020-05-24 11:54:16.967151 | PC |
10 | Genji | OFFENSE | 2.93 | 50.54 | 2.17 | 11.95 | All | 2020-05-24 11:54:16.967151 | PC |
11 | Soldier: 76 | OFFENSE | 2.38 | 49.48 | 1.86 | 9.78 | All | 2020-05-24 11:54:16.967151 | PC |
12 | Junkrat | DEFENSE | 2.15 | 52.12 | 2.65 | 11.61 | All | 2020-05-24 11:54:16.967151 | PC |
13 | Wrecking Ball | TANK | 2.15 | 50.94 | 1.86 | 3.02 | All | 2020-05-24 11:54:16.967151 | PC |
14 | D.Va | TANK | 2.05 | 48.38 | 2.11 | 6.53 | All | 2020-05-24 11:54:16.967151 | PC |
15 | Echo | OFFENSE | 2.01 | 50.35 | 1.94 | 7.51 | All | 2020-05-24 11:54:16.967151 | PC |
16 | Winston | TANK | 1.99 | 47.72 | 1.75 | 6.19 | All | 2020-05-24 11:54:16.967151 | PC |
17 | Reaper | OFFENSE | 1.94 | 50.56 | 1.86 | 13.61 | All | 2020-05-24 11:54:16.967151 | PC |
18 | Brigitte | SUPPORT | 1.93 | 55.70 | 1.79 | 7.04 | All | 2020-05-24 11:54:16.967151 | PC |
19 | Baptiste | SUPPORT | 1.83 | 46.50 | 2.59 | 11.44 | All | 2020-05-24 11:54:16.967151 | PC |
20 | Doomfist | OFFENSE | 1.63 | 52.35 | 1.88 | 7.41 | All | 2020-05-24 11:54:16.967151 | PC |
21 | Orisa | TANK | 1.62 | 49.77 | 1.95 | 6.77 | All | 2020-05-24 11:54:16.967151 | PC |
22 | Hanzo | DEFENSE | 1.60 | 47.44 | 2.09 | 10.41 | All | 2020-05-24 11:54:16.967151 | PC |
23 | Tracer | OFFENSE | 1.52 | 49.48 | 1.50 | 7.16 | All | 2020-05-24 11:54:16.967151 | PC |
24 | Pharah | OFFENSE | 1.51 | 54.15 | 1.85 | 14.75 | All | 2020-05-24 11:54:16.967151 | PC |
25 | Zenyatta | SUPPORT | 1.49 | 53.46 | 1.90 | 8.28 | All | 2020-05-24 11:54:16.967151 | PC |
26 | Widowmaker | DEFENSE | 1.28 | 50.44 | 1.49 | 6.62 | All | 2020-05-24 11:54:16.967151 | PC |
27 | Torbjörn | DEFENSE | 1.16 | 57.40 | 2.79 | 7.69 | All | 2020-05-24 11:54:16.967151 | PC |
28 | Mei | DEFENSE | 0.77 | 48.07 | 2.48 | 6.75 | All | 2020-05-24 11:54:16.967151 | PC |
29 | Symmetra | SUPPORT | 0.66 | 58.08 | 2.90 | 9.13 | All | 2020-05-24 11:54:16.967151 | PC |
30 | Sombra | OFFENSE | 0.47 | 47.34 | 1.84 | 12.21 | All | 2020-05-24 11:54:16.967151 | PC |
31 | Bastion | DEFENSE | 0.31 | 55.00 | 2.38 | 16.33 | All | 2020-05-24 11:54:16.967151 | PC |
Hero | Role | Pick % | Win % | Tie % | On Fire % | Rank | Date | Platform | |
---|---|---|---|---|---|---|---|---|---|
0 | Reinhardt | TANK | 12.08 | 52.10 | 2.61 | 7.15 | All | 2020-05-24 11:54:16.967151 | PC |
1 | Zarya | TANK | 7.51 | 52.90 | 2.47 | 12.39 | All | 2020-05-24 11:54:16.967151 | PC |
2 | Roadhog | TANK | 3.40 | 49.70 | 1.75 | 9.34 | All | 2020-05-24 11:54:16.967151 | PC |
3 | Sigma | TANK | 3.07 | 50.85 | 2.22 | 12.90 | All | 2020-05-24 11:54:16.967151 | PC |
4 | Wrecking Ball | TANK | 2.15 | 50.94 | 1.86 | 3.02 | All | 2020-05-24 11:54:16.967151 | PC |
5 | D.Va | TANK | 2.05 | 48.38 | 2.11 | 6.53 | All | 2020-05-24 11:54:16.967151 | PC |
6 | Winston | TANK | 1.99 | 47.72 | 1.75 | 6.19 | All | 2020-05-24 11:54:16.967151 | PC |
7 | Orisa | TANK | 1.62 | 49.77 | 1.95 | 6.77 | All | 2020-05-24 11:54:16.967151 | PC |
8 | Reinhardt | TANK | 9.03 | 48.86 | 2.71 | 7.48 | Bronze | 2020-05-24 11:55:05.315769 | PC |
9 | Zarya | TANK | 4.92 | 48.83 | 2.59 | 8.94 | Bronze | 2020-05-24 11:55:05.315769 | PC |
10 | D.Va | TANK | 4.04 | 44.32 | 2.59 | 6.29 | Bronze | 2020-05-24 11:55:05.315769 | PC |
11 | Sigma | TANK | 3.59 | 47.81 | 2.42 | 10.79 | Bronze | 2020-05-24 11:55:05.315769 | PC |
12 | Orisa | TANK | 3.45 | 48.09 | 2.51 | 5.80 | Bronze | 2020-05-24 11:55:05.315769 | PC |
13 | Roadhog | TANK | 3.03 | 45.85 | 1.57 | 7.37 | Bronze | 2020-05-24 11:55:05.315769 | PC |
14 | Wrecking Ball | TANK | 1.99 | 47.83 | 2.21 | 2.62 | Bronze | 2020-05-24 11:55:05.315769 | PC |
15 | Winston | TANK | 1.94 | 46.27 | 1.47 | 5.53 | Bronze | 2020-05-24 11:55:05.315769 | PC |
16 | Reinhardt | TANK | 10.68 | 51.15 | 2.45 | 6.97 | Silver | 2020-05-24 11:55:53.912738 | PC |
17 | Zarya | TANK | 6.32 | 50.62 | 2.27 | 9.63 | Silver | 2020-05-24 11:55:53.912738 | PC |
18 | Sigma | TANK | 3.29 | 49.51 | 2.30 | 11.47 | Silver | 2020-05-24 11:55:53.912738 | PC |
19 | Roadhog | TANK | 3.11 | 46.85 | 1.78 | 7.78 | Silver | 2020-05-24 11:55:53.912738 | PC |
20 | D.Va | TANK | 2.88 | 47.58 | 2.23 | 6.44 | Silver | 2020-05-24 11:55:53.912738 | PC |
21 | Orisa | TANK | 2.25 | 49.21 | 1.97 | 6.06 | Silver | 2020-05-24 11:55:53.912738 | PC |
22 | Winston | TANK | 2.04 | 46.44 | 1.93 | 5.65 | Silver | 2020-05-24 11:55:53.912738 | PC |
23 | Wrecking Ball | TANK | 1.87 | 49.78 | 2.07 | 2.73 | Silver | 2020-05-24 11:55:53.912738 | PC |
24 | Reinhardt | TANK | 11.71 | 51.44 | 2.65 | 7.08 | Gold | 2020-05-24 11:56:43.966592 | PC |
25 | Zarya | TANK | 7.36 | 51.99 | 2.41 | 11.44 | Gold | 2020-05-24 11:56:43.966592 | PC |
26 | Roadhog | TANK | 3.33 | 48.78 | 1.87 | 8.85 | Gold | 2020-05-24 11:56:43.966592 | PC |
27 | Sigma | TANK | 3.05 | 51.00 | 2.13 | 12.78 | Gold | 2020-05-24 11:56:43.966592 | PC |
28 | D.Va | TANK | 2.15 | 48.59 | 2.09 | 6.89 | Gold | 2020-05-24 11:56:43.966592 | PC |
29 | Winston | TANK | 2.00 | 47.03 | 1.72 | 6.16 | Gold | 2020-05-24 11:56:43.966592 | PC |
30 | Wrecking Ball | TANK | 1.92 | 49.80 | 1.99 | 2.80 | Gold | 2020-05-24 11:56:43.966592 | PC |
31 | Orisa | TANK | 1.50 | 50.30 | 2.02 | 7.04 | Gold | 2020-05-24 11:56:43.966592 | PC |
32 | Reinhardt | TANK | 12.96 | 52.26 | 2.67 | 7.31 | Platinum | 2020-05-24 11:57:34.194215 | PC |
33 | Zarya | TANK | 8.32 | 53.60 | 2.53 | 13.17 | Platinum | 2020-05-24 11:57:34.194215 | PC |
34 | Roadhog | TANK | 3.70 | 50.96 | 1.75 | 10.12 | Platinum | 2020-05-24 11:57:34.194215 | PC |
35 | Sigma | TANK | 2.69 | 51.17 | 2.27 | 13.64 | Platinum | 2020-05-24 11:57:34.194215 | PC |
36 | Wrecking Ball | TANK | 2.20 | 51.69 | 1.76 | 3.18 | Platinum | 2020-05-24 11:57:34.194215 | PC |
37 | Winston | TANK | 1.88 | 47.48 | 1.61 | 6.50 | Platinum | 2020-05-24 11:57:34.194215 | PC |
38 | D.Va | TANK | 1.57 | 49.53 | 1.85 | 6.69 | Platinum | 2020-05-24 11:57:34.194215 | PC |
39 | Orisa | TANK | 1.14 | 50.83 | 1.56 | 7.22 | Platinum | 2020-05-24 11:57:34.194215 | PC |
40 | Reinhardt | TANK | 13.57 | 53.54 | 2.66 | 7.13 | Diamond | 2020-05-24 11:58:23.985963 | PC |
41 | Zarya | TANK | 8.74 | 55.04 | 2.63 | 14.53 | Diamond | 2020-05-24 11:58:23.985963 | PC |
42 | Roadhog | TANK | 3.66 | 52.05 | 1.55 | 10.44 | Diamond | 2020-05-24 11:58:23.985963 | PC |
43 | Sigma | TANK | 2.97 | 53.00 | 2.16 | 14.27 | Diamond | 2020-05-24 11:58:23.985963 | PC |
44 | Wrecking Ball | TANK | 2.31 | 53.23 | 1.73 | 3.20 | Diamond | 2020-05-24 11:58:23.985963 | PC |
45 | Winston | TANK | 1.99 | 49.35 | 1.92 | 6.54 | Diamond | 2020-05-24 11:58:23.985963 | PC |
46 | D.Va | TANK | 1.35 | 48.66 | 2.12 | 6.26 | Diamond | 2020-05-24 11:58:23.985963 | PC |
47 | Orisa | TANK | 1.14 | 49.17 | 1.36 | 7.33 | Diamond | 2020-05-24 11:58:23.985963 | PC |
48 | Reinhardt | TANK | 13.46 | 54.65 | 2.34 | 6.76 | Master | 2020-05-24 11:59:13.886732 | PC |
49 | Zarya | TANK | 7.38 | 54.39 | 2.48 | 14.34 | Master | 2020-05-24 11:59:13.886732 | PC |
50 | Sigma | TANK | 3.38 | 49.71 | 1.76 | 12.66 | Master | 2020-05-24 11:59:13.886732 | PC |
51 | Roadhog | TANK | 3.05 | 51.43 | 1.87 | 9.84 | Master | 2020-05-24 11:59:13.886732 | PC |
52 | Wrecking Ball | TANK | 2.97 | 50.90 | 1.46 | 3.30 | Master | 2020-05-24 11:59:13.886732 | PC |
53 | Winston | TANK | 2.32 | 51.52 | 1.99 | 6.13 | Master | 2020-05-24 11:59:13.886732 | PC |
54 | Orisa | TANK | 1.86 | 48.13 | 2.55 | 7.01 | Master | 2020-05-24 11:59:13.886732 | PC |
55 | D.Va | TANK | 1.41 | 51.03 | 2.02 | 4.87 | Master | 2020-05-24 11:59:13.886732 | PC |
56 | Reinhardt | TANK | 10.08 | 55.29 | 2.46 | 7.06 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
57 | Sigma | TANK | 5.92 | 51.74 | 2.64 | 13.20 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
58 | Zarya | TANK | 5.43 | 57.78 | 2.38 | 15.29 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
59 | Wrecking Ball | TANK | 4.15 | 52.30 | 1.65 | 3.50 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
60 | Orisa | TANK | 3.85 | 50.63 | 2.40 | 6.79 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
61 | Winston | TANK | 2.58 | 53.08 | 1.85 | 5.78 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
62 | D.Va | TANK | 2.45 | 52.97 | 1.94 | 5.12 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
63 | Roadhog | TANK | 2.23 | 51.62 | 1.18 | 8.81 | Grandmaster | 2020-05-24 12:00:03.487597 | PC |
The data set above is a csv file from overbuff.com. The table is divided into columns that are role, pick rate, win rate, rank, and the hero name as the relevant columns. Using the table provided, we created tables dividing the heroes based on their rank and and their roles. This data is taken from May 2020 data, and is from Overwatch 1.
# SECOND, Overbuff Grandmaster data from Overbuff, from GitHub CSV online (NOTE: Data is from Overbuff.com December 2021).
import requests
import pandas as pd
# URL: https://github.com/rshah5324/rshah5324.github.io/blob/main/overbuff2021grandmaster.csv
overbuff_2021 = pd.read_csv("https://raw.githubusercontent.com/rshah5324/rshah5324.github.io/main/overbuff2021grandmaster.csv", sep=',')
# Dropping rows 31-35 as these characters are exclusive to Overwatch 2 and also lack enough data still.
overbuff_2021 = overbuff_2021.drop(range(32,36))
# Sorting the dataframe alphabetically by hero.
overbuff_2021 = overbuff_2021.sort_values('Hero')
# Reindexing the dataframe to start at 1 and stop at 32.
overbuff_2021.index = pd.RangeIndex(start = 1, stop = 33, step = 1)
for i in range(1,33):
# Changing 'Hero' column to fix hero names.
overbuff_2021.loc[i,'Hero'] = hero_list[i-1]
# Changing 'Pick Rate' column values to be floats.
str = overbuff_2021.loc[i,'Pick Rate'][:-1]
overbuff_2021.loc[i,'Pick Rate'] = float(str)
# Changing 'Win Rate' column values to be floats.
str = overbuff_2021.loc[i,'Win Rate'][:-1]
overbuff_2021.loc[i,'Win Rate'] = float(str)
# Renamig 'Pick Rate' column to 'Pick %' and 'Win Rate' column to 'Win %' (keeping column naming consistent).
overbuff_2021 = overbuff_2021.rename(columns = {'Pick Rate': 'Pick %', 'Win Rate': 'Win %'})
# Dropping old index column from CSV file.
overbuff_2021 = overbuff_2021.drop('Unnamed: 0', axis=1)
# Adding roles to every hero.
overbuff_2021.insert(1,'Role', overbuff_pc_all_sorted['Role'].tolist())
# Printing cleaned up Overbuff dataframe.
display(overbuff_2021)
Hero | Role | Pick % | Win % | KDA | |
---|---|---|---|---|---|
1 | Ana | SUPPORT | 9.05 | 48.28 | 1.99 |
2 | Ashe | OFFENSE | 2.12 | 50.36 | 2.91 |
3 | Baptiste | SUPPORT | 2.41 | 47.28 | 2.15 |
4 | Bastion | DEFENSE | 1.04 | 48.32 | 2.81 |
5 | Brigitte | SUPPORT | 1.81 | 52.96 | 2.05 |
6 | D.Va | TANK | 3.16 | 45.1 | 2.74 |
7 | Doomfist | OFFENSE | 3.06 | 48.61 | 4.87 |
8 | Echo | OFFENSE | 1.43 | 49.64 | 2.13 |
9 | Genji | OFFENSE | 0.79 | 50.96 | 3.11 |
10 | Hanzo | DEFENSE | 3.3 | 48.64 | 2.74 |
11 | Junkrat | DEFENSE | 2.88 | 46.69 | 2.82 |
12 | Lúcio | SUPPORT | 2.67 | 49.54 | 2.59 |
13 | McCree | OFFENSE | 3.72 | 52.1 | 2.41 |
14 | Mei | DEFENSE | 1.1 | 48.63 | 2.96 |
15 | Mercy | SUPPORT | 6.72 | 48.21 | 0.54 |
16 | Moira | SUPPORT | 4.93 | 50.57 | 3.76 |
17 | Orisa | TANK | 2.28 | 46.7 | 3.06 |
18 | Pharah | OFFENSE | 1.78 | 51.49 | 3.09 |
19 | Reaper | OFFENSE | 1.73 | 49.64 | 2.99 |
20 | Reinhardt | TANK | 3.75 | 51.48 | 2.39 |
21 | Roadhog | TANK | 3.0 | 46.93 | 3.57 |
22 | Sigma | TANK | 1.91 | 51.97 | 3.97 |
23 | Soldier: 76 | OFFENSE | 3.19 | 47.7 | 3.16 |
24 | Sombra | OFFENSE | 1.27 | 44.44 | 3.20 |
25 | Symmetra | SUPPORT | 1.06 | 54.77 | 3.21 |
26 | Torbjörn | DEFENSE | 1.13 | 53.39 | 3.59 |
27 | Tracer | OFFENSE | 1.55 | 45.53 | 2.72 |
28 | Widowmaker | DEFENSE | 2.21 | 44.39 | 2.39 |
29 | Winston | TANK | 1.86 | 50.33 | 3.26 |
30 | Wrecking Ball | TANK | 1.28 | 50.3 | 3.34 |
31 | Zarya | TANK | 3.01 | 48.71 | 4.11 |
32 | Zenyatta | SUPPORT | 3.89 | 51.77 | 2.66 |
The data set above is a csv file from overbuff.com. The table is divided into columns that are role, pick rate, win rate, rank, and the hero name as the relevant columns. Using the table provided, we created tables dividing the heroes based on their rank and and their roles. This data is taken from December 2021 data, and is from Overwatch 1.
# THIRD, importing necessary packages that we will use and data from esportstales.com (NOTE: Data is from April 2022)
import pandas as pd
import plotly.io as pio
pio.renderers.default='notebook'
pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 50)
pd.set_option('display.width', 300)
# Getting our data from esportstales.com (NOTE: Data is from April 2022)
# esportstales URL
esports = pd.read_html('https://www.esportstales.com/overwatch/tier-list-and-most-played-heroes')
# The esports April 2022 Most Popular Hero by Rank
esports_rank = esports[0]
# Renaming '#' to 'Popularity Ranking' to make the column name more descriptive.
esports_rank = esports_rank.rename(columns = {'#': 'Popularity Ranking'})
# Displaying the first table (Most Popular Hero by Rank)
display(esports_rank)
# The following esports April 2022 'tiers' display the pick % and the win % (they are just the Grandmaster data, NOT Master, Diamond..etc)/
esports_tier1 = esports[1]
esports_tier2 = esports[2]
esports_tier3 = esports[3]
esports_tier4 = esports[4]
# Creating a new column, 'Tier', and displaying the respective tier for each hero.
esports_tier1['Tier'] = 1
esports_tier2['Tier'] = 2
esports_tier3['Tier'] = 3
esports_tier4['Tier'] = 4
# Combining the different tier tables into one large table with every hero in Overwatch 1.
frames = [esports_tier1, esports_tier2, esports_tier3, esports_tier4]
df1 = pd.concat(frames)
df1 = pd.DataFrame(df1)
# Renaming the columns 'Name' to 'Hero Name', 'Pick%' to 'Pick %', and 'Win%' to 'Win %'.
df1 = df1.rename(columns = {'Hero': 'Delete','Name': 'Hero', 'Pick%': 'Pick %', 'Win%': 'Win %'})
# Dropping the 'Hero' column (Removing redundancy).
df1 = df1.drop('Delete', axis = 1)
# Sorting the table in alphabetical order by hero name.
df1 = df1.sort_values('Hero')
# Resetting the indices on the far left to make them start from 1.
df1.index = pd.RangeIndex(start = 1, stop = 33, step = 1)
# Change Cassidy to McCree
df1.Hero = df1.Hero.apply(lambda x: 'McCree' if 'Cassidy' in x else x)
# Printing out the dataframe (NOTE****, this is DATA from GRANDMASTER ONLY***).
df1 = df1.sort_values('Hero')
# Resetting the indices on the far left to make them start from 1.
df1.index = pd.RangeIndex(start = 1, stop = 33, step = 1)
# Adding roles to dataframe.
df1.insert(1, 'Role', overbuff_pc_all_sorted['Role'].tolist())
# Sorting by win percentage.
esports_win_percent = df1.sort_values('Win %', ascending = False)
# Sorting by pick percentage.
esports_pick_percent = df1.sort_values('Pick %', ascending = False)
# Sorting by hero name alphabetically.
esports_name_order = df1.sort_values('Hero')
# Stripping '%' from Win % and making it a float.
esports_name_order['Win %'] = esports_name_order['Win %'].str.rstrip('%').astype('float')
# Stripping '%' from Pick % and making it a float.
esports_name_order['Pick %'] = esports_name_order['Pick %'].str.rstrip('%').astype('float')
# Displaying order by name.
display(esports_name_order)
Popularity Ranking | Bronze | Silver | Gold | Platinum | Diamond | Master | Grandmaster | |
---|---|---|---|---|---|---|---|---|
0 | 1 | Mercy | Ana | Ana | Ana | Ana | Ana | Sigma |
1 | 2 | Ana | Reinhardt | Reinhardt | Reinhardt | Reinhardt | Mercy | Roadhog |
2 | 3 | Reinhardt | Mercy | Mercy | Mercy | Roadhog | Roadhog | Wrecking Ball |
3 | 4 | Moira | Moira | Soldier: 76 | Roadhog | Mercy | Sigma | Zenyatta |
4 | 5 | Soldier: 76 | Soldier: 76 | Zarya | Zarya | Cassidy | Reinhardt | Ana |
5 | 6 | D.Va | Zarya | Cassidy | Cassidy | Zarya | Baptiste | Baptiste |
6 | 7 | Junkrat | Cassidy | Roadhog | Soldier: 76 | Sigma | Zenyatta | D.Va |
7 | 8 | Cassidy | Roadhog | Moira | Hanzo | Soldier: 76 | Cassidy | Tracer |
8 | 9 | Lúcio | Baptiste | Baptiste | Sigma | D.Va | D.Va | Brigitte |
9 | 10 | Baptiste | D.Va | Hanzo | Baptiste | Zenyatta | Wrecking Ball | Mercy |
10 | 11 | Zarya | Junkrat | Sigma | D.Va | Baptiste | Tracer | Lúcio |
11 | 12 | Hanzo | Hanzo | D.Va | Zenyatta | Hanzo | Lúcio | Reinhardt |
12 | 13 | Sigma | Lúcio | Lúcio | Genji | Winston | Brigitte | Orisa |
13 | 14 | Roadhog | Sigma | Zenyatta | Lúcio | Lúcio | Hanzo | Cassidy |
14 | 15 | Reaper | Zenyatta | Genji | Winston | Genji | Soldier: 76 | Zarya |
Hero | Role | Pick % | Win % | Tier | |
---|---|---|---|---|---|
1 | Ana | SUPPORT | 6.06 | 53.54 | 1 |
2 | Ashe | OFFENSE | 2.57 | 52.01 | 3 |
3 | Baptiste | SUPPORT | 6.05 | 52.73 | 1 |
4 | Bastion | DEFENSE | 0.25 | 53.49 | 4 |
5 | Brigitte | SUPPORT | 5.05 | 53.82 | 2 |
6 | D.Va | TANK | 5.55 | 52.65 | 1 |
7 | Doomfist | OFFENSE | 1.80 | 55.85 | 3 |
8 | Echo | OFFENSE | 1.56 | 53.60 | 3 |
9 | Genji | OFFENSE | 1.32 | 56.89 | 4 |
10 | Hanzo | DEFENSE | 2.70 | 50.82 | 3 |
11 | Junkrat | DEFENSE | 0.86 | 54.03 | 4 |
12 | Lúcio | SUPPORT | 4.58 | 55.30 | 2 |
13 | McCree | OFFENSE | 2.93 | 50.27 | 3 |
14 | Mei | DEFENSE | 0.29 | 51.00 | 4 |
15 | Mercy | SUPPORT | 4.60 | 53.26 | 2 |
16 | Moira | SUPPORT | 1.12 | 53.56 | 4 |
17 | Orisa | TANK | 3.15 | 52.48 | 3 |
18 | Pharah | OFFENSE | 0.59 | 53.38 | 4 |
19 | Reaper | OFFENSE | 0.45 | 49.21 | 4 |
20 | Reinhardt | TANK | 4.12 | 55.19 | 2 |
21 | Roadhog | TANK | 7.17 | 52.21 | 1 |
22 | Sigma | TANK | 7.19 | 54.75 | 1 |
23 | Soldier: 76 | OFFENSE | 2.17 | 53.41 | 3 |
24 | Sombra | OFFENSE | 0.26 | 56.11 | 4 |
25 | Symmetra | SUPPORT | 0.46 | 51.57 | 4 |
26 | Torbjörn | DEFENSE | 1.08 | 53.27 | 4 |
27 | Tracer | OFFENSE | 5.19 | 53.60 | 2 |
28 | Widowmaker | DEFENSE | 1.67 | 51.19 | 3 |
29 | Winston | TANK | 2.73 | 54.86 | 3 |
30 | Wrecking Ball | TANK | 6.88 | 52.60 | 1 |
31 | Zarya | TANK | 2.81 | 54.11 | 3 |
32 | Zenyatta | SUPPORT | 6.82 | 53.55 | 1 |
In the data set above collected from esportsrank there are two tables. the first table divides the heroes based on their popularity as the rows and the ranks distributions as the columns. In this case, popularity is defined as pick percentage. In the second table, we there are now 4 columns, the hero name, their pick rate, win rate, and overall tier based entirely on their pick rate. The third table after that is the table sorted based on tiers and pick rate.
# FOURTH, getting our data from overbuff.com for Overwatch 2 November 2022 Data.
import requests
import pandas as pd
# URL: https://raw.githubusercontent.com/rshah5324/rshah5324.github.io/main/overbuff2022.csv
overbuff_2022 = pd.read_csv("https://raw.githubusercontent.com/rshah5324/rshah5324.github.io/main/overbuff2022.csv", sep=',')
# Dropping rows 31-35 as these characters are exclusive to Overwatch 2 and also lack enough data still.
overbuff_2022 = overbuff_2022.drop(range(32,36))
# Sorting the dataframe alphabetically by hero.
overbuff_2022 = overbuff_2022.sort_values('Hero')
# Reindexing the dataframe to start at 1 and stop at 32.
overbuff_2022.index = pd.RangeIndex(start = 1, stop = 33, step = 1)
for i in range(1,33):
# Changing 'Hero' column to fix hero names.
overbuff_2022.loc[i,'Hero'] = hero_list[i-1]
# Changing 'Pick Rate' column values to be floats.
str = overbuff_2022.loc[i,'Pick Rate'][:-1]
overbuff_2022.loc[i,'Pick Rate'] = float(str)
# Changing 'Win Rate' column values to be floats.
str = overbuff_2022.loc[i,'Win Rate'][:-1]
overbuff_2022.loc[i,'Win Rate'] = float(str)
# Renaming 'Pick Rate' column to 'Pick %' and 'Win Rate' column to 'Win %' (keeping column naming consistent).
overbuff_2022 = overbuff_2022.rename(columns = {'Pick Rate': 'Pick %', 'Win Rate': 'Win %'})
# Dropping old index column from CSV file.
overbuff_2022 = overbuff_2022.drop('Unnamed: 0', axis = 1)
# Adding roles to every hero.
overbuff_2022.insert(1, 'Role', overbuff_pc_all_sorted['Role'].tolist())
# Printing cleaned up Overbuff dataframe.
display(overbuff_2022)
Hero | Role | Pick % | Win % | KDA | |
---|---|---|---|---|---|
1 | Ana | SUPPORT | 8.23 | 43.1 | 1.82 |
2 | Ashe | OFFENSE | 1.71 | 46.72 | 3.14 |
3 | Baptiste | SUPPORT | 1.47 | 44.43 | 2.00 |
4 | Bastion | DEFENSE | 0.82 | 32.48 | 3.19 |
5 | Brigitte | SUPPORT | 1.57 | 51.73 | 2.43 |
6 | D.Va | TANK | 4.31 | 37.17 | 2.62 |
7 | Doomfist | OFFENSE | 4.06 | 41.58 | 4.90 |
8 | Echo | OFFENSE | 1.69 | 46.34 | 2.13 |
9 | Genji | OFFENSE | 0.48 | 48.57 | 3.31 |
10 | Hanzo | DEFENSE | 4.35 | 38.3 | 2.83 |
11 | Junkrat | DEFENSE | 3.64 | 36.55 | 2.81 |
12 | Lúcio | SUPPORT | 2.77 | 36.6 | 2.68 |
13 | McCree | OFFENSE | 3.84 | 42.79 | 2.38 |
14 | Mei | DEFENSE | 1.51 | 35.79 | 2.99 |
15 | Mercy | SUPPORT | 6.65 | 41.83 | 0.47 |
16 | Moira | SUPPORT | 3.93 | 46.72 | 3.86 |
17 | Orisa | TANK | 2.0 | 44.24 | 3.23 |
18 | Pharah | OFFENSE | 2.34 | 37.39 | 3.03 |
19 | Reaper | OFFENSE | 2.05 | 37.76 | 3.11 |
20 | Reinhardt | TANK | 6.26 | 45.34 | 2.26 |
21 | Roadhog | TANK | 3.87 | 39.96 | 3.48 |
22 | Sigma | TANK | 1.4 | 48.14 | 3.91 |
23 | Soldier: 76 | OFFENSE | 3.15 | 37.72 | 3.22 |
24 | Sombra | OFFENSE | 1.45 | 35.47 | 3.12 |
25 | Symmetra | SUPPORT | 1.3 | 44.43 | 3.29 |
26 | Torbjörn | DEFENSE | 1.19 | 41.37 | 4.18 |
27 | Tracer | OFFENSE | 2.62 | 36.47 | 2.78 |
28 | Widowmaker | DEFENSE | 3.54 | 35.7 | 2.49 |
29 | Winston | TANK | 2.61 | 43.98 | 3.17 |
30 | Wrecking Ball | TANK | 1.58 | 47.57 | 3.07 |
31 | Zarya | TANK | 4.54 | 45.0 | 4.03 |
32 | Zenyatta | SUPPORT | 3.68 | 45.02 | 2.63 |
The data set above is a csv file from overbuff.com. The table is divided into columns that are role, pick rate, win rate, rank, and the hero name as the relevant columns. Using the table provided, we created tables dividing the heroes based on their rank and and their roles. This data is on Overwatch 2 and is the most recent up to date statistics.
First. let's investigate how different roles pick ratios change across ranks.
# Here, we will find the average pick rate of every role and print out how often a rank chooses it.
import matplotlib.pyplot as plt
import pandas as pd
import numpy
plt.rcParams["figure.figsize"] = (10,6)
# Getting a list of each unique rank (Bronze, Silver, Gold, etc.).
ranks = overbuff_pc['Rank'].unique()
# List to hold each average pick percentage.
average_percent = []
# Color for each bar that will be graphed.
c = ['red', 'brown', 'grey', 'yellow', 'silver', 'blue', 'orange', 'black']
for rank in ranks:
# Creating a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Rank'] == rank]
# Getting the tanks.
curr_table = curr_table.loc[curr_table['Role'] == 'TANK']
# Addding up the pick rate of each tank.
total_percent = curr_table['Pick %'].sum()
# Finding the average pick rate of tanks for each rank.
average_pick_rate = total_percent / len(curr_table)
average_percent.append(average_pick_rate)
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.bar(ranks, average_percent, color = c)
plt.title('Average Pick Rate of Tank Role Based on Rank', fontsize = 20)
plt.ylabel('Average % Pick Rate', fontsize = 20)
plt.show()
import matplotlib.pyplot as plt
import pandas as pd
import numpy
plt.rcParams["figure.figsize"] = (10,6)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# List to hold each percent.
average_percent = []
# Color for each bar that will be graphed.
c = ['red', 'brown', 'grey', 'yellow', 'silver', 'blue', 'orange', 'black']
for rank in ranks:
# Creating a new temporare dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Rank'] == rank]
# Getting the supports.
curr_table = curr_table.loc[curr_table['Role'] == 'SUPPORT']
total_percent = curr_table['Pick %'].sum()
# Finding the average pick rate of support for each rank.
average_pick_rate = total_percent / len(curr_table)
average_percent.append(average_pick_rate)
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.bar(ranks, average_percent, color = c)
plt.title('Average Pick Rate of Support Role Based on Rank', fontsize = 20)
plt.ylabel('Average % Pick Rate', fontsize = 20)
plt.show()
import matplotlib.pyplot as plt
import pandas as pd
import numpy
plt.rcParams["figure.figsize"] = (10,6)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# List to hold each percent.
average_percent = []
# Color for each bar that will be graphed.
c = ['red', 'brown', 'grey', 'yellow', 'silver', 'blue', 'orange', 'black']
for rank in ranks:
# Creating a new temporare dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Rank'] == rank]
# Getting the offense.
curr_table = curr_table.loc[curr_table['Role'] == 'OFFENSE']
total_percent = curr_table['Pick %'].sum()
# Finding the average pick rate of offense for each rank.
average_pick_rate = total_percent / len(curr_table)
average_percent.append(average_pick_rate)
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.bar(ranks, average_percent, color = c)
plt.title('Average Pick Rate of Offense Role Based on Rank', fontsize = 20)
plt.ylabel('Average % Pick Rate', fontsize = 20)
plt.show()
import matplotlib.pyplot as plt
import pandas as pd
import numpy
plt.rcParams["figure.figsize"] = (10,6)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# List to hold each percent.
average_percent = []
# Color for each bar that will be graphed.
c = ['red', 'brown', 'grey', 'yellow', 'silver', 'blue', 'orange', 'black']
for rank in ranks:
# Creating a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Rank'] == rank]
# Getting the defense.
curr_table = curr_table.loc[curr_table['Role'] == 'DEFENSE']
total_percent = curr_table['Pick %'].sum()
# Finding the average pick rate of defense for each rank.
average_pick_rate = total_percent / len(curr_table)
average_percent.append(average_pick_rate)
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.bar(ranks, average_percent, color = c)
plt.title('Average Pick Rate of Defense Role Based on Rank', fontsize = 20)
plt.ylabel('Average % Pick Rate', fontsize = 20)
plt.show()
Both tank and support have the highest pick rate across all ranks. Tank pick rate follows a trend where the role is picked less at lower ranks, and the most at the highest ranks, following a somewhat linear increase. Support has a rather staggered pick rate across ranks. It’s picked the most in grandmaster, gold, and bronze respectively, and least picked in masters, the tier right before grandmaster.
Offense and defense both have the lowest pick rates with offense at an average of 2.2 and defense at an average of 1.2. One thing to note about offensive and defensive characters is that their roles were categorically combined, such that offense and defense became one role: dps (damage per second). With that in mind, even if you add the averages of both pick rates together, they still end up being 1% less than both support and tank.
Offensive characters have the lowest pick rate in grandmaster rank, but peak in platinum. Offensive characters in general tend to lose popularity in higher tiers of overwatch due to the majority of higher ranking players being support or tank mains. Defense has the highest pick rate in bronze, and the lowest pick rate in platinum. A lot of defense characters are easy to play and pick up which leads to a higher pick rate in the lower ranks of bronze and silver. Their higher pick rates in master and grandmaster can be attributed to their ability to counter tanks better than offensive heroes can.
Overall, tanks and supports boast higher pick rates mostly because of their pivotal nature in team compositions. Without a healer to survive or a tank to block damage, you will never be able to survive whereas offensive and defensive characters were not nearly as pivotal to winning.
# Based on the above data, we will find out how often, on average, a certain rank will win using a certain role.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# Tank characters.
characters = overbuff_pc_tank['Hero'].unique()
# List to hold each percent.
average_percent = []
# Iterating through each unique role and their tank win rate.
for hero in characters:
# Creating a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Hero'] == hero]
# Getting the tanks.
curr_table = curr_table.loc[curr_table['Role'] == 'TANK']
plt.plot(curr_table['Rank'], curr_table['Win %'], color=np.random.rand(3,), label = hero, marker ='o')
plt.title('Winning Rate of Tanks Based on Rank', fontsize = 15)
plt.xlabel('Rank', fontsize = 15)
plt.ylabel('Win Rate %', fontsize = 20)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
When taking into account the win rate of characters based on their ranks an important factor to consider is the skill gap. The tank role for example, not a single hero reaches past a 50% winrate in bronze, but most of the win rates end up significantly higher at the higher ranks. This can be attributed to tank being a role that is difficult to play properly without the skills required to play the heroes. As the ranks climb, certain heroes stand out where others tend to falter. The top two heroes for tank are Zarya and Reinhardt, two characters that have synergistic tendencies. The reason why they have the highest winrates is due in part to both of the heroes being played together. When played at their fullest potential, they become the strongest tanks in the game, easily eclipsing all other heroes. That being said, even in average to below average ranks, Zarya and Reinhardt remain the highest win rates due to their easy to learn skill sets.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# Support characters.
characters = overbuff_pc_support['Hero'].unique()
# List to hold each percent.
average_percent = []
# Iterating through each unique role and their support win rate.
for hero in characters:
# Creating a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Hero'] == hero]
# Getting the supports.
curr_table = curr_table.loc[curr_table['Role'] == 'SUPPORT']
plt.plot(curr_table['Rank'], curr_table['Win %'], color=np.random.rand(3,), label = hero, marker ='o')
plt.title('Winning Rate of Support Based on Rank', fontsize = 15)
plt.xlabel('Rank', fontsize = 15)
plt.ylabel('Win Rate %', fontsize = 20)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
Supports have a much more even distribution across ranks. The most interesting hero to make note of is Symmetra who has an impressive win rate up until grandmaster where her win rate drops. Symmetra is also a rather strange character, because when she was a support, she was the only support in the game that had no form of healing. She functioned more as a defense hero with a lot of utility rather than a support. But she still had the support role, thus skewing the list rather heavily. Besides that outlier, characters such as Ana and Baptiste struggle to get solid results in lower ranks due to their high difficulty, but have a much easier time in the higher ranks.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# Offense characters.
characters = overbuff_pc_offense['Hero'].unique()
# List to hold each percent.
average_percent = []
# Iterating through each unique role and their offense win rate.
for hero in characters:
# Creating a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Hero'] == hero]
# Getting the offense.
curr_table = curr_table.loc[curr_table['Role'] == 'OFFENSE']
plt.plot(curr_table['Rank'], curr_table['Win %'], color=np.random.rand(3,), label = hero, marker ='o')
plt.title('Winning Rate of Offense Based on Rank', fontsize = 15)
plt.xlabel('Rank', fontsize = 15)
plt.ylabel('Win Rate %', fontsize = 20)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
Offensive heroes suffer a similar fate as tank heroes, they have low win rates at lower ranks. The reason is rather simple, almost every character in the offensive role requires high quality aim in order to utilize properly. Certain characters do not require aim as much, such as Reaper or Pharah, but their kits and skill sets are unique such that if you do not have a good grasp of the heroes kits, you will struggle to find success.
One noteworthy hero on this list would be Sombra, with the absolute lowest win rate in bronze of nearly 42.5%. In grandmaster rank, however, she has almost a 62.5% win rate. As a hero, Sombras main job is “hacking” other enemy heroes, which disables their abilities for a short time. It’s an extremely useful and debilitating ability, but because of the short duration, players had to be able to communicate with their teams in order to coordinate properly. It was only really seen at higher ranks such as grandmaster, where Sombra could be used to her fullest potential. The same could be said about most if not all of the offensive heroes, but Sombra in particular has the biggest jump from a 42.5% win rate in bronze to a 62.5% win rate in grandmaster.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Getting a list of each unique role.
ranks = overbuff_pc['Rank'].unique()
# Defense characters.
characters = overbuff_pc_defense['Hero'].unique()
# List to hold each percent.
average_percent = []
# Iterate through each unique role and their defense win rate.
for hero in characters:
# Create a new temporary dataframe for each.
curr_table = overbuff_pc.loc[overbuff_pc['Hero'] == hero]
# Getting the defense.
curr_table = curr_table.loc[curr_table['Role'] == 'DEFENSE']
plt.plot(curr_table['Rank'], curr_table['Win %'], color=np.random.rand(3,), label = hero, marker ='o')
plt.title('Winning Rate of Defense Based on Rank', fontsize = 15)
plt.xlabel('Rank', fontsize = 15)
plt.ylabel('Win Rate %', fontsize = 20)
plt.grid(True)
plt.legend() # Display the hero on the corner.
plt.show()
Unlike offensive heroes, defensive heroes have a wider variety of hero abilities. The two lowest win rate heroes in bronze, Widowmaker and Hanzo, are heroes that rely heavily on the skill and aim of the person playing them, thus in lower ranks their win rates are lower. Torbjorn on the other hand, is a hero who places a turret down that fires for him. In lower elos, this functions as a 7th player, but in higher elos, teams coordinate their attacks to properly shoot the turret all at once, nullifying its effectiveness. The win rate variance in the defense is way higher than any of the other roles due to the volatility of the characters in each rank.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Tank characters.
characters = overbuff_pc_grandmaster['Hero'].unique()
# Getting the Grandmaster dataframe for tanks only.
overbuff_pc_grandmaster_tank = overbuff_pc_grandmaster.loc[overbuff_pc_grandmaster['Role'] == 'TANK']
# Tank characters.
characters = overbuff_pc_grandmaster_tank['Hero'].tolist()
win_per = overbuff_pc_grandmaster_tank['Win %'].tolist()
pick_per = overbuff_pc_grandmaster_tank['Pick %'].tolist()
# Plotting the points.
plt.scatter(win_per, pick_per, label='Tank Hero', color='purple')
# Write the hero names above the plot.
for i in range(len(win_per)):
plt.text(win_per[i], pick_per[i] + 0.1, characters[i], ha="center", va="bottom")
# Obtaining the slope, m, and intercept, b, of the plot.
m, b = np.polyfit(win_per, pick_per, 1)
# Plotting the regression line.
plt.plot(win_per, m*(np.array(win_per))+b, color='orange')
plt.title('Tank Win Rate to Pick Rate in Grandmaster', fontsize = 15)
plt.xlabel('Win Rate %', fontsize = 15)
plt.ylabel('Pick Rate %', fontsize = 15)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
The most obvious outlier in the tank role for pick rate % is Reinhardt at an over 10% pick rate. The general trend is that the win rate is higher for heroes with higher pick rates.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Support characters.
characters = overbuff_pc_grandmaster['Hero'].unique()
# Getting the Grandmaster dataframe for support only.
overbuff_pc_grandmaster_support = overbuff_pc_grandmaster.loc[overbuff_pc_grandmaster['Role'] == 'SUPPORT']
# Support characters.
characters = overbuff_pc_grandmaster_support['Hero'].tolist()
win_per = overbuff_pc_grandmaster_support['Win %'].tolist()
pick_per = overbuff_pc_grandmaster_support['Pick %'].tolist()
# Plotting the points.
plt.scatter(win_per, pick_per, label = 'Support Hero', color ='green')
# Writing the hero names above the plot.
for i in range(len(win_per)):
plt.text(win_per[i], pick_per[i] + 0.1, characters[i], ha="center", va="bottom")
# Obtaining the slope, m, and intercept, b, of the plot.
m, b = np.polyfit(win_per, pick_per, 1)
# Plotting the regression line.
plt.plot(win_per, m*(np.array(win_per))+b, color='orange')
plt.title('Support Win Rate to Pick Rate in Grandmaster', fontsize = 15)
plt.xlabel('Win Rate %', fontsize = 15)
plt.ylabel('Pick Rate %', fontsize = 15)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
The most popular hero in the support role is Ana, that being said, the support with the best win rate is Mercy. The reason why Mercy is significantly higher than most other characters is due to her synergy with other offensive heroes. When played with those heroes in tandem, her win rate is significantly higher, but because she is best with those characters, specifically Pharah and Echo, her play rate is not that high. The trend is similar to tanks, where higher win rate characters also have a higher pick rate with Ana skewing the data.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Offense characters.
characters = overbuff_pc_grandmaster['Hero'].unique()
# Getting the Grandmaster dataframe for offense only.
overbuff_pc_grandmaster_offense = overbuff_pc_grandmaster.loc[overbuff_pc_grandmaster['Role'] == 'OFFENSE']
# Offense characters.
characters = overbuff_pc_grandmaster_offense['Hero'].tolist()
win_per = overbuff_pc_grandmaster_offense['Win %'].tolist()
pick_per = overbuff_pc_grandmaster_offense['Pick %'].tolist()
# Plotting the points.
plt.scatter(win_per, pick_per, label='Offense Hero', color='red')
# Writing the hero names above the plot.
for i in range(len(win_per)):
plt.text(win_per[i], pick_per[i] + 0.05, characters[i], ha="center", va="bottom")
# Obtaining the slope, m, and intercept, b, of the plot.
m, b = np.polyfit(win_per, pick_per, 1)
# Plotting the regression line.
plt.plot(win_per, m*(np.array(win_per))+b, color='orange')
plt.title('Offense Win Rate to Pick Rate in Grandmaster', fontsize = 15)
plt.xlabel('Win Rate %', fontsize = 15)
plt.ylabel('Pick Rate %', fontsize = 15)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
Offense is the only graph with a trend going downwards. The highest pick rate hero for offense is McCree, but he also has the lowest win rate amongst all offense heroes. Both Sombra and Pharah have some of the lowest pick rates among offense heroes, but they share similar win rates. They both function very similarly as counters for certain heroes. Sombra is a great hero if an enemy tank is going out of control, and Pharah is a great hero to pick if your enemy can’t deal with someone in the air. But they are only picked in those specific scenarios, that’s why when they are picked, they usually do really well.
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
plt.rcParams["figure.figsize"] = (15,10)
# Defense characters.
characters = overbuff_pc_grandmaster['Hero'].unique()
# Getting the Grandmaster dataframe for defense only.
overbuff_pc_grandmaster_defense = overbuff_pc_grandmaster.loc[overbuff_pc_grandmaster['Role'] == 'DEFENSE']
# Defense characters.
characters = overbuff_pc_grandmaster_defense['Hero'].tolist()
win_per = overbuff_pc_grandmaster_defense['Win %'].tolist()
pick_per = overbuff_pc_grandmaster_defense['Pick %'].tolist()
# Plotting the points.
plt.scatter(win_per, pick_per, label = 'Defense Hero', color='blue')
# Writing the hero names above the plot.
for i in range(len(win_per)):
plt.text(win_per[i], pick_per[i] + 0.05, characters[i], ha="center", va="bottom")
# Obtaining the slope, m, and intercept, b, of the plot.
m, b = np.polyfit(win_per, pick_per, 1)
# Plotting the regression line.
plt.plot(win_per, m*(np.array(win_per))+b, color='orange')
plt.title('Defense Win Rate to Pick Rate in Grandmaster', fontsize = 15)
plt.xlabel('Win Rate %', fontsize = 15)
plt.ylabel('Pick Rate %', fontsize = 15)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
The defense graph has an upwards trend, and has a few heroes that clearly need help. Bastion in particular is a hero who has both a low win rate and a low pick rate in Grandmaster ranks. Without Bastion on the list, the trend might have evened out a bit more. The main trend followers include Hanzo, Widowmaker, and Torbjorn, all 3 function rather similarly at high ranks requiring lots of skill.
Tank: Time to Win Rate Analysis
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Data for Grandmasters in May 2020.
overbuff_pc_tank_grandmaster = overbuff_pc_tank.loc[overbuff_pc_tank['Rank'] == 'Grandmaster']
overbuff_pc_tank_grandmaster = overbuff_pc_tank_grandmaster.sort_values('Hero')
# Data for Grandmasters in December 2021.
overbuff_2021_tank_grandmaster = overbuff_2021.loc[overbuff_2021['Role'] == 'TANK']
# Data for Grandmasters in April 2022.
esports_name_order_tank_grandmaster = esports_name_order.loc[esports_name_order['Role'] == 'TANK']
# Data for Grandmasters in November 2022.
overbuff_2022_tank_grandmaster = overbuff_2022.loc[overbuff_2022['Role'] == 'TANK']
fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)
# Plotting violin plot on axes 1.
ax1.violinplot(overbuff_pc_tank_grandmaster['Win %'], showmedians = True)
ax1.set_title('Tank Win Rate in May 2020')
ax1.set_ylabel("Win %")
# Plotting violin plot on axes 2.
ax2.violinplot(overbuff_2021_tank_grandmaster['Win %'].tolist(), showmedians = True)
ax2.set_title('Tank Win Rate in December 2021')
# Plotting violin plot on axes 3.
ax3.violinplot(esports_name_order_tank_grandmaster['Win %'].tolist(), showmedians = True)
ax3.set_title('Tank Win Rate in April 2022')
# Plotting violin plot on axes 4.
ax4.violinplot(overbuff_2022_tank_grandmaster['Win %'].tolist(), showmedians = True)
ax4.set_title('Tank Win Rate in November 2022 (OW2)')
plt.show()
Support: Time to Win Rate Analysis
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Data for Grandmasters in May 2020.
overbuff_pc_support_grandmaster = overbuff_pc_support.loc[overbuff_pc_support['Rank'] == 'Grandmaster']
overbuff_pc_support_grandmaster = overbuff_pc_support_grandmaster.sort_values('Hero')
# Data for Grandmasters in December 2021.
overbuff_2021_support_grandmaster = overbuff_2021.loc[overbuff_2021['Role'] == 'SUPPORT']
# Data for Grandmasters in April 2022.
esports_name_order_support_grandmaster = esports_name_order.loc[esports_name_order['Role'] == 'SUPPORT']
# Data for Grandmasters in November 2022.
overbuff_2022_support_grandmaster = overbuff_2022.loc[overbuff_2022['Role'] == 'SUPPORT']
fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)
# Plotting violin plot on axes 1.
ax1.violinplot(overbuff_pc_support_grandmaster['Win %'], showmedians=True)
ax1.set_title('Support Win Rate in May 2020')
ax1.set_ylabel("Win %")
# Plotting violin plot on axes 2.
ax2.violinplot(overbuff_2021_support_grandmaster['Win %'].tolist(), showmedians=True)
ax2.set_title('Support Win Rate in December 2021')
# Plotting violin plot on axes 3.
ax3.violinplot(esports_name_order_support_grandmaster['Win %'].tolist(), showmedians=True)
ax3.set_title('Support Win Rate in April 2022')
# Plotting violin plot on axes 4.
ax4.violinplot(overbuff_2022_support_grandmaster['Win %'].tolist(), showmedians=True)
ax4.set_title('Support Win Rate in November 2022 (OW2)')
plt.show()
Offense: Time to Win Rate Analysis
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Data for Grandmasters in May 2020.
overbuff_pc_offense_grandmaster = overbuff_pc_offense.loc[overbuff_pc_offense['Rank'] == 'Grandmaster']
overbuff_pc_offense_grandmaster = overbuff_pc_offense_grandmaster.sort_values('Hero')
# Data for Grandmasters in December 2021.
overbuff_2021_offense_grandmaster = overbuff_2021.loc[overbuff_2021['Role'] == 'OFFENSE']
# Data for Grandmasters in April 2022.
esports_name_order_offense_grandmaster = esports_name_order.loc[esports_name_order['Role'] == 'OFFENSE']
# Data for Grandmasters in November 2022.
overbuff_2022_offense_grandmaster = overbuff_2022.loc[overbuff_2022['Role'] == 'OFFENSE']
fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)
# Plotting violin plot on axes 1.
ax1.violinplot(overbuff_pc_offense_grandmaster['Win %'], showmedians=True)
ax1.set_title('Offense Win Rate in May 2020')
ax1.set_ylabel("Win %")
# Plotting violin plot on axes 2.
ax2.violinplot(overbuff_2021_offense_grandmaster['Win %'].tolist(), showmedians=True)
ax2.set_title('Offense Win Rate in December 2021')
# Plotting violin plot on axes 3.
ax3.violinplot(esports_name_order_offense_grandmaster['Win %'].tolist(), showmedians=True)
ax3.set_title('Offense Win Rate in April 2022')
# Plotting violin plot on axes 4.
ax4.violinplot(overbuff_2022_offense_grandmaster['Win %'].tolist(), showmedians=True)
ax4.set_title('Offense Win Rate in November 2022 (OW2)')
plt.show()
Defense: Time to Win Rate Analysis
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Data for Grandmasters in May 2020.
overbuff_pc_defense_grandmaster = overbuff_pc_defense.loc[overbuff_pc_defense['Rank'] == 'Grandmaster']
overbuff_pc_defense_grandmaster = overbuff_pc_defense_grandmaster.sort_values('Hero')
# Data for Grandmasters in December 2021.
overbuff_2021_defense_grandmaster = overbuff_2021.loc[overbuff_2021['Role'] == 'DEFENSE']
# Data for Grandmasters in April 2022.
esports_name_order_defense_grandmaster = esports_name_order.loc[esports_name_order['Role'] == 'DEFENSE']
# Data for Grandmasters in November 2022.
overbuff_2022_defense_grandmaster = overbuff_2022.loc[overbuff_2022['Role'] == 'DEFENSE']
fig, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=1, ncols=4)
# Plotting violin plot on axes 1.
ax1.violinplot(overbuff_pc_defense_grandmaster['Win %'], showmedians=True)
ax1.set_title('Defense Win Rate in May 2020')
ax1.set_ylabel("Win %")
# Plotting violin plot on axes 2.
ax2.violinplot(overbuff_2021_defense_grandmaster['Win %'].tolist(), showmedians=True)
ax2.set_title('Defense Win Rate in December 2021')
# Plotting violin plot on axes 3.
ax3.violinplot(esports_name_order_defense_grandmaster['Win %'].tolist(), showmedians=True)
ax3.set_title('Defense Win Rate in April 2022')
# Plotting violin plot on axes 4.
ax4.violinplot(overbuff_2022_defense_grandmaster['Win %'].tolist(), showmedians=True)
ax4.set_title('Defense Win Rate in November 2022 (OW2)')
plt.show()
The violin charts we are most interested in are the Defense and the Tanks. Tank in particular saw a huge win rate shift overall in Overwatch 2, with most of the heroes having a higher win rate. The defense heroes on the other hand had a wide spread win rate leading up to Overwatch 2, but the violin graph tightened towards the center in Overwatch 2. We will be exploring these two data sets in our hypothesis section.
In this last section, we will try to create a model in order to predict who got nerfed and who got buffed going into Overwatch 2.
We are going to combine all of the tables win rates together in order to analyze certain heroes variance over time. One important factor to note during analyzation is that the win ratio of heroes in Overwatch 2 is significantly lower overall, but we can still determine who became stronger or weaker based on where they were compared to the other heroes in previous years.
In order to compare how accurate our model is, we have the patch notes here that list the buffs/nerfs each hero got in order to see how accurate our model was.
# Resetting the indices of the grandmaster dataframes of all four time periods for defense heroes.
overbuff_pc_defense_grandmaster = overbuff_pc_defense_grandmaster.reset_index()
overbuff_2021_defense_grandmaster = overbuff_2021_defense_grandmaster.reset_index()
esports_name_order_defense_grandmaster = esports_name_order_defense_grandmaster.reset_index()
overbuff_2022_defense_grandmaster = overbuff_2022_defense_grandmaster.reset_index()
# Getting the unique defense heros.
characters = overbuff_pc_defense_grandmaster['Hero'].unique()
# Indices for the x-axis.
time = ['May 2020', 'December 2021', 'April 2022', 'November 2022']
# Temporary list to hold win percentages for each defense hero.
temp = []
counter = 0
# Iterating through each defense hero.
for hero in characters:
# Appending each win percentage each year for each defense hero.
temp.append(overbuff_pc_defense_grandmaster['Win %'][counter])
temp.append(overbuff_2021_defense_grandmaster['Win %'][counter])
temp.append(esports_name_order_defense_grandmaster['Win %'][counter])
temp.append(overbuff_2022_defense_grandmaster['Win %'][counter])
# Plotting the win percentage each hero for each defense hero.
plt.plot(time, temp, color = np.random.rand(3,), label = hero, marker ='o')
counter += 1
temp = []
plt.title('Win Percentage of Defense Heroes Over Time', fontsize = 15)
plt.xlabel('Years', fontsize = 15)
plt.ylabel('Win Percentage', fontsize = 20)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
Before the switch to overwatch 2, Bastion had a decent win rate sitting at 54% compard to most of the other defense heroes and he was second overall. That being said, once overwatch 2 started, Bastion fell off hard hitting a 32% win rate and having the lowest amongst defense heroes. Based on the data here, Bastion was definitely nerfed going into Overwatch 2. Torbjorn on the other hand got a big buff in Overwatch 2, having the highest win rate overall amongst the defense heroes at a 42%.
If we look at the hero changes, Bastion actually receieved some of the most changes out of all heroes, receiving a complete overhaul to his kit. That being said, his win rate ended up lower than all other heroes. Our model cannot properly determine if a hero got reworked or not, and whether a rework can be considered a buff or nerf can only be determined after play testing. Torbjorn did get buffed, but not significantly enough where he should be the number 1 win rate among all other defense heroes. The main explanation for this would be that Torbjorn is turning the 5v5 game of Overwatch 2 into a 6v5. The loss of 1 player to shoot Torbjorn's turret allowed him to be one of the strongest heroes in the game coming into Overwatch 2.
# Resetting the indices of the grandmaster dataframes of all four time periods for tank heroes.
overbuff_pc_tank_grandmaster = overbuff_pc_tank_grandmaster.reset_index()
overbuff_2021_tank_grandmaster = overbuff_2021_tank_grandmaster.reset_index()
esports_name_order_tank_grandmaster = esports_name_order_tank_grandmaster.reset_index()
overbuff_2022_tank_grandmaster = overbuff_2022_tank_grandmaster.reset_index()
# Getting the unique tank heros.
characters = overbuff_pc_tank_grandmaster['Hero'].unique()
# Temporary list to hold win percentages for each tank hero.
temp = []
counter = 0
# Iterating through each tank hero.
for hero in characters:
# Appending each win percentage each year for each tank hero.
temp.append(overbuff_pc_tank_grandmaster['Win %'][counter])
temp.append(overbuff_2021_tank_grandmaster['Win %'][counter])
temp.append(esports_name_order_tank_grandmaster['Win %'][counter])
temp.append(overbuff_2022_tank_grandmaster['Win %'][counter])
# Plotting the win percentage each hero for each tank hero.
plt.plot(time, temp, color=np.random.rand(3,), label = hero, marker ='o')
counter += 1
temp = []
plt.title('Win Percentage of Tank Heroes Over Time', fontsize = 15)
plt.xlabel('Years', fontsize = 15)
plt.ylabel('Win Percentage', fontsize = 20)
plt.grid(True)
plt.legend() # Displaying the hero on the corner.
plt.show()
Two heroes that ended up benefitting from the switch to Overwatch 2 were based on our model were Wrecking Ball and Sigma. Wrecking Ball in particular has shown a trend of being average to below average among the other tanks in the years, but in Overwatch 2, his win rate compared to the other tanks was higher signifying an overall buff. Out of the tanks who have the lower win rates, the one who saw the biggest change overall was Winston, who was top 3 tanks overall over the time periods we have discussed up until Overwatch 2. Come Overwatch 2. he has fallen to the bottom 3 win rates over all signifying an overall power loss.
One of the biggest changes that happened in Overwatch 2 was the switch from two tanks to one tank on a team. As compensation, all the tanks universely got buffs in order to help them live without another tank. Heroes who can survive on their own as tanks were the ones who benefitted the most from these changes. Interestingly enough based on the patch notes, Winston ended up getting a net buff overall but saw his win rate drop in Overwatch 2. Sigma and Wrecking Ball on the other hand saw very few buffs besides minor tankiness buffs yet their win rates amongst their competition increased. These two tanks in particular are excelling in a 5v5 environment which is a factor that our model does not take into account.
Overwatch 2 has receieved incredible support among its player base who enjoy watching the game evolve as time continues. As you can tell, the game can be very complex as it gets to the higher levels, but hopefully this tutorial has sparked interest or renewed interest for all players. What is clear about our model is that it is difficult to determine what heroes have been buffed or nerfed purely on stats alone. That's why we encourage you to look into it yourself. You never know which hero might be the right fit for you to play.
There are plenty of other statistics to measure a heroes value as well. One thing we did not touch on was which heroes are good on which maps which leads to a huge variation on win rates and pick rates. This will change team compositions and lead to even more discoveries.