Alt text

WHAT IS LEAGUE OF LEGENDS?

League of Legends is a team-based strategy game where two teams of five powerful champions face off to destroy the other’s base. Choose from over 150 champions to make epic plays, secure kills, and take down towers as you battle your way to victory.

DESTROY THE BASE

The Nexus is the heart of both teams’ bases. Destroy the enemy’s Nexus first to win the game.

THE MAP

No description has been provided for this image

Blue and Red team fight on the same map every game. Blue side is on the bottom-left corner, and Red side is on the upper-right corner. The map is symmetrical to its center. It is composed of 3 lanes (toplane, midlane, botlane) and jungle between them. Players pick position in accordance with the role they have decided in the champion select, and has to move from their respective Nexus to the correct position. Each time a player dies or decides to back, he spawns at his respective Nexus.

CLEAR THE PATH

Your team needs to clear at least one lane to get to the enemy Nexus. Blocking your path are defense structures called turrets and inhibitors. Each lane has three turrets and one inhibitor, and each Nexus is guarded by two turrets.

TAKE ON THE JUNGLE

In between the lanes is the jungle, where neutral monsters and jungle plants reside. The two most important monsters are Baron Nashor, the Heralds and the Drakes. Killing these units grants unique buffs for your team and can also turn the tide of the game.

CHOOSE THE LANE

There are five positions that make up the recommended team comp for the game. Each lane lends itself to certain kinds of champions and roles—try them all or lock in to the lane that calls you.

POWER THE CHAMP

Champions get stronger by earning experience to level up and gold to buy more powerful items as the game progresses. Staying on top of these two factors is crucial to overpowering the enemy team and destroying their base.

UNLOCK ABILITIES

Champions have five core abilities, two special spells, and up to seven items at a time. Figuring out the optimal ability order, summoner spells, and item build for your champion will help you succeed as a team.

DATASET

This dataset contains the first 10min. stats of approx. 10k ranked games (SOLO QUEUE) from a high ELO (DIAMOND I to MASTER). Players have roughly the same level.

Each game is unique. The gameId can help you to fetch more attributes from the Riot API.

There are 19 features per team (38 in total) collected after 10min in-game. This includes kills, deaths, gold, experience, level…

The column blueWins is the target value. A value of 1 means the blue team has won. 0 otherwise.

There is no missing value.

In this notebook, we will try to:

  • determine which features are relevant to win the game and which are not
  • predict the winning percentage of the game based on the first 10 minutes of the game
In [1]:
import pandas as pd
pd.set_option('display.max_columns', 500)

from scipy.stats import ttest_1samp

from matplotlib import pyplot as plt
import seaborn as sns

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, AdaBoostClassifier, ExtraTreesClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report

import time
import warnings
warnings.filterwarnings('ignore')

Prepare data¶

In [2]:
df = pd.read_csv('high_diamond_ranked_10min.csv')
df.head()
Out[2]:
gameId blueWins blueWardsPlaced blueWardsDestroyed blueFirstBlood blueKills blueDeaths blueAssists blueEliteMonsters blueDragons blueHeralds blueTowersDestroyed blueTotalGold blueAvgLevel blueTotalExperience blueTotalMinionsKilled blueTotalJungleMinionsKilled blueGoldDiff blueExperienceDiff blueCSPerMin blueGoldPerMin redWardsPlaced redWardsDestroyed redFirstBlood redKills redDeaths redAssists redEliteMonsters redDragons redHeralds redTowersDestroyed redTotalGold redAvgLevel redTotalExperience redTotalMinionsKilled redTotalJungleMinionsKilled redGoldDiff redExperienceDiff redCSPerMin redGoldPerMin
0 4519157822 0 28 2 1 9 6 11 0 0 0 0 17210 6.6 17039 195 36 643 -8 19.5 1721.0 15 6 0 6 9 8 0 0 0 0 16567 6.8 17047 197 55 -643 8 19.7 1656.7
1 4523371949 0 12 1 0 5 5 5 0 0 0 0 14712 6.6 16265 174 43 -2908 -1173 17.4 1471.2 12 1 1 5 5 2 2 1 1 1 17620 6.8 17438 240 52 2908 1173 24.0 1762.0
2 4521474530 0 15 0 0 7 11 4 1 1 0 0 16113 6.4 16221 186 46 -1172 -1033 18.6 1611.3 15 3 1 11 7 14 0 0 0 0 17285 6.8 17254 203 28 1172 1033 20.3 1728.5
3 4524384067 0 43 1 0 4 5 5 1 0 1 0 15157 7.0 17954 201 55 -1321 -7 20.1 1515.7 15 2 1 5 4 10 0 0 0 0 16478 7.0 17961 235 47 1321 7 23.5 1647.8
4 4436033771 0 75 4 0 6 6 6 0 0 0 0 16400 7.0 18543 210 57 -1004 230 21.0 1640.0 17 2 1 6 6 7 1 1 0 0 17404 7.0 18313 225 67 1004 -230 22.5 1740.4
In [3]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9879 entries, 0 to 9878
Data columns (total 40 columns):
 #   Column                        Non-Null Count  Dtype  
---  ------                        --------------  -----  
 0   gameId                        9879 non-null   int64  
 1   blueWins                      9879 non-null   int64  
 2   blueWardsPlaced               9879 non-null   int64  
 3   blueWardsDestroyed            9879 non-null   int64  
 4   blueFirstBlood                9879 non-null   int64  
 5   blueKills                     9879 non-null   int64  
 6   blueDeaths                    9879 non-null   int64  
 7   blueAssists                   9879 non-null   int64  
 8   blueEliteMonsters             9879 non-null   int64  
 9   blueDragons                   9879 non-null   int64  
 10  blueHeralds                   9879 non-null   int64  
 11  blueTowersDestroyed           9879 non-null   int64  
 12  blueTotalGold                 9879 non-null   int64  
 13  blueAvgLevel                  9879 non-null   float64
 14  blueTotalExperience           9879 non-null   int64  
 15  blueTotalMinionsKilled        9879 non-null   int64  
 16  blueTotalJungleMinionsKilled  9879 non-null   int64  
 17  blueGoldDiff                  9879 non-null   int64  
 18  blueExperienceDiff            9879 non-null   int64  
 19  blueCSPerMin                  9879 non-null   float64
 20  blueGoldPerMin                9879 non-null   float64
 21  redWardsPlaced                9879 non-null   int64  
 22  redWardsDestroyed             9879 non-null   int64  
 23  redFirstBlood                 9879 non-null   int64  
 24  redKills                      9879 non-null   int64  
 25  redDeaths                     9879 non-null   int64  
 26  redAssists                    9879 non-null   int64  
 27  redEliteMonsters              9879 non-null   int64  
 28  redDragons                    9879 non-null   int64  
 29  redHeralds                    9879 non-null   int64  
 30  redTowersDestroyed            9879 non-null   int64  
 31  redTotalGold                  9879 non-null   int64  
 32  redAvgLevel                   9879 non-null   float64
 33  redTotalExperience            9879 non-null   int64  
 34  redTotalMinionsKilled         9879 non-null   int64  
 35  redTotalJungleMinionsKilled   9879 non-null   int64  
 36  redGoldDiff                   9879 non-null   int64  
 37  redExperienceDiff             9879 non-null   int64  
 38  redCSPerMin                   9879 non-null   float64
 39  redGoldPerMin                 9879 non-null   float64
dtypes: float64(6), int64(34)
memory usage: 3.0 MB

Note: All columns are numeric, so we can use .describe() to get a quick overview of the data

In [4]:
df.describe()
Out[4]:
gameId blueWins blueWardsPlaced blueWardsDestroyed blueFirstBlood blueKills blueDeaths blueAssists blueEliteMonsters blueDragons blueHeralds blueTowersDestroyed blueTotalGold blueAvgLevel blueTotalExperience blueTotalMinionsKilled blueTotalJungleMinionsKilled blueGoldDiff blueExperienceDiff blueCSPerMin blueGoldPerMin redWardsPlaced redWardsDestroyed redFirstBlood redKills redDeaths redAssists redEliteMonsters redDragons redHeralds redTowersDestroyed redTotalGold redAvgLevel redTotalExperience redTotalMinionsKilled redTotalJungleMinionsKilled redGoldDiff redExperienceDiff redCSPerMin redGoldPerMin
count 9.879000e+03 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000 9879.000000
mean 4.500084e+09 0.499038 22.288288 2.824881 0.504808 6.183925 6.137666 6.645106 0.549954 0.361980 0.187974 0.051422 16503.455512 6.916004 17928.110133 216.699565 50.509667 14.414111 -33.620306 21.669956 1650.345551 22.367952 2.723150 0.495192 6.137666 6.183925 6.662112 0.573135 0.413098 0.160036 0.043021 16489.041401 6.925316 17961.730438 217.349226 51.313088 -14.414111 33.620306 21.734923 1648.904140
std 2.757328e+07 0.500024 18.019177 2.174998 0.500002 3.011028 2.933818 4.064520 0.625527 0.480597 0.390712 0.244369 1535.446636 0.305146 1200.523764 21.858437 9.898282 2453.349179 1920.370438 2.185844 153.544664 18.457427 2.138356 0.500002 2.933818 3.011028 4.060612 0.626482 0.492415 0.366658 0.216900 1490.888406 0.305311 1198.583912 21.911668 10.027885 2453.349179 1920.370438 2.191167 149.088841
min 4.295358e+09 0.000000 5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 10730.000000 4.600000 10098.000000 90.000000 0.000000 -10830.000000 -9333.000000 9.000000 1073.000000 6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 11212.000000 4.800000 10465.000000 107.000000 4.000000 -11467.000000 -8348.000000 10.700000 1121.200000
25% 4.483301e+09 0.000000 14.000000 1.000000 0.000000 4.000000 4.000000 4.000000 0.000000 0.000000 0.000000 0.000000 15415.500000 6.800000 17168.000000 202.000000 44.000000 -1585.500000 -1290.500000 20.200000 1541.550000 14.000000 1.000000 0.000000 4.000000 4.000000 4.000000 0.000000 0.000000 0.000000 0.000000 15427.500000 6.800000 17209.500000 203.000000 44.000000 -1596.000000 -1212.000000 20.300000 1542.750000
50% 4.510920e+09 0.000000 16.000000 3.000000 1.000000 6.000000 6.000000 6.000000 0.000000 0.000000 0.000000 0.000000 16398.000000 7.000000 17951.000000 218.000000 50.000000 14.000000 -28.000000 21.800000 1639.800000 16.000000 2.000000 0.000000 6.000000 6.000000 6.000000 0.000000 0.000000 0.000000 0.000000 16378.000000 7.000000 17974.000000 218.000000 51.000000 -14.000000 28.000000 21.800000 1637.800000
75% 4.521733e+09 1.000000 20.000000 4.000000 1.000000 8.000000 8.000000 9.000000 1.000000 1.000000 0.000000 0.000000 17459.000000 7.200000 18724.000000 232.000000 56.000000 1596.000000 1212.000000 23.200000 1745.900000 20.000000 4.000000 1.000000 8.000000 8.000000 9.000000 1.000000 1.000000 0.000000 0.000000 17418.500000 7.200000 18764.500000 233.000000 57.000000 1585.500000 1290.500000 23.300000 1741.850000
max 4.527991e+09 1.000000 250.000000 27.000000 1.000000 22.000000 22.000000 29.000000 2.000000 1.000000 1.000000 4.000000 23701.000000 8.000000 22224.000000 283.000000 92.000000 11467.000000 8348.000000 28.300000 2370.100000 276.000000 24.000000 1.000000 22.000000 22.000000 28.000000 2.000000 1.000000 1.000000 2.000000 22732.000000 8.200000 22269.000000 289.000000 92.000000 10830.000000 9333.000000 28.900000 2273.200000
In [5]:
print('NaN values: ', df.isna().sum().sum())
NaN values:  0

Note: Data don't have any missing values

In [6]:
df.drop_duplicates(inplace=True)

Clean data¶

In [7]:
df['gameId'].nunique() == len(df)
Out[7]:
True

Note: There are no duplicate games, so the gameId is valid. We can drop it.

In [8]:
df.drop('gameId', axis=1, inplace=True)

As domain knowledge, we know that the game first blood is just for only one team. So blueFirstBlood is the opposite of redFirstBlood. We must check if there are any inconsistencies.

In [9]:
sum(df['blueFirstBlood'] == df['redFirstBlood'])  # validate that there are no games where both teams got first blood
Out[9]:
0

blueGoldDiff and redGoldDiff are must be the opposite of each other. We must check if there are any inconsistencies.

In [10]:
sum(df['blueGoldDiff'] != -df['redGoldDiff'])  # validate that there are no games where the gold difference is not the same for both teams
Out[10]:
0

blueExperienceDiff and redExperienceDiff are must be the opposite of each other. We must check if there are any inconsistencies.

In [11]:
sum(df['blueExperienceDiff'] != -df['redExperienceDiff'])  # validate that there are no games where the experience difference is not the same for both teams
Out[11]:
0

Hyporthesis test: Is there a significant win rate difference between the blue team and the red team?¶

In [12]:
sns.countplot(x='blueWins', data=df)
plt.xlabel('Team Wins')
plt.ylabel('Count')
plt.xticks([0, 1], ['Red', 'Blue'])
for p in plt.gca().patches:
  height = p.get_height()
  percentage = f'{100 * height / len(df):.2f}%'
  plt.gca().annotate(percentage, (p.get_x() + p.get_width() / 2, height), ha='center', va='bottom')
plt.title('Team Win Percentage at Origin data')
plt.show()
No description has been provided for this image

H0: Porportion of blue team win rate = Porportion of red team win rate = 0.5
H1: Porportion of blue team win rate != Porportion of red team win rate != 0.5

We will use One-sample proportion tests

In [13]:
mu = 0.5
alpha = 0.05
_, p_value = ttest_1samp(df['blueWins'], mu, alternative='two-sided')
print('p-value: ', p_value)
p-value:  0.8484114428598137
In [14]:
if p_value < alpha:
  print('Reject H0')
else:
  print('Fail to reject H0')
Fail to reject H0

Note: Because p_value is greater than 0.05, we fail to reject the null hypothesis. So there is no significant win rate difference between the blue team and the red team. In other words, the blue team and the red team have the same win rate 50%.

Note 2: So that we move red team data to new rows and change suffixes of columns from _blue and _red to _friendly and _enemy. The result is double rows of data that blue team and red team are treated equally.

Manipulate data¶

In [15]:
new_df = {}
for col in df.columns:
  if col.startswith('blue'):
    new_df['friendly' + col[len('blue'):]] = []
  elif col.startswith('red'):
    new_df['enemy' + col[len('red'):]] = []
In [16]:
for index, row in df.iterrows():
  for team in ['blue', 'red']:
    if team == 'red':
      new_df['friendlyWins'].append(1 - row['blueWins'])
      for col in df.columns[1:]:
        if col.startswith('red'):
          new_df['friendly' + col[len('red'):]].append(row[col])
        else:
          new_df['enemy' + col[len('blue'):]].append(row[col])
    else:
      new_df['friendlyWins'].append(row['blueWins'])
      for col in df.columns[1:]:
        if col.startswith('blue'):
          new_df['friendly' + col[len('blue'):]].append(row[col])
        else:
          new_df['enemy' + col[len('red'):]].append(row[col])
In [17]:
new_df = pd.DataFrame(new_df)
new_df.head()
Out[17]:
friendlyWins friendlyWardsPlaced friendlyWardsDestroyed friendlyFirstBlood friendlyKills friendlyDeaths friendlyAssists friendlyEliteMonsters friendlyDragons friendlyHeralds friendlyTowersDestroyed friendlyTotalGold friendlyAvgLevel friendlyTotalExperience friendlyTotalMinionsKilled friendlyTotalJungleMinionsKilled friendlyGoldDiff friendlyExperienceDiff friendlyCSPerMin friendlyGoldPerMin enemyWardsPlaced enemyWardsDestroyed enemyFirstBlood enemyKills enemyDeaths enemyAssists enemyEliteMonsters enemyDragons enemyHeralds enemyTowersDestroyed enemyTotalGold enemyAvgLevel enemyTotalExperience enemyTotalMinionsKilled enemyTotalJungleMinionsKilled enemyGoldDiff enemyExperienceDiff enemyCSPerMin enemyGoldPerMin
0 0.0 28.0 2.0 1.0 9.0 6.0 11.0 0.0 0.0 0.0 0.0 17210.0 6.6 17039.0 195.0 36.0 643.0 -8.0 19.5 1721.0 15.0 6.0 0.0 6.0 9.0 8.0 0.0 0.0 0.0 0.0 16567.0 6.8 17047.0 197.0 55.0 -643.0 8.0 19.7 1656.7
1 1.0 15.0 6.0 0.0 6.0 9.0 8.0 0.0 0.0 0.0 0.0 16567.0 6.8 17047.0 197.0 55.0 -643.0 8.0 19.7 1656.7 28.0 2.0 1.0 9.0 6.0 11.0 0.0 0.0 0.0 0.0 17210.0 6.6 17039.0 195.0 36.0 643.0 -8.0 19.5 1721.0
2 0.0 12.0 1.0 0.0 5.0 5.0 5.0 0.0 0.0 0.0 0.0 14712.0 6.6 16265.0 174.0 43.0 -2908.0 -1173.0 17.4 1471.2 12.0 1.0 1.0 5.0 5.0 2.0 2.0 1.0 1.0 1.0 17620.0 6.8 17438.0 240.0 52.0 2908.0 1173.0 24.0 1762.0
3 1.0 12.0 1.0 1.0 5.0 5.0 2.0 2.0 1.0 1.0 1.0 17620.0 6.8 17438.0 240.0 52.0 2908.0 1173.0 24.0 1762.0 12.0 1.0 0.0 5.0 5.0 5.0 0.0 0.0 0.0 0.0 14712.0 6.6 16265.0 174.0 43.0 -2908.0 -1173.0 17.4 1471.2
4 0.0 15.0 0.0 0.0 7.0 11.0 4.0 1.0 1.0 0.0 0.0 16113.0 6.4 16221.0 186.0 46.0 -1172.0 -1033.0 18.6 1611.3 15.0 3.0 1.0 11.0 7.0 14.0 0.0 0.0 0.0 0.0 17285.0 6.8 17254.0 203.0 28.0 1172.0 1033.0 20.3 1728.5

Create KDA features

In [18]:
friendly_kda = []
enemy_kda = []
for index, row in new_df.iterrows():
  if row['friendlyDeaths'] == 0:
    friendly_kda.append(row['friendlyKills'] + row['friendlyAssists'])
  else:
    friendly_kda.append((row['friendlyKills'] + row['friendlyAssists']) / row['friendlyDeaths'])
  
  if row['enemyDeaths'] == 0:
    enemy_kda.append(row['enemyKills'] + row['enemyAssists'])
  else:
    enemy_kda.append((row['enemyKills'] + row['enemyAssists']) / row['enemyDeaths'])

new_df['friendlyKDA'] = friendly_kda
new_df['enemyKDA'] = enemy_kda

Create difference features between friendly and enemy columns

In [19]:
new_df['diffWardsPlaced'] = new_df['friendlyWardsPlaced'] - new_df['enemyWardsPlaced']
new_df['diffWardsDestroyed'] = new_df['friendlyWardsDestroyed'] - new_df['enemyWardsDestroyed']
new_df['firstBlood'] = new_df['friendlyFirstBlood']
new_df['diffKills'] = new_df['friendlyKills'] - new_df['enemyKills']
new_df['diffDeaths'] = new_df['friendlyDeaths'] - new_df['enemyDeaths']
new_df['diffAssists'] = new_df['friendlyAssists'] - new_df['enemyAssists']
new_df['diffEliteMonsters'] = new_df['friendlyEliteMonsters'] - new_df['enemyEliteMonsters']
new_df['diffDragons'] = new_df['friendlyDragons'] - new_df['enemyDragons']
new_df['diffHeralds'] = new_df['friendlyHeralds'] - new_df['enemyHeralds']
new_df['diffTowersDestroyed'] = new_df['friendlyTowersDestroyed'] - new_df['enemyTowersDestroyed']
new_df['diffGold'] = new_df['friendlyGoldDiff']
new_df['diffAvgLevel'] = new_df['friendlyAvgLevel'] - new_df['enemyAvgLevel']
new_df['diffExperience'] = new_df['friendlyExperienceDiff']
new_df['diffMinionsKilled'] = new_df['friendlyTotalMinionsKilled'] - new_df['enemyTotalMinionsKilled']
new_df['diffJungleMinionsKilled'] = new_df['friendlyTotalJungleMinionsKilled'] - new_df['enemyTotalJungleMinionsKilled']
new_df['diffCSPerMin'] = new_df['friendlyCSPerMin'] - new_df['enemyCSPerMin']
new_df['diffGoldPerMin'] = new_df['friendlyGoldPerMin'] - new_df['enemyGoldPerMin']
new_df['diffKDA'] = new_df['friendlyKDA'] - new_df['enemyKDA']

Drop unnecessary columns

In [20]:
new_df.drop([
  'friendlyFirstBlood', 'friendlyKills', 'friendlyDeaths', 'friendlyAssists', 'friendlyEliteMonsters',
  'friendlyDragons', 'friendlyHeralds', 'friendlyTowersDestroyed', 'friendlyGoldDiff', 'friendlyAvgLevel',
  'friendlyExperienceDiff', 'friendlyTotalMinionsKilled', 'friendlyTotalJungleMinionsKilled', 'friendlyCSPerMin',
  'friendlyGoldPerMin', 'enemyFirstBlood', 'enemyKills', 'enemyDeaths', 'enemyAssists', 'enemyEliteMonsters',
  'enemyDragons', 'enemyHeralds', 'enemyTowersDestroyed', 'enemyGoldDiff', 'enemyAvgLevel', 'enemyExperienceDiff',
  'enemyTotalMinionsKilled', 'enemyTotalJungleMinionsKilled', 'enemyCSPerMin', 'enemyGoldPerMin', 'friendlyWardsPlaced',
  'enemyWardsPlaced', 'friendlyWardsDestroyed', 'enemyWardsDestroyed', 'friendlyTotalGold', 'friendlyTotalExperience',
  'enemyTotalGold', 'enemyTotalExperience', 'enemyTotalGold', 'enemyTotalExperience', 'friendlyKDA', 'enemyKDA'
], axis=1, inplace=True)

Train, test split¶

Before go further, we must split the data into train and test sets to avoid data leakage.

In [21]:
df_train, df_test = train_test_split(new_df, test_size=0.2, random_state=42, stratify=new_df['friendlyWins'])
In [22]:
df_train.describe()
Out[22]:
friendlyWins diffWardsPlaced diffWardsDestroyed firstBlood diffKills diffDeaths diffAssists diffEliteMonsters diffDragons diffHeralds diffTowersDestroyed diffGold diffAvgLevel diffExperience diffMinionsKilled diffJungleMinionsKilled diffCSPerMin diffGoldPerMin diffKDA
count 15806.000000 15806.000000 15806.000000 15806.000000 15806.00000 15806.00000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000 15806.000000
mean 0.500000 0.104454 0.005441 0.500949 -0.00601 0.00601 -0.000759 -0.000822 -0.003480 0.002657 -0.000063 -4.932621 -0.002683 -8.644376 -0.061116 -0.037770 -0.006112 -0.493262 -0.014567
std 0.500016 26.038485 2.845906 0.500015 4.16984 4.16984 5.748455 1.068516 0.880698 0.589473 0.321437 2441.729603 0.476593 1915.058145 31.039593 14.314846 3.103959 244.172960 5.131358
min 0.000000 -254.000000 -22.000000 0.000000 -16.00000 -17.00000 -22.000000 -2.000000 -1.000000 -1.000000 -3.000000 -10830.000000 -2.600000 -9333.000000 -127.000000 -72.000000 -12.700000 -1083.000000 -39.823529
25% 0.000000 -4.000000 -1.000000 0.000000 -3.00000 -3.00000 -4.000000 -1.000000 -1.000000 0.000000 0.000000 -1573.750000 -0.400000 -1263.750000 -20.000000 -9.000000 -2.000000 -157.375000 -2.168290
50% 0.500000 0.000000 0.000000 1.000000 0.00000 0.00000 0.000000 0.000000 0.000000 0.000000 0.000000 3.500000 0.000000 -5.500000 0.000000 0.000000 0.000000 0.350000 0.000000
75% 1.000000 4.000000 1.000000 1.000000 3.00000 3.00000 4.000000 1.000000 1.000000 0.000000 0.000000 1577.000000 0.400000 1240.000000 20.000000 9.000000 2.000000 157.700000 2.166667
max 1.000000 260.000000 23.000000 1.000000 17.00000 16.00000 25.000000 2.000000 1.000000 1.000000 3.000000 10830.000000 2.600000 9333.000000 127.000000 72.000000 12.700000 1083.000000 35.833333

EDA¶

Correlation¶

In [23]:
plt.figure(figsize=(12, 10))
sns.heatmap(df_train.corr(), cmap='coolwarm', annot=True, fmt='.2f')
plt.show()
No description has been provided for this image

Mean of stats by win/loss¶

In [24]:
df_train.groupby('friendlyWins').mean()
Out[24]:
diffWardsPlaced diffWardsDestroyed firstBlood diffKills diffDeaths diffAssists diffEliteMonsters diffDragons diffHeralds diffTowersDestroyed diffGold diffAvgLevel diffExperience diffMinionsKilled diffJungleMinionsKilled diffCSPerMin diffGoldPerMin diffKDA
friendlyWins
0.0 -0.273187 -0.215235 0.400987 -1.995571 1.995571 -2.178287 -0.281665 -0.212578 -0.069088 -0.049222 -1245.481716 -0.217715 -941.380741 -9.550677 -2.436417 -0.955068 -124.548172 -2.116901
1.0 0.482095 0.226117 0.600911 1.983551 -1.983551 2.176768 0.280020 0.205618 0.074402 0.049095 1235.616475 0.212350 924.091990 9.428445 2.360876 0.942844 123.561647 2.087767

Note: In genaral, the team with the higher stats like wards placed, wards destroyed, KDA, gold, experience, Drake, Herald, towers destroyed, etc. will win the game.

Wards placed¶

In [25]:
df_train.groupby('diffWardsPlaced')['friendlyWins'].agg(['mean', 'count'])
Out[25]:
mean count
diffWardsPlaced
-254.0 1.0 1
-234.0 0.0 1
-218.0 0.0 1
-200.0 0.0 1
-195.0 1.0 1
... ... ...
200.0 1.0 1
218.0 1.0 1
234.0 1.0 1
254.0 0.0 1
260.0 1.0 1

316 rows × 2 columns

In [26]:
sns.histplot(x='diffWardsPlaced', hue='friendlyWins', data=df_train, bins=50, kde=False)
plt.show()
No description has been provided for this image

Elite monsters¶

In [27]:
df_train.groupby('diffEliteMonsters')['friendlyWins'].agg(['mean', 'count'])
Out[27]:
mean count
diffEliteMonsters
-2.0 0.269398 1147
-1.0 0.368231 4432
0.0 0.500000 4670
1.0 0.634174 4401
2.0 0.723183 1156
  • Team got -2 elite monsters at 10 minutes has 28.03% chance to win the game.
  • Team got -1 elite monsters at 10 minutes has 36.82% chance to win the game.
  • Team got 0 elite monsters at 10 minutes has 50.00% chance to win the game.
  • Team got 1 elite monsters at 10 minutes has 62.98% chance to win the game.
  • Team got 2 elite monsters at 10 minutes has 72.16% chance to win the game.

First blood¶

In [28]:
df_train.groupby('firstBlood')['friendlyWins'].mean()
Out[28]:
firstBlood
0.0    0.399848
1.0    0.599773
Name: friendlyWins, dtype: float64
  • Team got first blood has 60.00% chance to win the game.

KDA¶

In [29]:
df_train.groupby('friendlyWins')['diffKDA'].mean()
Out[29]:
friendlyWins
0.0   -2.116901
1.0    2.087767
Name: diffKDA, dtype: float64
In [30]:
min_diffKDA = int(df_train['diffKDA'].min())
max_diffKDA = int(df_train['diffKDA'].max())
step = 10

for i in range(min_diffKDA, max_diffKDA, step):
  winning_rate = df_train[(df_train['diffKDA'] >= i) & (df_train['diffKDA'] < i + step)]['friendlyWins'].mean()
  print(f'with diffKDA between {i} and {i + step}: winning rate is {winning_rate*100:.2f} %')
with diffKDA between -39 and -29: winning rate is 0.00 %
with diffKDA between -29 and -19: winning rate is 1.30 %
with diffKDA between -19 and -9: winning rate is 10.49 %
with diffKDA between -9 and 1: winning rate is 36.81 %
with diffKDA between 1 and 11: winning rate is 73.44 %
with diffKDA between 11 and 21: winning rate is 92.21 %
with diffKDA between 21 and 31: winning rate is 98.18 %
with diffKDA between 31 and 41: winning rate is 100.00 %

diff Towers Destroyed¶

In [31]:
gb_diffTowersDestroyed = df_train.groupby('diffTowersDestroyed')['friendlyWins'].mean()
In [32]:
for idx in gb_diffTowersDestroyed.index:
  print(f'with diffTowersDestroyed = {idx}: winning rate is {gb_diffTowersDestroyed[idx]*100:.2f} %')
with diffTowersDestroyed = -3.0: winning rate is 0.00 %
with diffTowersDestroyed = -2.0: winning rate is 12.20 %
with diffTowersDestroyed = -1.0: winning rate is 25.25 %
with diffTowersDestroyed = 0.0: winning rate is 49.92 %
with diffTowersDestroyed = 1.0: winning rate is 76.66 %
with diffTowersDestroyed = 2.0: winning rate is 87.50 %
with diffTowersDestroyed = 3.0: winning rate is 100.00 %
In [33]:
df_train.groupby(['friendlyWins', 'firstBlood'])['diffTowersDestroyed'].mean()
Out[33]:
friendlyWins  firstBlood
0.0           0.0          -0.069497
              1.0          -0.018933
1.0           0.0           0.016487
              1.0           0.070752
Name: diffTowersDestroyed, dtype: float64
  • At lose, the team got first blood can reduce the difference towers destroyed from -0.069 to -0.019
  • At win, the team didn't get first blood can loss the difference towers destroyed from 0.071 to 0.016

diff Gold¶

In [34]:
min_diffGold = int(df_train['diffGold'].min())
max_diffGold = int(df_train['diffGold'].max())
step = 2000

for i in range(min_diffGold, max_diffGold, step):
  winning_rate = df_train[(df_train['diffGold'] >= i) & (df_train['diffGold'] < i + step)]['friendlyWins'].mean()
  print(f'with diffGold between {i} and {i + step}: winning rate is {winning_rate*100:.2f} %')
with diffGold between -10830 and -8830: winning rate is 0.00 %
with diffGold between -8830 and -6830: winning rate is 0.00 %
with diffGold between -6830 and -4830: winning rate is 3.43 %
with diffGold between -4830 and -2830: winning rate is 12.37 %
with diffGold between -2830 and -830: winning rate is 26.96 %
with diffGold between -830 and 1170: winning rate is 52.54 %
with diffGold between 1170 and 3170: winning rate is 76.11 %
with diffGold between 3170 and 5170: winning rate is 90.34 %
with diffGold between 5170 and 7170: winning rate is 98.43 %
with diffGold between 7170 and 9170: winning rate is 100.00 %
with diffGold between 9170 and 11170: winning rate is 100.00 %

diff experience¶

In [35]:
min_diffEXP = int(df_train['diffExperience'].min())
max_diffEXP = int(df_train['diffExperience'].max())
step = 2000

for i in range(min_diffEXP, max_diffEXP, step):
  winning_rate = df_train[(df_train['diffExperience'] >= i) & (df_train['diffExperience'] < i + step)]['friendlyWins'].mean()
  print(f'with diffExperience between {i} and {i + step}: winning rate is {winning_rate*100:.2f} %')
with diffExperience between -9333 and -7333: winning rate is 0.00 %
with diffExperience between -7333 and -5333: winning rate is 1.79 %
with diffExperience between -5333 and -3333: winning rate is 6.75 %
with diffExperience between -3333 and -1333: winning rate is 19.60 %
with diffExperience between -1333 and 667: winning rate is 44.98 %
with diffExperience between 667 and 2667: winning rate is 73.21 %
with diffExperience between 2667 and 4667: winning rate is 90.85 %
with diffExperience between 4667 and 6667: winning rate is 97.22 %
with diffExperience between 6667 and 8667: winning rate is 100.00 %
with diffExperience between 8667 and 10667: winning rate is 100.00 %
In [36]:
sns.scatterplot(data=df_train[df_train['friendlyWins'] == 1], x='diffGold', y='diffExperience', color='#61C0BF')
plt.axvline(x=0, color='black', linestyle='--')
plt.axhline(y=0, color='black', linestyle='--')
plt.title('Gold and Experience difference for winning team')
plt.show()
No description has been provided for this image
In [37]:
sns.scatterplot(data=df_train[df_train['friendlyWins'] == 0], x='diffGold', y='diffExperience', color='#F38181')
plt.axvline(x=0, color='black', linestyle='--')
plt.axhline(y=0, color='black', linestyle='--')
plt.title('Gold and Experience difference for losing team')
plt.show()
No description has been provided for this image

Predict model¶

In [38]:
X = df_train.copy()
X.drop(
  [
    'diffWardsPlaced',	'diffWardsDestroyed', 'diffHeralds', 'diffTowersDestroyed', 'diffJungleMinionsKilled'
  ], axis=1, inplace=True)
y = X.pop('friendlyWins')

X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
In [39]:
class LazyClassifier:
  def __init__(self, random_state=42):
    self.models = {
      'Logistic Regression': LogisticRegression(),
      'Random Forest': RandomForestClassifier(random_state=random_state),
      'Gradient Boosting': GradientBoostingClassifier(random_state=random_state),
      'AdaBoost': AdaBoostClassifier(random_state=random_state),
      'Extra Trees': ExtraTreesClassifier(random_state=random_state),
      'Decision Tree': DecisionTreeClassifier(random_state=random_state),
      'SVM': SVC(),
      'XGBoost': XGBClassifier(random_state=random_state),
      'LightGBM': LGBMClassifier(random_state=random_state),
      'CatBoost': CatBoostClassifier(verbose=False, random_seed=random_state)
    }
    self.fit_times = []
    self.predict_times = []
    self.train_scores = []
    self.val_scores = []
  
  def fit(self, X_train, y_train, X_val, y_val):
    for name, model in self.models.items():
      print(f'Fitting {name}...')
      t1_fitting = time.time()
      model.fit(X_train, y_train)
      t2_fitting = time.time()
      self.fit_times.append(t2_fitting - t1_fitting)
      train_score = accuracy_score(y_train, model.predict(X_train))
      self.train_scores.append(train_score)

      print(f'Predicting {name}...')
      t1_predicting = time.time()
      y_pred = model.predict(X_val)
      t2_predicting = time.time()
      self.predict_times.append(t2_predicting - t1_predicting)
      val_score = accuracy_score(y_val, y_pred)
      self.val_scores.append(val_score)
      print(f'{name} done after {t2_predicting - t1_fitting:.2f}s', end='\n\n')

  def summary(self):
    results = pd.DataFrame({
      'Model': list(self.models.keys()),
      'Train Score': self.train_scores,
      'Val Score': self.val_scores,
      'Fit Time': self.fit_times,
      'Predict Time': self.predict_times
    })
    return results.sort_values(['Val Score', 'Train Score'], ascending=False)

Baseline model¶

In [40]:
baseline_model = LazyClassifier()
baseline_model.fit(X_train, y_train, X_val, y_val)
Fitting Logistic Regression...
Predicting Logistic Regression...
Logistic Regression done after 0.09s

Fitting Random Forest...
Predicting Random Forest...
Random Forest done after 3.87s

Fitting Gradient Boosting...
Predicting Gradient Boosting...
Gradient Boosting done after 3.21s

Fitting AdaBoost...
Predicting AdaBoost...
AdaBoost done after 0.80s

Fitting Extra Trees...
Predicting Extra Trees...
Extra Trees done after 2.06s

Fitting Decision Tree...
Predicting Decision Tree...
Decision Tree done after 0.19s

Fitting SVM...
Predicting SVM...
SVM done after 14.76s

Fitting XGBoost...
Predicting XGBoost...
XGBoost done after 0.14s

Fitting LightGBM...
[LightGBM] [Info] Number of positive: 6322, number of negative: 6322
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000470 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 1621
[LightGBM] [Info] Number of data points in the train set: 12644, number of used features: 13
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.500000 -> initscore=0.000000
Predicting LightGBM...
LightGBM done after 0.17s

Fitting CatBoost...
Predicting CatBoost...
CatBoost done after 7.75s

In [41]:
baseline_model.summary()
Out[41]:
Model Train Score Val Score Fit Time Predict Time
3 AdaBoost 0.730228 0.744782 0.723273 0.019336
6 SVM 0.722082 0.741935 4.549563 2.104097
0 Logistic Regression 0.726906 0.741619 0.083477 0.001002
9 CatBoost 0.772619 0.741303 7.736666 0.003433
2 Gradient Boosting 0.739718 0.740354 3.188040 0.005680
8 LightGBM 0.778472 0.736243 0.154515 0.003527
1 Random Forest 0.999921 0.725490 3.632452 0.053254
4 Extra Trees 1.000000 0.722011 1.718927 0.077894
7 XGBoost 0.832094 0.717900 0.127292 0.004000
5 Decision Tree 1.000000 0.641999 0.178621 0.002045

Standard scale¶

In [42]:
from sklearn.preprocessing import StandardScaler
In [43]:
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
In [44]:
scaled_model =  LazyClassifier()
scaled_model.fit(X_train_scaled, y_train, X_val_scaled, y_val)
Fitting Logistic Regression...
Predicting Logistic Regression...
Logistic Regression done after 0.03s

Fitting Random Forest...
Predicting Random Forest...
Random Forest done after 3.89s

Fitting Gradient Boosting...
Predicting Gradient Boosting...
Gradient Boosting done after 3.26s

Fitting AdaBoost...
Predicting AdaBoost...
AdaBoost done after 0.84s

Fitting Extra Trees...
Predicting Extra Trees...
Extra Trees done after 2.04s

Fitting Decision Tree...
Predicting Decision Tree...
Decision Tree done after 0.19s

Fitting SVM...
Predicting SVM...
SVM done after 15.32s

Fitting XGBoost...
Predicting XGBoost...
XGBoost done after 0.10s

Fitting LightGBM...
[LightGBM] [Info] Number of positive: 6322, number of negative: 6322
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000445 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 1629
[LightGBM] [Info] Number of data points in the train set: 12644, number of used features: 13
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.500000 -> initscore=0.000000
Predicting LightGBM...
LightGBM done after 0.10s

Fitting CatBoost...
Predicting CatBoost...
CatBoost done after 7.83s

In [45]:
scaled_model.summary()
Out[45]:
Model Train Score Val Score Fit Time Predict Time
0 Logistic Regression 0.729279 0.746363 0.027555 0.001007
3 AdaBoost 0.730228 0.744782 0.758977 0.018542
6 SVM 0.728725 0.743833 4.475697 2.198871
9 CatBoost 0.772619 0.741303 7.819383 0.007479
2 Gradient Boosting 0.739718 0.740354 3.242692 0.005013
8 LightGBM 0.776099 0.734029 0.084430 0.003232
1 Random Forest 0.999921 0.723909 3.653182 0.054327
4 Extra Trees 1.000000 0.722011 1.710580 0.076102
7 XGBoost 0.832094 0.717900 0.093278 0.003071
5 Decision Tree 1.000000 0.640734 0.181613 0.000997

Polynomial features¶

In [46]:
from sklearn.preprocessing import PolynomialFeatures
In [47]:
poly = PolynomialFeatures(degree=2, include_bias=False)
In [48]:
X_poly_train = poly.fit_transform(X_train)
X_poly_val = poly.transform(X_val)
In [49]:
X_poly_train_scaled = scaler.fit_transform(X_poly_train)
X_poly_val_scaled = scaler.transform(X_poly_val)
In [50]:
poly_model = LazyClassifier()
poly_model.fit(X_poly_train, y_train, X_poly_val, y_val)
Fitting Logistic Regression...
Predicting Logistic Regression...
Logistic Regression done after 0.28s

Fitting Random Forest...
Predicting Random Forest...
Random Forest done after 14.31s

Fitting Gradient Boosting...
Predicting Gradient Boosting...
Gradient Boosting done after 35.44s

Fitting AdaBoost...
Predicting AdaBoost...
AdaBoost done after 6.48s

Fitting Extra Trees...
Predicting Extra Trees...
Extra Trees done after 4.10s

Fitting Decision Tree...
Predicting Decision Tree...
Decision Tree done after 1.97s

Fitting SVM...
Predicting SVM...
SVM done after 48.73s

Fitting XGBoost...
Predicting XGBoost...
XGBoost done after 0.54s

Fitting LightGBM...
[LightGBM] [Info] Number of positive: 6322, number of negative: 6322
[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.004349 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 18436
[LightGBM] [Info] Number of data points in the train set: 12644, number of used features: 104
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.500000 -> initscore=0.000000
Predicting LightGBM...
LightGBM done after 0.34s

Fitting CatBoost...
Predicting CatBoost...
CatBoost done after 14.50s

In [51]:
poly_model.summary()
Out[51]:
Model Train Score Val Score Fit Time Predict Time
9 CatBoost 0.792075 0.743201 14.125572 0.078137
0 Logistic Regression 0.721686 0.743201 0.275979 0.001008
3 AdaBoost 0.728883 0.741935 6.192745 0.062771
2 Gradient Boosting 0.744622 0.736559 35.380134 0.006046
8 LightGBM 0.810978 0.728969 0.326339 0.004001
1 Random Forest 0.999921 0.725174 14.059252 0.057003
4 Extra Trees 1.000000 0.719481 3.799300 0.069373
7 XGBoost 0.901851 0.700506 0.527928 0.003000
5 Decision Tree 1.000000 0.641682 1.964353 0.002012
6 SVM 0.507039 0.488931 22.344151 5.245456

Early stopping¶

In [52]:
cat_model = CatBoostClassifier(loss_function='Logloss', task_type='GPU', random_state=42)
cat_model.fit(X_train, y_train, verbose=False, eval_set=[(X_val, y_val)], early_stopping_rounds=300)
y_pred = cat_model.predict(X_val)
print('Acurracy: ', accuracy_score(y_val, y_pred))
Acurracy:  0.7441492726122707
In [53]:
print(classification_report(y_val, y_pred))
              precision    recall  f1-score   support

         0.0       0.75      0.73      0.74      1581
         1.0       0.74      0.76      0.75      1581

    accuracy                           0.74      3162
   macro avg       0.74      0.74      0.74      3162
weighted avg       0.74      0.74      0.74      3162

Test model¶

In [54]:
X_test = df_test.copy()
X_test.drop(
  [
    'diffWardsPlaced',	'diffWardsDestroyed', 'diffHeralds', 'diffTowersDestroyed', 'diffJungleMinionsKilled'
  ], axis=1, inplace=True)
y_test = X_test.pop('friendlyWins')
In [55]:
y_pred = cat_model.predict(X_test)
print(accuracy_score(y_test, y_pred))
0.7386133603238867
In [56]:
print(classification_report(y_test, y_pred))
              precision    recall  f1-score   support

         0.0       0.74      0.73      0.74      1976
         1.0       0.73      0.75      0.74      1976

    accuracy                           0.74      3952
   macro avg       0.74      0.74      0.74      3952
weighted avg       0.74      0.74      0.74      3952

In [57]:
y_prob = cat_model.predict_proba(X_test)[:, 1]
y_prob = [round(prob*100, 2) for prob in y_prob]
In [58]:
pd.DataFrame({
  'Actual': y_test,
  'Predicted': y_pred,
  'Winning chance (%)': y_prob
})
Out[58]:
Actual Predicted Winning chance (%)
10969 1.0 1.0 65.80
8740 1.0 1.0 83.80
16778 1.0 0.0 21.26
889 1.0 1.0 73.70
4187 1.0 1.0 93.47
... ... ... ...
13962 0.0 0.0 17.74
9818 0.0 0.0 11.54
3047 0.0 0.0 25.71
18072 1.0 0.0 42.91
3030 1.0 1.0 98.09

3952 rows × 3 columns

In [ ]: