-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ccd8d1b
Showing
10 changed files
with
548 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# LoL Win Probabilities | ||
|
||
LoL Win Probabilities is a project inspired by the work of the LoL Esports team in partnership with AWS on Win Probability for Worlds 2023 (article [here](https://lolesports.com/article/dev-diary-win-probability-powered-by-aws-at-worlds/blt403ee07f98e2e0fc)) | ||
|
||
## What is LoL Win Probabilities | ||
|
||
LoL Win Probabilities uses Machine Learning to estimate a team's probability to win the game at any time of the game. The win probability is calculated by comparing the current game state with historical games in similar situations taking a large amount of metrics such as the game time and the gold difference into account. | ||
|
||
It helps to better understand the anatomy of a given game and it gives insights on the value and efficiency of different strategies and objectives during the game. | ||
|
||
<img src="images/winprob_chart.png" height="600"> | ||
|
||
## How does LoL Win Probabilities work | ||
|
||
LoL Win Probabilities is based on a Machine Learning algorithm named XGBoost that is pre-trained on a large dataset of S14 Challenger SoloQ games from EUW, KR and NA servers. | ||
|
||
The model takes into account a large set of features that represent the state of the game with as much details as possible. | ||
|
||
* Game time | ||
* Gold% (for each player) | ||
* XP% (for each player) | ||
* \# of players alive on each team | ||
* \# of turrets taken (each type of turret is counted separately) | ||
* Time before inhibitor respawn | ||
* Herald | ||
* \# of Void Grubs | ||
* \# of Dragons (each elemental dragon counted separately) | ||
* Dragon Soul | ||
* Elder buff timer (for each player) | ||
* \# of players with Elder buff | ||
* Nashor buff timer (for each player) | ||
* \# of players with Nashor buff | ||
|
||
## Win Probability vs Gold Difference | ||
|
||
The current go-to metric to estimate which team is more likely to win a game is the gold difference. The win probability takes more elements of the game into account (XP, dragons, void grubs...) and provides a more accurate and readable way to follow the state of a game. The win probability is more intuitive and doesn't need additional game knowledge to be understood whereas gold difference often needs more context. A win probability of 40% has always the same meaning during the game whereas a +2k gold difference has a different impact depending on the game time. | ||
|
||
## Further Work | ||
|
||
Further improvements can be made to the win probability model by better representing the state of the game. For example a good addition would be a team composition feature which would take champions scaling, team synergies and counters into account. Another axis to explore is to evaluate the impact of individual plays or decisions throughout the game to help players and teams to take the decision that would give them the best edge on the opponent. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from sqlalchemy import create_engine | ||
import os | ||
import pandas as pd | ||
from src.data.utils import get_games | ||
from src.data.dataset import get_competitive_match_data | ||
from src.data.features import get_features | ||
from src.models.inference import predict | ||
from src.models.utils import init_model | ||
|
||
def main(): | ||
""" Update the database with new games data """ | ||
|
||
engine = create_engine(os.environ.get('DATABASE_URL')) | ||
|
||
oldest_date = pd.to_datetime(pd.read_sql_query('SELECT MAX("DateTime_UTC") AS latest_date FROM competitive_games', con = engine)['latest_date'].values[0]).strftime('%y-%m-%d %H:%M:%S') | ||
leagues = pd.read_sql_query('SELECT "League" FROM leagues', con = engine)['League'].values | ||
new_games = get_games(leagues, oldest_date) | ||
new_games = pd.DataFrame.from_dict([item["title"] for item in new_games]) | ||
new_games.to_csv('data/new_games.csv', index = None) | ||
new_games = new_games.drop(columns = ['DateTime UTC__precision']) | ||
new_games = new_games.rename(columns = {'N GameInMatch' : 'NGameInMatch', 'DateTime UTC' : 'DateTime_UTC', 'Name' : 'Tournament'}) | ||
new_games['DateTime_UTC'] = pd.to_datetime(new_games['DateTime_UTC']) | ||
new_dataset = pd.DataFrame.from_dict(get_competitive_match_data(new_games['RiotPlatformGameId'].values, new_games['RiotVersion'])) | ||
new_features = get_features(new_dataset) | ||
model = init_model('models/winprob_soloq_competitive.json') | ||
preds = predict(model, new_features.drop(columns = ['match_id', 'winning_team']).values) | ||
new_predictions = pd.DataFrame() | ||
new_predictions['Id'] = new_dataset['match_id'] + '_' + new_features['minute'].astype(str) | ||
new_predictions['RiotPlatformGameId'] = new_dataset['match_id'] | ||
new_predictions['Minute'] = new_features['minute'] | ||
new_predictions['GoldDiff'] = new_dataset[[f'player{i}_gold' for i in range(1, 6)]].sum(axis = 1) - new_dataset[[f'player{i}_gold' for i in range(6, 11)]].sum(axis = 1) | ||
new_predictions['WinProbability'] = preds[:, 0] | ||
new_games.to_sql('competitive_games', engine, if_exists = 'append', index = None) | ||
new_predictions.to_sql('competitive_predictions', engine, if_exists = 'append', index = None) | ||
|
||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
from src.data.utils import riot_api_request | ||
import pandas as pd | ||
import mwrogue.esports_client as ec | ||
|
||
RIOT_API_KEY = 'RGAPI-faec5e54-6e5a-4dd8-a3ab-fc8746fdae31' | ||
|
||
server_mapping = {'euw1' : 'europe', 'na1' : 'americas', 'kr' : 'asia'} | ||
|
||
def get_puuids(server, tier): | ||
|
||
url = f'https://{server}.api.riotgames.com/lol/league/v4/{tier}leagues/by-queue/RANKED_SOLO_5x5?api_key={RIOT_API_KEY}' | ||
|
||
response = riot_api_request(url) | ||
data = response.json() | ||
|
||
def get_puuid(summonerId): | ||
response = riot_api_request(f'https://{server}.api.riotgames.com/lol/summoner/v4/summoners/{summonerId}?api_key={RIOT_API_KEY}') | ||
data = response.json() | ||
return data['puuid'] | ||
|
||
puuids = [get_puuid(player['summonerId']) for player in data['entries']] | ||
|
||
return puuids | ||
|
||
def get_match_ids(server, puuids, count, start_time): | ||
|
||
match_ids = [] | ||
|
||
for puuid in puuids: | ||
url = f'https://{server_mapping[server]}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?startTime={start_time}&queue=420&type=ranked&start=0&count={count}&api_key={RIOT_API_KEY}' | ||
response = riot_api_request(url) | ||
data = response.json() | ||
match_ids += data | ||
|
||
return list(set(match_ids)) | ||
|
||
def get_soloq_match_data(match_ids, existing_dataset = None): | ||
|
||
if existing_dataset is not None: | ||
dataset = existing_dataset.to_dict('records') | ||
else: | ||
dataset = [] | ||
|
||
for i, match_id in enumerate(match_ids): | ||
print(match_id) | ||
server = match_id.split('_')[0].lower() | ||
match_url = f'https://{server_mapping[server]}.api.riotgames.com/lol/match/v5/matches/{match_id}?api_key={RIOT_API_KEY}' | ||
try: | ||
match_response = riot_api_request(match_url) | ||
except Exception as e: | ||
print(e) | ||
else: | ||
match_data = match_response.json() | ||
try: | ||
end_of_game_result = match_data['info']['endOfGameResult'] | ||
except: | ||
end_of_game_result = 'GameComplete' | ||
if end_of_game_result == 'GameComplete' and match_data['info']['participants'][0]['gameEndedInEarlySurrender'] == False and match_data['info']['participants'][0]['gameEndedInSurrender'] == False and match_data['info']['gameDuration'] > 300: | ||
winning_team = match_data['info']['teams'][0]['teamId'] // 100 if match_data['info']['teams'][0]['win'] else match_data['info']['teams'][1]['teamId'] // 100 | ||
champion_picks = {} | ||
for i in range(10): | ||
champion_picks[f'player{i + 1}_champion'] = match_data['info']['participants'][i]['championName'] | ||
|
||
timeline_url = f'https://{server_mapping[server]}.api.riotgames.com/lol/match/v5/matches/{match_id}/timeline?api_key={RIOT_API_KEY}' | ||
timeline_response = riot_api_request(timeline_url) | ||
timeline = timeline_response.json() | ||
timeline_data = get_match_timeline_data(match_id, timeline, winning_team, champion_picks) | ||
dataset += timeline_data | ||
if i % 100 == 0: | ||
dataset_df = pd.DataFrame.from_dict(dataset) | ||
dataset_df.to_csv('data/dataset.csv', index = None) | ||
return dataset | ||
|
||
def get_competitive_match_data(rpg_ids, riot_versions, existing_dataset = None): | ||
|
||
if existing_dataset is not None: | ||
dataset = existing_dataset.to_dict('records') | ||
else: | ||
dataset = [] | ||
|
||
client = ec.EsportsClient('lol') | ||
|
||
for i in range(len(rpg_ids)): | ||
rpg_id = rpg_ids[i] | ||
riot_version = riot_versions[i] | ||
if rpg_id is not None and riot_version is not None: | ||
print(rpg_id) | ||
riot_version = int(riot_version) | ||
timeline = {} | ||
data, timeline['info'] = client.get_data_and_timeline(rpg_id, riot_version) | ||
winning_team = 1 if data['teams'][0]['win'] else 2 | ||
champion_picks = {} | ||
for i in range(10): | ||
champion_picks[f'player{i + 1}_champion'] = data['participants'][i]['championName'] | ||
timeline_data = get_match_timeline_data(rpg_id, timeline, winning_team, champion_picks) | ||
dataset += timeline_data | ||
else: | ||
print('None RiotPlatformGameId') | ||
return dataset | ||
|
||
|
||
def get_match_timeline_data(match_id, timeline, winning_team, champion_picks): | ||
|
||
"""Base Respawn Wait time (BRW)""" | ||
brw = [6, 6, 8, 8, 10, 12, 16, 21, 26, 32.5, 35, 37.5, 40, 42.5, 45, 47.5, 50, 52.5] | ||
|
||
def get_tif(timestamp): | ||
"""Get the Time Increase Factor (TIF) for Respawn Wait time""" | ||
minutes = timestamp // 60000 | ||
if minutes < 15: | ||
tif = 0 | ||
elif minutes < 30: | ||
tif = 0 + 2 * (minutes - 15) * 0.425 / 100 | ||
elif minutes < 45: | ||
tif = 12.75 + 2 * (minutes - 30) * 0.3 / 100 | ||
else: | ||
tif = 21.75 + 2 * (minutes - 45) * 1.45 / 100 | ||
return tif | ||
|
||
rows = [] | ||
|
||
respawn_tracker = {'player1_respawn' : 0, 'player2_respawn' : 0, 'player3_respawn' : 0, 'player4_respawn' : 0, 'player5_respawn' : 0, 'player6_respawn' : 0, 'player7_respawn' : 0, 'player8_respawn' : 0, 'player9_respawn' : 0, 'player10_respawn' : 0} | ||
turret_tracker = {'team1_outerTurret' : 0, 'team1_innerTurret' : 0, 'team1_baseTurret' : 0, 'team1_nexusTurret' : 0, 'team2_outerTurret' : 0, 'team2_innerTurret' : 0, 'team2_baseTurret' : 0, 'team2_nexusTurret' : 0} | ||
inhibitor_respawn_tracker = {'team1_topInhibitor' : 0, 'team1_midInhibitor' : 0, 'team1_botInhibitor' : 0, 'team2_topInhibitor' : 0, 'team2_midInhibitor' : 0, 'team2_botInhibitor' : 0} | ||
dragon_tracker = {'team1_airDragon' : 0, 'team1_fireDragon' : 0, 'team1_hextechDragon' : 0, 'team1_chemtechDragon' : 0, 'team1_earthDragon' : 0, 'team1_waterDragon' : 0, 'team2_airDragon' : 0, 'team2_fireDragon' : 0, 'team2_hextechDragon' : 0, 'team2_chemtechDragon' : 0, 'team2_earthDragon' : 0, 'team2_waterDragon' : 0} | ||
dragon_soul_tracker = {'team1_airDragonSoul' : 0, 'team1_fireDragonSoul' : 0, 'team1_hextechDragonSoul' : 0, 'team1_chemtechDragonSoul' : 0, 'team1_earthDragonSoul' : 0, 'team1_waterDragonSoul' : 0, 'team2_airDragonSoul' : 0, 'team2_fireDragonSoul' : 0, 'team2_hextechDragonSoul' : 0, 'team2_chemtechDragonSoul' : 0, 'team2_earthDragonSoul' : 0, 'team2_waterDragonSoul' : 0} | ||
elder_dragon_tracker = {'team1_elderDragon' : 0, 'team2_elderDragon' : 0} | ||
elder_buff_tracker = {'player1_elderDragon' : 0, 'player2_elderDragon' : 0, 'player3_elderDragon' : 0, 'player4_elderDragon' : 0, 'player5_elderDragon' : 0, 'player6_elderDragon' : 0, 'player7_elderDragon' : 0, 'player8_elderDragon' : 0, 'player9_elderDragon' : 0, 'player10_elderDragon' : 0} | ||
nashor_tracker = {'team1_nashor' : 0, 'team2_nashor' : 0} | ||
nashor_buff_tracker = {'player1_nashor' : 0, 'player2_nashor' : 0, 'player3_nashor' : 0, 'player4_nashor' : 0, 'player5_nashor' : 0, 'player6_nashor' : 0, 'player7_nashor' : 0, 'player8_nashor' : 0, 'player9_nashor' : 0, 'player10_nashor' : 0} | ||
grub_tracker = {'team1_grub' : 0, 'team2_grub' : 0} | ||
herald_tracker = {'team1_herald' : 0, 'team2_herald' : 0} | ||
|
||
current_stats = {} | ||
current_stats['match_id'] = match_id | ||
current_stats['winning_team'] = winning_team | ||
# Do not consider the last frame that corresponds to end game (nothing to predict and high bias with nexus turret destroyed?) | ||
for frame in timeline['info']['frames'][:-1]: | ||
current_stats['timestamp'] = frame['timestamp'] | ||
for event in frame['events']: | ||
if event['type'] == 'LEVEL_UP': | ||
current_stats[f'player{event["participantId"]}_level'] = event['level'] | ||
elif event['type'] == 'CHAMPION_KILL': | ||
victim_id = event['victimId'] | ||
victim_level = current_stats[f'player{victim_id}_level'] | ||
kill_timestamp = event['timestamp'] | ||
respawn_time = (brw[victim_level - 1] + brw[victim_level - 1] * get_tif(kill_timestamp)) * 1000 | ||
respawn_tracker[f'player{int(victim_id)}_respawn'] = kill_timestamp + respawn_time | ||
# Remove Nashor and Elder Dragon buffs | ||
nashor_buff_tracker[f'player{int(victim_id)}_nashor'] = 0 | ||
elder_buff_tracker[f'player{int(victim_id)}_elderDragon'] = 0 | ||
elif event['type'] == 'BUILDING_KILL': | ||
if event['buildingType'] == 'TOWER_BUILDING': | ||
turret_type = event['towerType'].split('_')[0].lower() | ||
turret_tracker[f'team{event["teamId"] // 100}_{turret_type}Turret'] += 1 | ||
elif event['buildingType'] == 'INHIBITOR_BUILDING': | ||
lane = event['laneType'].split('_')[0].lower() | ||
inhibitor_respawn_tracker[f'team{event["teamId"] // 100}_{lane}Inhibitor'] = event['timestamp'] + 5 * 60000 | ||
elif event['type'] == 'ELITE_MONSTER_KILL': | ||
if event['monsterType'] == 'DRAGON': | ||
if event['monsterSubType'] == 'ELDER_DRAGON': | ||
elder_dragon_tracker[f'team{event["killerTeamId"] // 100}_elderDragon'] = event['timestamp'] + 150 * 1000 | ||
# Check players alive to give buff | ||
if event["killerTeamId"] == 100: | ||
for i in range(1, 6): | ||
if respawn_tracker[f'player{i}_respawn'] <= event['timestamp']: | ||
elder_buff_tracker[f'player{i}_elderDragon'] = event['timestamp'] + 150 * 1000 | ||
elif event["killerTeamId"] == 200: | ||
for i in range(6, 11): | ||
if respawn_tracker[f'player{i}_respawn'] <= event['timestamp']: | ||
elder_buff_tracker[f'player{i}_elderDragon'] = event['timestamp'] + 150 * 1000 | ||
else: | ||
dragon_type = event['monsterSubType'].split('_')[0].lower() | ||
dragon_tracker[f'team{event["killerTeamId"] // 100}_{dragon_type}Dragon'] += 1 | ||
if dragon_tracker[f'team{event["killerTeamId"] // 100}_airDragon'] + dragon_tracker[f'team{event["killerTeamId"] // 100}_fireDragon'] + dragon_tracker[f'team{event["killerTeamId"] // 100}_hextechDragon'] + dragon_tracker[f'team{event["killerTeamId"] // 100}_chemtechDragon'] + dragon_tracker[f'team{event["killerTeamId"] // 100}_earthDragon'] + dragon_tracker[f'team{event["killerTeamId"] // 100}_waterDragon'] == 4: | ||
dragon_soul_tracker[f'team{event["killerTeamId"] // 100}_{dragon_type}DragonSoul'] = 1 | ||
elif event['monsterType'] == 'BARON_NASHOR': | ||
nashor_tracker[f'team{event["killerTeamId"] // 100}_nashor'] = event['timestamp'] + 180 * 1000 | ||
# Check players alive to give buff | ||
if event["killerTeamId"] == 100: | ||
for i in range(1, 6): | ||
if respawn_tracker[f'player{i}_respawn'] <= event['timestamp']: | ||
nashor_buff_tracker[f'player{i}_nashor'] = event['timestamp'] + 180 * 1000 | ||
elif event["killerTeamId"] == 200: | ||
for i in range(6, 11): | ||
if respawn_tracker[f'player{i}_respawn'] <= event['timestamp']: | ||
nashor_buff_tracker[f'player{i}_nashor'] = event['timestamp'] + 180 * 1000 | ||
elif event['monsterType'] == 'HORDE': | ||
if event['killerTeamId'] in [100, 200]: | ||
grub_tracker[f'team{event["killerTeamId"] // 100}_grub'] += 1 | ||
elif event['monsterType'] == 'RIFTHERALD': | ||
if event['killerTeamId'] in [100, 200]: | ||
herald_tracker[f'team{event["killerTeamId"] // 100}_herald'] = 1 | ||
elif event['type'] == 'ITEM_DESTROYED': | ||
if event['itemId'] == 3513: | ||
herald_tracker[f'team{int(event["participantId"] > 5) + 1}_herald'] = 0 | ||
|
||
|
||
participantFrames = frame['participantFrames'] | ||
|
||
for i in range(1, 11): | ||
participant = str(i) | ||
current_stats[f'player{participant}_gold'] = participantFrames[participant]['totalGold'] | ||
current_stats[f'player{participant}_xp'] = participantFrames[participant]['xp'] | ||
current_stats[f'player{participant}_level'] = participantFrames[participant]['level'] | ||
|
||
if respawn_tracker[f'player{int(participant)}_respawn'] > frame['timestamp']: | ||
current_stats[f'player{participant}_isAlive'] = 0 | ||
else: | ||
current_stats[f'player{participant}_isAlive'] = 1 | ||
rows.append({**current_stats, **champion_picks, **respawn_tracker, **turret_tracker, **inhibitor_respawn_tracker, **dragon_tracker, **dragon_soul_tracker, **elder_dragon_tracker, **elder_buff_tracker, **nashor_tracker, **nashor_buff_tracker, **herald_tracker, **grub_tracker}) | ||
return rows |
Oops, something went wrong.