Розробка алгоритму прогнозування результатів матчів Dota 2, частина 2: Розширення набору даних і додавання нових функцій
«Шлях постійного вдосконалення живиться бажанням бути кращим ніж вчора, а завтра — кращим ніж сьогодні». — Анонім
Автор: www.deviantart.com/jirojh
Всім привіт! Сьогодні ми зануримося у другу частину нашого грандіозного МН-проєкту!
У першій частині цього циклу статей я розповів про основні етапи побудови алгоритму прогнозування результатів матчів Dota 2, зосередившись на зборі даних та базовому налаштуванні моделі. Зокрема, я розповів про підготовку початкових даних, вибір основних характеристик і створення базової моделі, а також обговорив основні ідеї та виклики, з якими довелося зіткнутися в процесі роботи. Ці кроки сформували міцний фундамент для прогнозування, але ми можемо зробити ще більше, щоб підвищити ефективність та зручність використання моделі. У другій частині я зосереджуся на розширенні набору даних і включенні нових функцій, орієнтованих на користувача, щоб підвищити точність прогнозування і зручність використання, пропонуючи гравцям більш інтерактивний досвід і більш глибоке розуміння динаміки матчів в Dota 2.
Передове конструювання ознак у прогнозуванні результатів матчів Dota 2
У першій частині цього циклу статей я заклав основу для прогнозування результатів матчів у Dota 2, зібравши основні метрики, вибравши базові ознаки та побудувавши початкову модель прогнозування. Хоча це стало відправною точкою, я розумів, що є ще багато чого, що ми можемо зробити, щоб підвищити прогностичну ефективність моделі, заглибившись в уточнення даних та конструювання ознак.
У другій частині я розповім про передові методи конструювання функцій, які перетворили початковий набір даних на більш змістовну та ефективну версію. Ці вдосконалення мають на меті відобразити складну динаміку командної гри в Dota 2, від індивідуальної статистики гравців до загальної стратегії команди.
1. Агреговані ознаки команди
Одним з ключових вдосконалень другої частини є розрахунок агрегованих ознак на рівні команди. Замість того, щоб користуватися індивідуальною статистикою гравців для кожної команди, я реалізував функцію усереднення важливих метрик між гравцями, щоб відобразити єдину командну ефективність. Сюди входять середні значення для:
- Відсоток перемог героя (для відображення ефективності вибору героя),
- Вбивства, смерті та асисти (KDA) та
- GPM (золото за хвилину) і XPM (досвід за хвилину).
Такий підхід дозволяє моделі вважати команди згуртованими одиницями, а не сукупністю окремих осіб, що дає їй ширший контекст при прогнозуванні результатів.
2. Впорядковані дані зі скороченням ознак
У першій частині набір даних включав численні індивідуальні статистичні дані по гравцях і командах. Однак для більш ефективної моделі я відмовився від багатьох з цих проміжних показників, об’єднавши їх. Консолідуючи дані в ключові ознаки, такі як середня кількість вбивств або приріст досвіду на рівні команди, модель фокусується на основній інформації без зайвих деталей. Таке скорочення ознак спрощує набір даних, дозволяючи моделі краще узагальнювати та швидше навчатися, не втрачаючи при цьому релевантних висновків.
3. Масштабування та нормалізація для узгодженості
Щоб гарантувати пропорційний внесок усіх ознак у прогнози, я застосував масштабування MinMax scaling для нормалізації значень по всьому набору даних. Шляхом перетворення даних у загальну шкалу від 0 до 1 цей процес забезпечує узгодженість у навчанні моделі та зменшує чутливість до значних числових розбіжностей між різними метриками. Щоб забезпечити аналогічну обробку майбутніх наборів даних, після початкового налаштування модель зберігає значення масштабу. Це дозволяє нам послідовно застосовувати ті самі перетворення в майбутньому, забезпечуючи відповідність нових даних початковому набору даних і запобігаючи викривленню результатів при введенні оновлених даних.
4. Додавання нових ознак для більшої глибини
І насамкінець, я розширив набір даних додатковими ознаками, які охоплюють аспекти командної стратегії та контролю над картою. Ключові нові ознаки включають:
- Участь у командному бою: демонстрація командної згуртованості в боях,
- Чистий капітал: як показник загальної економічної ефективності кожної команди,
- Розміщення observer та sentry ward: для відображення контролю над картою та стратегії огляду.
Ці особливості дозволяють моделі глибше вивчити динаміку команд, враховуючи не лише сиру статистику, а й ігрові тактики, які впливають на результати матчів.
Візуалізація: Розуміння важливості ознак
Візуалізація даних є важливим кроком у розумінні взаємозв’язків між ознаками та цільовою змінною, особливо в такій складній грі, як Dota 2. У цьому розділі я продемонструю, як візуалізувати кореляцію між різними ознаками та результатом матчу, зокрема, зосередившись на стовпці «radiant_win». Це допоможе визначити, які ознаки є найвпливовішими у прогнозуванні результатів матчів, і правильно скерувати подальші зусилля в конструюванні ознак.
Для цього я використав кореляційну матрицю та візуалізував її за допомогою теплової карти. Нижче наведено скрипт Python, який я використовував для завантаження набору даних, підготовки даних і створення теплової карти:
import os
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from structure.helpers import prepare_match_prediction_data
pd.set_option("display.max_columns", None)
file_path = os.path.join("..", "dataset", "train_data", "all_data_match_predict.csv")
scaler_path = "../scaler.pkl"
# Load and prepare the dataset
df = pd.read_csv(file_path)
df = prepare_match_prediction_data(df, scaler_path)
# Specify the features and target column
# features = df.columns[:-1].tolist() # All columns except the last one
target = "radiant_win" # Change to your target column name
features = df.columns.drop(target).tolist()
corr_matrix = df.corr()
# Visualize with a heatmap
plt.figure(figsize=(12, 10))
sns.heatmap(
corr_matrix[["radiant_win"]].sort_values(by="radiant_win", ascending=False),
annot=True,
cmap="coolwarm",
)
plt.title("Correlation of Features with Radiant Win")
plt.show()
Пояснення щодо коду:
- Завантаження та підготовка даних: Набір даних завантажується з файлу CSV, а функція prepare_match_prediction_data викликається для попередньої обробки даних, включаючи масштабування та нормалізацію.
- Кореляційна матриця: Кореляційна матриця розраховується за допомогою функції corr() з Pandas, яка вимірює, наскільки сильно ознаки пов’язані між собою та з цільовою змінною «radiant_win».
- Візуалізація теплової карти: Теплова карта створюється за допомогою функції Seaborn’s heatmap(). Кореляційні значення з «radiant_win» відсортовані в порядку спадання, що дозволяє швидко визначити, які ознаки мають найсильніші позитивні або негативні кореляції з цільовим показником.
- Інтерпретація теплової карти: Теплова карта забезпечує візуальне розуміння важливості ознак. Елементи, які сильно корелюють (позитивно чи негативно) з результатом «radiant_win» можуть бути пріоритетними для подальшого налаштування моделі або конструювання ознак.
Висновки на основі візуалізації

Рис.: Кореляція ознак з результатом «radiant_win».
Вивчаючи теплову карту, ми можемо отримати цінну інформацію про те, які ознаки можуть найкраще передбачити успіх команди Radiant. Наприклад, якщо певні метрики, такі як середня кількість вбивств або участь у командних боях, демонструють сильну позитивну кореляцію з «radiant_win», це може бути важливою ознакою, на якій слід зосередитися в майбутніх ітераціях моделі.
Проаналізувавши кореляцію теплової карти, пов’язану зі стовпцем radiant_win, можна виявити кілька суттєвих тенденцій щодо того, як різні ознаки впливають на результат команди Radiant.
- Золото за хвилину (GPM): GPM команди Radiant показує сильну позитивну кореляцію з перемогою. Це свідчить про те, що коли гравці Radiant ефективно накопичують золото, вони можуть собі дозволити кращі предмети та апгрейди, що підвищує їхню загальну силу під час матчу.
- Досвід за хвилину (XPM): Як і GPM, XPM також позитивно корелює з перемогами команди Radiant. Більший приріст досвіду за хвилину дає змогу гравцям команди Radiant швидше підвищувати рівень, відкриваючи важливі навички та здібності, які можуть переломити хід бою.
- KDA (Вбивства, смерті, асисти): Співвідношення KDA для гравців команди Radiant також демонструє сильну позитивну кореляцію з перемогою. Високе значення KDA свідчить про те, що гравці не лише забезпечують собі вбивства та асисти, але й зводять до мінімуму смертність. Ця метрика відображає як індивідуальну ефективність, так і ефективну командну координацію, що має важливе значення в конкурентних матчах.
Ці позитивні кореляції означають, що коли команда Radiant досягає успіху в GPM, XPM і KDA, їхні шанси на перемогу значно зростають. Цей висновок підкреслює важливість цих метрик для оцінки ефективності команди.
Цікаво, що хоча аналіз зосереджений на команді Radiant, важливо порівняти її з показниками команди Dire. І хоча ми не будемо заглиблюватися в їхні негативні кореляції, висновок очевидний: коли команда Dire демонструє нижчі показники GPM, XPM та KDA, це зменшує їхню ймовірність перемоги.
Цей крок візуалізації не тільки покращує наше розуміння набору даних, але також надає інформацію щодо наших наступних кроків у вдосконаленні моделі та виборі ознак. Розуміючи взаємозв’язок між цими метриками та стовпцем radiant_win, ми можемо вдосконалити нашу модель, щоб визначити пріоритетні характеристики, які суттєво впливають на ймовірність успіху команди Radiant. Вдосконалюючи ці ознаки за допомогою збору даних та конструювання, ми можемо побудувати більш надійну модель прогнозування, яка точно відображає динаміку ігрового процесу.
Підвищення класу гравця для прогнозування матчів Dota 2
У цьому розділі я розповім вам про вдосконалення, внесені до класу гравців Player, який є ключовим для нашої моделі прогнозування результатів матчів у Dota 2. Удосконалення стосуються надійного пошуку даних, кращої статистичної обробки та покращеного відображення об’єктів, що в цілому збагачує можливості аналізу гравців.
Ключові вдосконалення
1. Ініціалізація та обробка даних: Оновлений клас Player ініціалізує атрибути гравця безпосередньо з заданого словника player_data. За відсутності даних він обнуляє всю статистику і отримує загальні дані гравця з останніх матчів. Такий подвійний підхід не тільки забезпечує гнучкість у створенні об’єктів, але й гарантує, що ми зможемо легко впоратися з ситуаціями, коли дані про гравця відсутні.
def __init__(self, account_id, name, hero_id, team, player_data=None): self.account_id = account_id self.team = team self.hero = Hero(hero_id) self.name = name if player_data: self.load_player_data(player_data) else: self.reset_stats() self.get_player_total_data()
Цей метод ініціалізації є більш чітким і полегшує оновлення, коли дані гравця доступні.
2. Ефективна агрегація даних: Новий метод get_player_total_data передбачає більш системний підхід до отримання та накопичення статистичних даних про гравців. Він отримує останні дані про матч з механізмом повторних спроб з метою забезпечення надійності, гарантуючи, що якщо дані не можуть бути отримані з першого разу, система зробить кілька спроб, перш ніж зазнає невдачі.
Наприклад, під час накопичення даних лічильники оновлюються лише тоді, коли дані про гравців успішно отримані, що забезпечує точне статистичне представлення без ризику ділення на нуль при обчисленні середніх значень.
def get_player_total_data(self):
"""Fetch player total data with retries on match data retrieval."""
recent_matches = self.fetch_recent_matches()
# Initialize counters for averages
participation_count = obs_count = sen_count = net_worth_count = 0
kills_count = deaths_count = assists_count = roshan_count = 0
last_hits_count = denies_count = gpm_count = xpm_count = level_count = 0
hero_damage_count = tower_damage_count = healing_count = 0
# Iterate through recent matches
for match in recent_matches:
match_id = match["match_id"]
match_data = self.fetch_match_data_with_retries(match_id)
if match_data is None:
logger.warning(f"Skipping match {match_id} after 5 attempts")
continue # Skip the match if it couldn't be retrieved
# Get player data
player_data = self.get_player_data(match_data)
if player_data:
logger.debug(
f"Processing match data for match ID {match_id}: {player_data}"
)
# Accumulate values safely
participation_count = self.accumulate_value(
player_data, "teamfight_participation", participation_count
)
obs_count = self.accumulate_value(player_data, "obs_placed", obs_count)
sen_count = self.accumulate_value(player_data, "sen_placed", sen_count)
net_worth_count = self.accumulate_value(
player_data, "net_worth", net_worth_count
)
kills_count = self.accumulate_value(player_data, "kills", kills_count)
deaths_count = self.accumulate_value(
player_data, "deaths", deaths_count
)
assists_count = self.accumulate_value(
player_data, "assists", assists_count
)
roshan_count = self.accumulate_value(
player_data, "roshans_killed", roshan_count
)
last_hits_count = self.accumulate_value(
player_data, "last_hits", last_hits_count
)
denies_count = self.accumulate_value(
player_data, "denies", denies_count
)
gpm_count = self.accumulate_value(
player_data, "gold_per_min", gpm_count
)
xpm_count = self.accumulate_value(player_data, "xp_per_min", xpm_count)
level_count = self.accumulate_value(player_data, "level", level_count)
hero_damage_count = self.accumulate_value(
player_data, "hero_damage", hero_damage_count
)
tower_damage_count = self.accumulate_value(
player_data, "tower_damage", tower_damage_count
)
healing_count = self.accumulate_value(
player_data, "hero_healing", healing_count
)
# Safely divide by the number of successful additions for each field
self.teamfight_participation = self.calculate_average(
self.teamfight_participation, participation_count
)
self.obs_placed = self.calculate_average(self.obs_placed, obs_count)
self.sen_placed = self.calculate_average(self.sen_placed, sen_count)
self.net_worth = self.calculate_average(self.net_worth, net_worth_count)
self.kills = self.calculate_average(self.kills, kills_count)
self.deaths = self.calculate_average(self.deaths, deaths_count)
self.assists = self.calculate_average(self.assists, assists_count)
self.roshans_killed = self.calculate_average(self.roshans_killed, roshan_count)
self.last_hits = self.calculate_average(self.last_hits, last_hits_count)
self.denies = self.calculate_average(self.denies, denies_count)
self.gold_per_min = self.calculate_average(self.gold_per_min, gpm_count)
self.xp_per_min = self.calculate_average(self.xp_per_min, xpm_count)
self.level = self.calculate_average(self.level, level_count)
self.hero_damage = self.calculate_average(self.hero_damage, hero_damage_count)
self.tower_damage = self.calculate_average(
self.tower_damage, tower_damage_count
)
self.hero_healing = self.calculate_average(self.hero_healing, healing_count)
3. Статистичні функції: Для оптимізації процесу оновлення статистики та обчислення середніх значень введено набір утиліт, таких як accumulate_value та calculate_average. Така модульність покращує читабельність коду та зручність його обслуговування. Обчислення інкапсульовані у функції, що полегшує модифікацію або розширення статистичних методів у майбутньому.
def accumulate_value(self, player_data, key, count): if key in player_data: setattr(self, key, getattr(self, key) + player_data[key]) count += 1 return count def calculate_average(self, total, count): return total / count if count > 0 else 0
Гіперпослиння на увесь файл
Висновки
Дані вдосконалення класу Player покликані забезпечити більш повну та надійну основу для аналізу продуктивності гравців у матчах Dota 2. Завдяки модульному підходу до обробки даних і чіткому статистичному представленню, ми створили основу для покращених можливостей прогнозування в нашій моделі.
Інтеграція повторних спроб під час отримання даних також готує систему до роботи з потенційними невідповідностями в доступності даних, забезпечуючи безперебійний користувацький досвід. По мірі того, як ми продовжуємо вдосконалювати нашу модель, ці оновлення класу Player допоможуть нам отримати більш точні прогнози та глибше зрозуміти поведінку гравців і динаміку команди.
Покращення класів героїв для аналізу контр-піків
Щоб розробити комплексну ознаку піків героя, ми модифікували наш існуючий клас Hero з попередніх статей, включивши в нього дані контр-піків проти героїв-суперників. Нижче наведено огляд модифікованого класу Hero та його додаткових можливостей для оцінки сили контр-піків.
Модифікований клас Hero
Клас Hero було розширено для підтримки оцінок контр-піків проти інших героїв. Ось короткий опис ключових компонентів і методів в цій оновленій версії:
- Розрахунок відсотку перемог: Клас обчислює відсоток перемог кожного героя у професійних матчах.
- Дані по контр-пікам: Метод set_counter_pick_data аналізує відсоток перемог героя проти конкретних героїв команди суперника, додаючи ці значення до даних героя.
Це оновлення дозволяє нам динамічно встановлювати дані контр-піків на основі конкретних ворожих героїв, надаючи нашій моделі більш детальне уявлення про потенційну ефективність кожного героя.
Оновлений код для класу Hero
Нижче наведено повний клас Hero із застосуванням методу set_counter_pick_data:
class Hero:
def __init__(self, hero_id):
self.hero_id = hero_id
self.features = self.get_hero_features()
self.name = self.features["name"] if self.features else "Unknown Hero"
self.counter_picks = []
if self.features and self.features["pro_pick"] > 0:
self.winrate = self.features["pro_win"] / self.features["pro_pick"]
else:
self.winrate = 0
logger.info(f"Initialized Hero: {self}")
def get_hero_features(self):
url = f"https://api.opendota.com/api/heroStats?api_key={opendota_key}"
logger.info(f"Fetching hero features for Hero ID: {self.hero_id}")
response = requests.get(url)
if response.status_code == 200:
heroes = response.json()
for hero in heroes:
if hero["id"] == self.hero_id:
logger.info(
f"Hero features retrieved for ID {self.hero_id}: {hero}"
)
return {
"hero_id": hero["id"],
"name": hero["localized_name"],
"pro_win": hero.get("pro_win", 0),
"pro_pick": hero.get("pro_pick", 0),
}
else:
logger.error(f"Error fetching hero features: {response.status_code}")
return None
def get_hero_matchups(self):
url = f"https://api.opendota.com/api/heroes/{self.hero_id}/matchups?api_key={opendota_key}"
logger.info(f"Fetching matchups for Hero ID: {self.hero_id}")
response = requests.get(url)
if response.status_code == 200:
hero_matchups = response.json()
logger.info(f"Matchups retrieved for Hero ID {self.hero_id}.")
return hero_matchups
else:
logger.error(f"Error fetching hero matchups: {response.status_code}")
return None
def set_counter_pick_data(self, hero_against_ids):
logger.info(f"Setting counter pick data for Hero ID: {self.hero_id}")
hero_matchups = self.get_hero_matchups()
if hero_matchups:
for hero_matchup in hero_matchups:
if hero_matchup["hero_id"] in hero_against_ids:
win_rate = (
hero_matchup["wins"] / hero_matchup["games_played"]
if hero_matchup["games_played"] > 0
else 0
)
self.counter_picks.append(
{"win_rate": win_rate, "hero_id": hero_matchup["hero_id"]}
)
logger.info(
f"Added counter pick for Hero ID: {hero_matchup['hero_id']} with win rate: {win_rate:.2f}"
)
else:
logger.warning(f"No matchups found for Hero ID: {self.hero_id}")
def __repr__(self):
return f"Hero(ID: {self.hero_id}, Name: {self.name}, Features: {self.features})"
Гіперпослиння на увесь файл
Дані піків героїв в класі матчу Match
Щоб динамічно готувати дані про піки героїв для прогнозування, ми додали функцію get_hero_match_data_for_prediction до класу Match. Ця функція обробляє шанси на перемогу контр-піків і шанси на перемогу героя в контексті конкретного матчу:
def get_hero_match_data_for_prediction(self):
if len(self.radiant_team.players) == 5 and len(self.dire_team.players) == 5:
dire_hero_ids = [player.hero.hero_id for player in self.dire_team.players]
radiant_hero_ids = [player.hero.hero_id for player in self.radiant_team.players]
for player in self.dire_team.players:
player.hero.set_counter_pick_data(radiant_hero_ids)
for player in self.radiant_team.players:
player.hero.set_counter_pick_data(dire_hero_ids)
match_data = {
"match_id": self.match_id,
"radiant_team_id": self.radiant_team.team_id,
"radiant_team_name": self.radiant_team.team_name,
"dire_team_id": self.dire_team.team_id,
"dire_team_name": self.dire_team.team_name,
}
# Compile hero data for each player on both teams
for i, player in enumerate(self.radiant_team.players):
match_data[f"radiant_player_{i + 1}_hero_id"] = player.hero.hero_id
match_data[f"radiant_player_{i + 1}_hero_winrate"] = player.hero.winrate
for n, counter_pick in enumerate(player.hero.counter_picks):
match_data[f"radiant_hero_{i + 1}_{n + 1}_counter_pick"] = (
counter_pick["win_rate"]
)
for i, player in enumerate(self.dire_team.players):
match_data[f"dire_player_{i + 1}_hero_id"] = player.hero.hero_id
match_data[f"dire_player_{i + 1}_hero_winrate"] = player.hero.winrate
for n, counter_pick in enumerate(player.hero.counter_picks):
match_data[f"dire_hero_{i + 1}_{n + 1}_counter_pick"] = (
counter_pick["win_rate"]
)
df = pd.DataFrame([match_data])
df = prepare_hero_pick_data(df)
return df
else:
raise ValueError("Both teams must have exactly 5 players.")
Гіперпослиння на увесь файл
Скрипт генерації набору даних: Інтеграція даних про піки героїв
Для формування навчальних даних ми розширили скрипт генерації набору даних, включивши в нього дані про контр-піки кожного героя. Це покращення допомагає моделі розпізнавати сприятливі та несприятливі матчапи для кожного героя в межах конкретного матчу.
Нижче наведено фрагмент коду скрипту генерації набору даних:
def generate_dataset():
api = OpenDotaApi()
dataset = []
premium_leagues = api.set_premium_leagues()
for premium_league in premium_leagues:
league_id = premium_league["leagueid"]
league_name = premium_league["name"]
tournament = Tournament(league_id=league_id, name=league_name)
tournament.get_league_matches()
for match in tournament.matches:
radiant_team = match.radiant_team
dire_team = match.dire_team
if len(radiant_team.players) == 5 and len(dire_team.players) == 5:
match_data = {
"match_id": match.match_id,
"radiant_team_id": radiant_team.team_id,
"radiant_team_name": radiant_team.team_name,
"dire_team_id": dire_team.team_id,
"dire_team_name": dire_team.team_name,
"radiant_win": match.radiant_win,
}
for i, player in enumerate(radiant_team.players):
match_data[f"radiant_player_{i + 1}_hero_id"] = (
player.hero.hero_id
)
match_data[f"radiant_player_{i + 1}_hero_winrate"] = (
player.hero.winrate
)
for n, counter_pick in enumerate(player.hero.counter_picks):
match_data[f"radiant_hero_{i + 1}_{n + 1}_counter_pick"] = (
counter_pick["win_rate"]
)
for i, player in enumerate(dire_team.players):
match_data[f"dire_player_{i + 1}_hero_id"] = (
player.hero.hero_id
)
match_data[f"dire_player_{i + 1}_hero_winrate"] = (
player.hero.winrate
)
for n, counter_pick in enumerate(player.hero.counter_picks):
match_data[f"dire_hero_{i + 1}_{n + 1}_counter_pick"] = (
counter_pick["win_rate"]
)
dataset.append(match_data)
df = pd.DataFrame(dataset)
df.to_csv("hero_pick_dataset.csv", index=False)
print("Hero pick dataset generated and saved.")
Гіперпослиння на увесь файл
Підготовка даних: Створення ознак героя
Після створення набору даних ми готуємо їх до аналізу та моделювання. Це включає в себе створення ознак, які підсумовують результати діяльності героїв для кожної команди та сприяють кращому прогнозному моделюванню.
Нижче наведено функції, які використовуються для підготовки даних, включаючи створення ознак героя:
def create_hero_features(df, team_prefix):
logger.info(f"Creating hero features for {team_prefix}")
try:
hero_columns = [
f"{team_prefix}_hero_{i}_{n}_counter_pick"
for i in range(1, 6)
for n in range(1, 6)
]
hero_winrate_columns = [
f"{team_prefix}_player_{i}_hero_winrate" for i in range(1, 6)
]
df[f"{team_prefix}_avg_counter_pick"] = df[hero_columns].mean(axis=1)
df[f"{team_prefix}_avg_hero_winrate"] = df[hero_winrate_columns].mean(axis=1)
df.drop(columns=hero_columns + hero_winrate_columns, inplace=True)
logger.info(f"Hero features created and columns dropped for {team_prefix}")
except Exception as e:
logger.error(f"Error in create_hero_features for {team_prefix}: {e}")
return df
def prepare_hero_pick_data(df):
logger.info("Preparing hero pick data")
try:
df = create_hero_features(df, "radiant")
df = create_hero_features(df, "dire")
try:
df["radiant_win"] = df["radiant_win"].astype(int)
except KeyError:
logger.warning("radiant_win column missing")
df.drop(
columns=[
"match_id",
"radiant_team_id",
"radiant_team_name",
"dire_team_id",
"dire_team_name",
*[f"radiant_player_{i}_hero_id" for i in range(1, 6)],
*[f"radiant_player_{i}_hero_name" for i in range(1, 6)],
*[f"dire_player_{i}_hero_id" for i in range(1, 6)],
*[f"dire_player_{i}_hero_name" for i in range(1, 6)],
],
inplace=True,
)
logger.info("Hero pick data prepared and relevant columns dropped")
except Exception as e:
logger.error(f"Error in prepare_hero_pick_data: {e}")
return df
Гіперпослиння на увесь файл
Пояснення щодо підготовки даних
- Створення ознак героя:
- Функція create_hero_features обчислює середні значення контр-піків і шанси на перемогу для кожної команди шляхом усереднення відповідних стовпців.
- Вона будує нові стовпці для середніх показників контр-піків і відсотка перемог героїв, відкидаючи початкові детальні стовпці для впорядкування набору даних.
2. Фінальна підготовка даних:
- Функція prepare_hero_pick_data керує процесом підготовки, викликаючи create_hero_features для обох команд.
- Вона гарантує, що стовпець radiant_win має правильний формат, відкидаючи несуттєві стовпці, щоб зберегти лише найбільш значущі для моделювання ознаки.
Такий структурований підхід до генерації та підготовки даних закладає міцний фундамент для тренування моделей машинного навчання, які можуть передбачати результати матчів на основі піків та контр-піків героїв.
Візуалізація даних про піки героїв
Після того, як ми згенерували набір даних з даними про піки та контр-піки героїв, ми візуалізували кореляції ознак, щоб зрозуміти, які матчапи можуть впливати на результати матчів. Нижче наведено код для візуалізації кореляції ознак з результатами матчів:
import os
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
file_path = os.path.join("..", "dataset", "train_data", "hero_pick_dataset.csv")
df = pd.read_csv(file_path)
# Compute correlation matrix
corr_matrix = df.corr()
# Visualize with a heatmap
plt.figure(figsize=(24, 20))
sns.heatmap(
corr_matrix[["radiant_win"]].sort_values(by="radiant_win", ascending=False),
annot=True,
cmap="coolwarm",
)
plt.title("Correlation of Features with Radiant Win")
plt.show()
Гіперпослиння на увесь файл
Ця візуалізація дає уявлення про те, як частота перемог кожного з героїв корелює з результатами матчів, що слугує основою для відбору ознак у моделі.
Ось результат нашої візуалізації кореляцій:

Рис.: Кореляція ознак героя з результатом «radiant_win».
На кореляційному графіку ми бачимо наступні ключові значення, пов’язані з командами Radiant та Dire:
- Середнє значення контр-піків команди Dire (-0.38): Ця значна негативна кореляція свідчить про те, що коли герої команди Dire мають вище середнє значення контр-піків проти команди Radiant, шанси команди Radiant на перемогу зменшуються. Ця метрика показує, наскільки ефективний склад команди Dire проти обраних героїв Radiant, причому сильне значення контр-піків команди Dire зазвичай зменшує ймовірність успіху Radiant.
- Середнє значення контр-піків команди Radiant (0.37): На противагу цьому, сильна позитивна кореляція означає, що шанси команди Radiant на перемогу зростають, коли герої команди Radiant обираються для ефективного протистояння складу команди Dire. Це підкреслює важливість контр-піків в складі команди, де перевага в матчапах героїв може суттєво вплинути на результат матчу.
- Середній відсоток перемог команди Radiant (-0.026) та середній відсоток перемог команди Dire (-0.079): Обидва ці значення мають дуже низьку кореляцію з radiant_win, що вказує на те, що середні відсотки перемог обраних героїв кожної команди можуть мати обмежений вплив на прогнозування результатів матчів. Це означає, що ефективність контр-піків є сильнішим фактором прогнозування успіху матчу, ніж сирі показники перемог героїв.
Ці спостереження вказують на те, що стратегії складу команди, особливо в контр-пікінгу, мають значний вплив на результати матчів. Високі середні значення контр-пікінгу, особливо на користь команди Radiant, покращують шанси команди Radiant на перемогу, тоді як перевага команди Dire у контр-пікінгу працює проти команди Radiant. Цей висновок дозволяє нам визначити пріоритетність ознак на основі ефективності контр-пікінгу при навчанні предиктивних моделей.
Навчальний скрипт моделі: Побудова нашого механізму прогнозування
Для ефективного навчання нашої моделі ми застосували структурований підхід, який охоплює підготовку даних, специфікацію функцій та навчання моделі. Це дозволяє нашій моделі передбачати результати матчів з більшою точністю, використовуючи зібрані нами детальні дані про вибір героїв.
Тренування моделі
Нижче наведено повний скрипт для навчання моделі з використанням набору даних, згенерованого на основі даних про вибір героїв:
import os
import logging
import pandas as pd
from ml.model import MainML
from structure.helpers import prepare_match_prediction_data
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Define the file paths
file_path = os.path.join("..", "dataset", "train_data", "all_data_match_predict.csv")
scaler_path = "../scaler.pkl"
# Load and prepare the dataset
df = pd.read_csv(file_path)
df = prepare_match_prediction_data(df, scaler_path)
# Specify the features and target column
target = "radiant_win" # Change to your target column name
features = df.columns.drop(target).tolist()
# Path to save the model
model_path = "../xgb_model.pkl" # Path where the model will be saved
# Create an instance of MainML
main_ml = MainML(df, model_path)
# Train and save the model
main_ml.train_and_save_model(features, target)
# Load the model
main_ml.load_model()
# Prepare new data for prediction (replace this with actual data)
new_data = df.tail(5).drop(columns=[target]) # Assuming the last row is new data to predict
prediction = main_ml.predict(new_data)
print(f"Prediction for new data: {prediction}")
Гіперпослиння на увесь файл
Пояснення щодо скрипту:
- Імпорт бібліотек: Скрипт починається з імпорту необхідних бібліотек, включаючи os, logging та pandas, а також спеціальних класів для навчання моделі та підготовки даних.
- Конфігурація логування: Логування налаштоване на збір і відображення повідомлень з часовими мітками, що допомагає відстежувати процес навчання.
- Шляхи до файлів: Шляхи для набору даних і файлу масштабування визначаються для того, щоб скрипт знав, звідки завантажувати дані і куди зберігати модель.
- Підготовка даних: Набір даних завантажується за допомогою pandas і готується за допомогою допоміжної функції prepare_match_prediction_data, яка може включати масштабування та інші кроки попередньої обробки.
- Специфікація ознак: Визначено цільову змінну, яка вказує, чи виграє команда Radiant (radiant_win), і підготовлено набір функцій шляхом вилучення цільової змінної з фрейму DataFrame.
- Інстанціювання моделі: Створюється екземпляр класу MainML, який відповідає за роботу з тренуванням та збереженням моделі.
- Тренування моделі: Тренування моделі та її збереження за вказаним шляхом здійснюється за допомогою методу train_and_save_model method.
- Завантаження моделі та прогнозування: Нарешті, модель завантажується назад, і проводиться прогнозування з використанням останніх п’яти рядків набору даних, які розглядаються як нові дані для тестування.
Покращення ефективності моделі
В результаті процесу конструювання ознак ми помітили значне покращення точності нашої моделі. Раніше наша модель досягала 60% точності. Після впровадження покращень наша модель XGBoost показала наступні показники ефективності:
2024-10-31 10:35:42,311 - ml.model - INFO - XGBoost Classification Report: precision recall f1-score support 0 1.00 0.99 0.99 259 1 0.99 1.00 0.99 252 accuracy 0.99 511 macro avg 0.99 0.99 0.99 511 weighted avg 0.99 0.99 0.99 511 2024-10-31 10:35:42,312 - ml.model - INFO - XGBoost Confusion Matrix: [[256 3] [ 1 251]]
Підсумкові результати
Звіт про класифікацію
У звіті про класифікацію зазначено наступне:
- Клас 0 (перемога Dire):
- Влучність: 1.00 — Модель відмінно передбачила всі перемоги команди Dire.
- Повнота: 0.99 — Модель успішно визначила 99% фактичних перемог команди Dire.
- Оцінка F1: 0.99 — Модель зберігає стійкий баланс між влучністю та повнотою.
- Опора: 259 — Було 259 фактичних перемог команди Dire.
2. Клас 1 (перемога Radiant):
- Влучність: 0.99 — Модель правильно передбачила 99% перемог команди Radiant.
- Повнота: 1.00 — Модель визначила усі фактичні перемоги команди Radiant.
- Оцінка F1: 0.99 — Відмінне значення між влучністю і повнотою.
- Опора: 252 — Було 252 фактичних перемог команди Dire.
- Загальна точність: 99% — Модель правильно класифікувала 99% всіх матчів.
Матриця невідповідностей
- Істинно негативні спрацьовування (ІН): 256 — Правильно спрогнозована перемога Dire.
- Хибно-позитивні спрацьовування (ХП): 3 — Неправильно спрогнозована перемога Radiant, оскільки перемагає Dire.
- Хибно-негативні спрацьовування (ХН): 1 — Неправильно спрогнозована перемога Dire, оскільки перемагає Radiant.
- Істинно позитивні спрацьовування (ІП): 251 — Правильно спрогнозована перемога Radiant.
Висновки
Загалом, модель демонструє відмінну прогностичну здатність, досягаючи високої точності в 99%. Вона ефективно балансує між влучністю та повнотою, з дуже малою кількістю помилкових класифікацій, що свідчить про те, що вона є надійним інструментом для прогнозування результатів матчів на основі вибору героїв та складів команд.
Прогнози матчів у Telegram
У нашому останньому оновленні користувачі тепер можуть вибирати матчі, які їх цікавлять, і обирати, чи хочуть вони отримати прогноз щодо загального переможця матчу або детальний аналіз матчапів героїв. Ця інтерактивна функція підвищує залученість користувачів і забезпечує більш персоналізований досвід.
Нижче наведені відповідні функції в рамках нашої реалізації:
def gen_match_markup_by_id(self, call):
logger.info(f"Generating match markup by ID for call: {call}")
dota_api = Dota2API(steam_api_key)
self.markup = dota_api.get_match_as_buttons(self.markup)
return self.markup
def gen_hero_match_markup_by_id(self, call):
logger.info(f"Generating hero match markup by ID for call: {call}")
dota_api = Dota2API(steam_api_key)
self.markup = dota_api.get_hero_match_as_buttons(self.markup)
return self.markup
def make_prediction_for_selected_match(self, call, match_id):
logger.info(f"Making prediction for selected match ID: {match_id}")
self.bot.send_message(
chat_id=call.message.chat.id,
text="Task started. This may take around 5 minutes. Please wait...",
)
dota_api = Dota2API(steam_api_key)
match = dota_api.build_single_match(match_id=match_id)
message = (
f"<b>Match ID:</b> {match.match_id}\n"
f"<b>Dire Team {Icons.direIcon}:</b> {match.dire_team.team_name} (ID: {match.dire_team.team_id})\n"
"<b>Players:</b>\n"
)
# List Dire team players
for player in match.dire_team.players:
message += (
f" - {player.name} {Icons.playerIcon}(Hero: {player.hero.name})\n"
)
message += (
f"\n<b>Radiant Team {Icons.radiantIcon}:</b> {match.radiant_team.team_name} (ID: {match.radiant_team.team_id})\n"
"<b>Players:</b>\n"
)
# List Radiant team players
for player in match.radiant_team.players:
message += (
f" - {player.name} {Icons.playerIcon}(Hero: {player.hero.name})\n"
)
# Prepare match data for prediction
df, top_features = match.get_match_data_for_prediction()
main_ml = MainML(None, "xgb_model.pkl")
main_ml.load_model()
prediction, probabilities = main_ml.predict(df)
message += f"\n<b>Prediction:</b> {'Radiant Wins' if prediction[0] == 1 else 'Dire Wins'}\n"
radiant_prob = probabilities[0][1] # Assuming class 1 is Radiant
dire_prob = probabilities[0][0] # Assuming class 0 is Dire
message += f"<b>Probabilities:</b> Radiant: {radiant_prob:.2%}, Dire: {dire_prob:.2%}\n"
message += "<b>----------------------------------------</b>\n" # Separator line in bold
# Log the message text
logger.info(f"Sending message to chat {call.message.chat.id}: {message}")
self.bot.send_message(
chat_id=call.message.chat.id, text=message, parse_mode="HTML"
)
logger.info(f"Prediction for match ID {match_id} sent successfully.")
def make_hero_pick_prediction_for_selected_match(self, call, match_id):
logger.info(f"Making hero pick prediction for match ID: {match_id}")
self.bot.send_message(
chat_id=call.message.chat.id,
text="Task started. This may take around 5 minutes. Please wait...",
)
dota_api = Dota2API(steam_api_key)
match = dota_api.build_single_match(match_id=match_id)
message = (
f"<b>Match ID:</b> {match.match_id}\n"
f"<b>Dire Team {Icons.direIcon}:</b> {match.dire_team.team_name} (ID: {match.dire_team.team_id})\n"
"<b>Players:</b>\n"
)
# List Dire team players
for player in match.dire_team.players:
message += (
f" - {player.name} {Icons.playerIcon}(Hero: {player.hero.name})\n"
)
message += (
f"\n<b>Radiant Team {Icons.radiantIcon}:</b> {match.radiant_team.team_name} (ID: {match.radiant_team.team_id})\n"
"<b>Players:</b>\n"
)
# List Radiant team players
for player in match.radiant_team.players:
message += (
f" - {player.name} {Icons.playerIcon}(Hero: {player.hero.name})\n"
)
# Prepare match data for prediction
df, top_features = match.get_hero_match_data_for_prediction()
hero_pick_ml = MainML(None, "xgb_model_hero_pick.pkl")
hero_pick_ml.load_model()
prediction, _ = hero_pick_ml.predict(df)
message += f"\n<b>Prediction:</b> {'Radiant pick is stronger' if prediction[0] == 1 else 'Dire pick is stronger'}\n"
message += "<b>----------------------------------------</b>\n" # Separator line in bold
# Log the message text
logger.info(f"Sending message to chat {call.message.chat.id}: {message}")
self.bot.send_message(
chat_id=call.message.chat.id, text=message, parse_mode="HTML"
)
logger.info(f"Hero pick prediction for match ID {match_id} sent successfully.")
Гіперпослиння на увесь файл
Взаємодія з користувачами
Коли користувачі обирають матч, вони бачать такі варіанти, як:
- Прогноз на переможця матчу: Дізнатися, яка команда, за прогнозом, переможе на основі поточного матчапу.
- Аналіз матчапу героїв: Оцінка сильних і слабких сторін кожного героя в обраному матчі, що може стати основою для прийняття стратегічних рішень.
Приклади скріншотів:

Рис.: Обрати цікавий для тебе матч

Рис.:Приклад прогнозу результату матчу

Рис.:Приклад порівняння драфту
На замітку: Логування та тестове охоплення
Щоб підвищити зручність використання та надійність нашої моделі прогнозування Dota 2, ми додали детальне логування та збільшили охоплення тестування.
- Логування: Ми впровадили систему логування, яка фіксує ключові події та помилки протягом усього процесу генерації набору даних, підготовки даних та тренування моделі. Це дозволяє нам легко відстежувати поведінку моделі та швидко діагностувати проблемні моменти в міру їх виникнення.
- Охоплення тестування: Завдяки збільшенню тестового охоплення, особливо для критично важливих функцій, ми гарантуємо, що наш код буде працювати належним чином. Комплексні тести допомагають виявити потенційні помилки на ранніх стадіях, сприяючи створенню надійної та зручної для обслуговування кодової бази.
Ці вдосконалення не тільки полегшують процеси відладки, але й підвищують загальну надійність нашої моделі та її адаптивність до майбутніх удосконалень.
Висновки
У цій статті ми заглибилися в тонкощі аналізу вибору героїв у Dota 2, вдосконаливши нашу модель за допомогою надійних даних контр-пікінгу та статистичних функцій, які підвищують точність прогнозування. Інтегрувавши ці функції, ми досягли значного покращення продуктивності моделі, підвищивши точність з 60% до вражаючих 99%. Цей стрибок у можливостях прогнозування підкреслює ефективність наших зусиль в області конструювання ознак.
Інтеграція детального логування та покращеного тестового охоплення не тільки забезпечує надійність наших процесів, але й закладає міцний фундамент для майбутніх розробок.
Я хотів би висловити щиру подяку спільноті на Reddit за їхню неоціненну підтримку та мотивацію протягом усього шляху. Ваші ідеї та заохочення відіграли важливу роль у просуванні цього проєкту вперед.
Якщо вам цікаво ознайомитися з повним кодом та деталями реалізації, ви можете знайти проект у моєму репозиторії на GitHub.
Кліфхенгер: Новий рубіж
Продовжуючи вдосконалювати нашу прогностичну модель, ми стоїмо на порозі вивчення передових методів, які можуть революціонізувати наш підхід. У наступному блоці ми дослідимо історичні показники героїв та команд, використовуючи інкрементне навчання для динамічної адаптації та оптимізації стратегій. Чи піднімуть ці нововведення прогностичні можливості нашої моделі до безпрецедентних висот? Приєднуйтесь до нас у цій захоплюючій подорожі у світ передових методів машинного навчання в Dota 2.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів