Створення алгоритму прогнозування результатів матчів у Dota 2: Мій шлях та досвід

«Я не геній, я не знаю майбутнього. Але мені подобається ідея, що розумні люди використовують технології та дані, щоб допомогти нам вдосконалюватися. Ми використовуємо аналітику, і вона нам дуже допомагає». — Юрген Клопп.

Як завзяті геймери, ми стежимо за непрогнозованістю через непередбачувану природу багатокористувацьких онлайн-ігор (MOBA). Dota 2 — одна з найскладніших MOBA. Існує багато факторів — герої, предмети, навички та стратегії гравців — які роблять передбачення результату турніру складним завданням. Оскільки люди, які люблять програмування і Dota 2, можуть визначати результат матчів Dota 2, цей проект розпочався як подарунок моєму брату, а незабаром перетворився на можливість вивчати Data Science, Машинне навчання та кіберспортивну галузь.

У цій статті я проведу вас через шлях створення предиктора для Dota 2: від збору та обробки даних до навчання моделей машинного навчання і, зрештою, прогнозування результатів. Якщо ви фанат Dota 2, програміст або просто цікавитеся процесом, я сподіваюся, що ця стаття буде для вас корисною. Вона дасть вам розуміння того, як побудована система та які виклики з нею пов’язані.

Чому я створив Dota 2 Predictor

Як фанат Dota 2, я часто задаюся питанням, які команди виграють або програють. Чи бувають збіги у виборі героїв? Можливо впливають особисті навички гравців? Чи які предмети вони купують? Натхненний цими питаннями та захопленням мого брата грою, я поставив собі за мету відповісти на ці питання, використовуючи дані та машинне навчання.

Структура набору даних і дизайн системи прогнозування

З чітким розумінням того, які класи нам потрібні — герої, гравці, команди, матчі, турніри — настав час зануритися глибше в те, як вони обробляються і використовуються для прогнозування результатів матчів. Ось як працює кожна категорія і як перетворити ці дані на ключові характеристики для наших моделей прогнозування.

Клас Hero

Клас Hero показує детальну інформацію про конкретного героя, включаючи відсоток перемог у професійних іграх. За допомогою методу get_hero_features() клас отримує таку інформацію, як ім’я героя, його піки та перемоги. Ця інформація важлива, тому що гра героя є ключовим фактором у результаті матчу.

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"
        if self.features and self.features["pro_pick"] > 0:
            self.winrate = self.features["pro_win"] / self.features["pro_pick"]
        else:
            self.winrate = 0
    def get_hero_features(self):
        url = f"https://api.opendota.com/api/heroStats?api_key={opendota_key}"
        response = requests.get(url)
        if response.status_code == 200:
            heroes = response.json()
            for hero in heroes:
                if hero["id"] == self.hero_id:
                    return {
                        "hero_id": hero["id"],
                        "name": hero["localized_name"],
                        "pro_win": hero.get("pro_win", 0),
                        "pro_pick": hero.get("pro_pick", 0),
                    }
        else:
            print(f"Error fetching data: {response.status_code}")
            return None
    def __repr__(self):
        return f"Hero(ID: {self.hero_id}, Name: {self.name}, Features: {self.features}

Клас Player

Клас Player представляє окремих гравців і записує їхню статистику, таку як вбивства, смерті, передачі, отримане золото та досвід за хвилину. Цю статистику можна отримати за допомогою методів get_player_total_data() та get_player_data(), обидва з яких покладаються на OpenDоta API.

class Player:
    def __init__(self, account_id, name, hero_id, team):
        self.account_id = account_id
        self.name = name
        self.team = team
        self.hero = Hero(hero_id)
        self.player_data = self.get_player_data()
        player_data = self.get_player_total_data()
        kills = find_dict_in_list(player_data, "field", "kills")
        self.kills = kills["sum"] / kills["n"] if kills["n"] > 0 else 0
        deaths = find_dict_in_list(player_data, "field", "deaths")
        self.deaths = deaths["sum"] / deaths["n"] if deaths["n"] > 0 else 0
        assists = find_dict_in_list(player_data, "field", "assists")
        self.assists = assists["sum"] / assists["n"] if assists["n"] > 0 else 0
        gold_per_min = find_dict_in_list(player_data, "field", "gold_per_min")
        self.gold_per_min = (
            gold_per_min["sum"] / gold_per_min["n"] if gold_per_min["n"] > 0 else 0
        )
        xp_per_min = find_dict_in_list(player_data, "field", "xp_per_min")
        self.xp_per_min = (
            xp_per_min["sum"] / xp_per_min["n"] if xp_per_min["n"] > 0 else 0
        )
        last_hits = find_dict_in_list(player_data, "field", "last_hits")
        self.last_hits = last_hits["sum"] / last_hits["n"] if last_hits["n"] > 0 else 0
        denies = find_dict_in_list(player_data, "field", "denies")
        self.denies = denies["sum"] / denies["n"] if denies["n"] > 0 else 0
    def get_player_total_data(self):
        """Fetch player total data with indefinite retries until success."""
        url = f"https://api.opendota.com/api/players/{self.account_id}/totals?api_key={opendota_key}&hero_id={self.hero.hero_id}&limit=30"
        while True:  # Retry loop
            try:
                response = requests.get(url)
                if response.status_code == 200:
                    return response.json()  # Successful response, exit loop
                else:
                    print(
                        f"Error fetching player data: {response.status_code}. Retrying..."
                    )
            except requests.exceptions.RequestException as e:
                print(f"Request failed: {e}. Retrying...")
            sleep(2)
    def get_player_data(self):
        # Fetch general win/loss data
        url = f"https://api.opendota.com/api/players/{self.account_id}/wl?api_key={opendota_key}"
        response = requests.get(url)
        if response.status_code == 200:
            data = response.json()
            player_stats = {
                "win_rate": (
                    data.get("win") / (data.get("win") + data.get("lose"))
                    if (data.get("win") + data.get("lose")) > 0
                    else 0
                ),
            }
            hero_url = f"https://api.opendota.com/api/players/{self.account_id}/heroes?api_key={opendota_key}&limit=30"
            hero_response = requests.get(hero_url)
            if hero_response.status_code == 200:
                hero_data = hero_response.json()
                for hero in hero_data:
                    if hero["hero_id"] == self.hero.hero_id:
                        # Calculate the hero's win rate
                        if hero["games"] > 0:
                            self.hero_win_rate = hero["win"] / hero["games"]
                        else:
                            self.hero_win_rate = 0
                        break
            else:
                print(f"Error fetching hero data: {hero_response.status_code}")
            return player_stats
        else:
            print(f"Error fetching player data: {response.status_code}")
            return None
    def __repr__(self):
        return f"Player({self.name}, Hero : {self.hero.name}, Team: {self.team}, Data: {self.player_data})"

На додаток до загальної інформації про перемоги/поразки, клас Player відстежує показники героя лише тоді, коли гравець грає цим героєм. Ця комбінація статистики кожного героя формує основу нашої системи прогнозування, поєднуючи навички гравців у виборі героя та минулі результати в поєднанні з оперативною інформацією.

Team клас

Клас Team об’єднує гравців у команди. Кожен об’єкт команди може додати об’єкт Player за допомогою методу add_player(). Ця конструкція важлива для відтворення реальних змагальних ситуацій, коли координація між членами команди та вибір героя може вплинути на результат матчу.

class Team:
    def __init__(self, team_name: str, team_id: int):
        self.team_name = team_name
        self.team_id = team_id
        self.players = []
    def add_player(self, player):
        self.players.append(player)
    def __repr__(self):
        return f"Team({self.team_name}, ID: {self.team_id}, Players: {self.players})"

Клас Match

Клас Match зберігає всю важливу статистику матчу, включаючи обидві команди, їхніх гравців та кінцевий результат матчу. Метод get_match_data() заповнює цю інформацію, витягуючи деталі матчу з OpenDota API, що дозволяє нам розбити вплив кожного гравця на матч.

class Match:
    def __init__(
        self,
        match_id: int,
        radiant_team_id: int,
        dire_team_id: int,
        league_id: int,
        radiant_win=None,
    ):
        self.match_id = match_id
        self.radiant_team_id = radiant_team_id
        self.dire_team_id = dire_team_id
        self.radiant_team = None
        self.dire_team = None
        self.league_id = league_id
        self.radiant_win = radiant_win
    def get_match_data(self):
        url = f"https://api.opendota.com/api/matches/{self.match_id}?api_key={opendota_key}"
        response = requests.get(url)
        if response.status_code == 200:
            match_info = response.json()
            radiant_team = Team(
                match_info["radiant_name"], match_info["radiant_team_id"]
            )
            dire_team = Team(match_info["dire_name"], match_info["dire_team_id"])
            self.radiant_win = match_info["radiant_win"]
            for player in match_info["players"]:
                if player["isRadiant"]:
                    player = Player(
                        player["account_id"],
                        player["name"],
                        player["hero_id"],
                        radiant_team.team_name,
                    )
                    radiant_team.add_player(player)
                else:
                    player = Player(
                        player["account_id"],
                        player["name"],
                        player["hero_id"],
                        dire_team.team_name,
                    )
                    dire_team.add_player(player)
            self.radiant_team = radiant_team
            self.dire_team = dire_team
    def get_match_data_for_prediction(self):
        if len(self.radiant_team.players) == 5 and len(self.dire_team.players) == 5:
            # Create a single row with match and player data
            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,
            }
            # Add radiant team player data (5 players)
            for i, player in enumerate(self.radiant_team.players):
                match_data[f"radiant_player_{i + 1}_id"] = player.account_id
                match_data[f"radiant_player_{i + 1}_name"] = player.name
                match_data[f"radiant_player_{i + 1}_hero_id"] = player.hero.hero_id
                match_data[f"radiant_player_{i + 1}_hero_name"] = player.hero.name
                match_data[f"radiant_player_{i + 1}_hero_winrate"] = player.hero.winrate
                match_data[f"radiant_player_{i + 1}_winrate"] = player.player_data[
                    "win_rate"
                ]
                match_data[f"radiant_player_{i + 1}_kills"] = player.kills
                match_data[f"radiant_player_{i + 1}_deaths"] = player.deaths
                match_data[f"radiant_player_{i + 1}_assists"] = player.assists
                match_data[f"radiant_player_{i + 1}_gold_per_min"] = player.gold_per_min
                match_data[f"radiant_player_{i + 1}_xp_per_min"] = player.xp_per_min
            # Add dire team player data (5 players)
            for i, player in enumerate(self.dire_team.players):
                match_data[f"dire_player_{i + 1}_id"] = player.account_id
                match_data[f"dire_player_{i + 1}_name"] = player.name
                match_data[f"dire_player_{i + 1}_hero_id"] = player.hero.hero_id
                match_data[f"dire_player_{i + 1}_hero_name"] = player.hero.name
                match_data[f"dire_player_{i + 1}_hero_winrate"] = player.hero.winrate
                match_data[f"dire_player_{i + 1}_winrate"] = player.player_data[
                    "win_rate"
                ]
                match_data[f"dire_player_{i + 1}_kills"] = player.kills
                match_data[f"dire_player_{i + 1}_deaths"] = player.deaths
                match_data[f"dire_player_{i + 1}_assists"] = player.assists
                match_data[f"dire_player_{i + 1}_gold_per_min"] = player.gold_per_min
                match_data[f"dire_player_{i + 1}_xp_per_min"] = player.xp_per_min
        df = pd.DataFrame([match_data])
        df = prepare_data(df)
        top_features = df.columns.tolist()
        return df, top_features
    def __repr__(self):
        # Prepare the Radiant team players
        radiant_players = "\n".join(
            [
                f"    Player: {player.name} (Hero : {player.hero.name})"
                for player in self.radiant_team.players
            ]
        )
        # Prepare the Dire team players
        dire_players = "\n".join(
            [
                f"    Player: {player.name} (Hero : {player.hero.name})"
                for player in self.dire_team.players
            ]
        )
        # Format the result
        return (
            f"Match ID: {self.match_id}\n"
            f"League ID: {self.league_id}\n"
            f"Radiant Team: {self.radiant_team.team_name}\n"
            f"Radiant Players:\n{radiant_players}\n"
            f"Dire Team: {self.dire_team.team_name}\n"
            f"Dire Players:\n{dire_players}\n"
            f"Radiant Win: {'Yes' if self.radiant_win else 'No'}"
        )

Метод get_match_data_for_prediction() збирає дані від обох команд, які потім форматуються для введення в модель прогнозування. Це забезпечує відповідну структуру набору даних для машинного навчання, відокремлюючи пов’язану статистику.

Клас Tournament

Нарешті, клас Tournament збирає всі матчі турніру і зберігає їх у вигляді списку. Це особливо корисно для аналізу великих груп матчів і виділення загальних тенденцій.

class Tournament:
    def __init__(self, league_id: int, name: str):
        self.league_id = league_id
        self.name = name
        self.matches = []
    def add_match(self, match):
        self.matches.append(match)
    def get_league_matches(self):
        url = f"https://api.opendota.com/api/leagues/{self.league_id}/matches?api_key={opendota_key}"
        response = requests.get(url)
        if response.status_code == 200:
            for match_info in response.json():
                match_id = match_info["match_id"]
                radiant_team_id = match_info["radiant_team_id"]
                dire_team_id = match_info["dire_team_id"]
                radiant_win = match_info["radiant_win"]
                match = Match(
                    match_id, radiant_team_id, dire_team_id, self.league_id, radiant_win
                )
                match.get_match_data()
                self.add_match(match)
        else:
            print(
                f"Error fetching matches for league {self.league_id}: {response.status_code}"
            )
    def __repr__(self):
        return f"Tournament({self.name}, ID: {self.league_id})"

Гіперпослиння на увесь файл

Генерація даних для професійних ліг Dota 2

Прогностична здатність моделі машинного навчання значною мірою залежить від якості даних, які вона обробляє. Для Dota 2 створення повного набору даних є надзвичайно важливим. У цьому розділі описано, як ми отримуємо історичні дані на рівні матчів. Зосереджуючись на турнірах преміум-класу з використанням OpenDota API, цей набір даних використовується для навчання моделі, яка прогнозує результати турніру на основі комбінації історичних показників і даних прямих трансляцій матчів.

1.Історичні дані про гравців

Одним з найважливіших джерел інформації є інформація з профілю гравця, яка містить метрики, що відображають загальну продуктивність у попередній грі. Вона дає уявлення про наступні питання:

  • Стабільність гравця: Гравці, які мають високі показники вбивств, асистувань або GPM (золото за хвилину), є більш ефективними та надійними.
  • Специфічні показники героя: Такі статистичні дані, як відсоток перемог гравців з певним героєм, показують, наскільки добре вони володіють цим персонажем.

Наприклад, за допомогою кінцевих посилань OpenDota /totals або /heroes ви можете збирати дані:

  • Вбивства за гру: Середню кількість вбивств, яку гравець заробив у всіх або останніх іграх.
  • Відсоток перемог для певного героя: Як часто гравці перемагають, використовуючи конкретних героїв.
  • GPM і XPM (досвід за хвилину): Вимірює, наскільки ефективно гравці добувають золото та отримують досвід з плином часу.

Ця статистика допомагає передбачити, як гравець, ймовірно, виступить у майбутніх матчах. В основному, історичні дані розкривають закономірності щодо загального рівня майстерності гравців, тенденцій та сильних сторін конкретного героя.

2. Використання історичних даних для прогнозування

Це тому, що історичні дані не прив’язані до конкретного змагання. Таким чином, вони слугують комплексним показником ефективності. Ось як ці показники впливають на модель:

  • Характеристики гравців: Такі показники, як вбивства, смерті, передачі, GPM і XPM використовуються як предиктори поведінки гравця в грі.
  • Усереднені історичні дані: Наприклад, середня кількість вбивств, відсоток перемог і GPM гравця в останніх N іграх дає більш стабільну оцінку його загального потенціалу.

Приклади історичних характеристик гравців можуть включати:

  • average_kills: Середня кількість вбивств гравця за останні N ігор.
  • win_rate: Відсоток перемог гравця в останніх N іграх.
  • hero_win_rate: Відсоток перемог гравця з певним героєм за останні N ігор.
  • gold_per_min: середній GPM гравця за останні N ігор.

Ці характеристики дозволяють моделі оцінити ймовірний вплив кожного гравця в конкретному матчі на основі його минулих результатів.

3. Характеристики героїв

На додаток до статистики гравців, важливу роль відіграють також дані про героїв. Ця статистика не відноситься до поточної гри, а є середнім значенням з усіх матчів, де був обраний герой. Це допомагає оцінити силу героїв у поточній меті:

  • Частота вибору: Як часто героя вибирають у професійних матчах.
  • Процент перемог: Відсоток перемог героя в професійних іграх.

Ці характеристики героя дають моделі розуміння ширшої ефективності даного героя в професійній грі, що має вирішальне значення при складанні прогнозів.

4. Поєднання історичних даних з даними матчів у реальному часі

Для моделі прогнозування результатів матчів важливими є як історичні дані, так і дані з живих матчів. Ідея полягає в тому, щоб об’єднати їх:

  • Історичні дані гравців: Таку інформацію, як вбивства, асисти, коефіцієнт перемог і GPM, які показують, як гравці виступали в минулому.
  • Дані про ефективність героя: Статистичні дані, такі як відсоток перемог героя та кількість обирань, які відображають загальну силу героя в професійних матчах.

Це поєднання історичних і реальних даних формує повний набір вхідних даних для моделей машинного навчання. Незважаючи на те, що історичні дані не відображають події конкретного матчу, вони слугують потужним індикатором потенціалу гравця та героя, який можна використовувати для прогнозування результатів.

5. Здобування даних про матчі за допомогою OpenDota API

З таким розумінням історичних даних та даних про героїв, наступним кроком є вилучення даних рівня матчу з профі-ліг Dota 2. Використовуючи API OpenDota, ми збираємо детальну інформацію про матчі з таких престижних турнірів, як The International та ESL One.

Ось скрипт на Python, який демонструє, як згенерувати набір даних з цих ліг, включно з даними на рівні матчів та гравців:

from structure.struct import Tournament
from structure.opendota import OpenDotaApi
def generate_dataset():
    api = OpenDotaApi()
    dataset = []
    premium_leagues = api.set_premium_leagues()
    last_big_leagues = [
        "ESL One Kuala Lumpur powered by Intel" "BetBoom Dacha Dubai 2024",
        "DreamLeague Season 22 powered by Intel",
        "Elite League Season 2 Main Event – presented by ESB",
        "ESL One Birmingham 2024 Powered by Intel",
        "DreamLeague Season 23 powered by Intel",
        "Riyadh Masters 2024 at Esports World Cup",
        "Clavision DOTA League S1 : Snow-Ruyi",
        "The International 2024",
        "PGL Wallachia 2024 Season 1",
    ]
    for premium_league in premium_leagues:
        league_id = premium_league["leagueid"]
        league_name = premium_league["name"]
        if league_name in last_big_leagues:
            tournament = Tournament(league_id=league_id, name=league_name)
            tournament.get_league_matches()  # Load matches for the tournament
            # Extract data from each match in the tournament
            for match in tournament.matches:
                radiant_team = match.radiant_team
                dire_team = match.dire_team
                # Ensure we have 5 players on each team
                if len(radiant_team.players) == 5 and len(dire_team.players) == 5:
                    # Create a single row with match and player data
                    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,  # True/False if Radiant team won
                    }
                    # Add radiant team player data (5 players)
                    for i, player in enumerate(radiant_team.players):
                        match_data[f"radiant_player_{i + 1}_id"] = player.account_id
                        match_data[f"radiant_player_{i + 1}_name"] = player.name
                        match_data[f"radiant_player_{i + 1}_hero_id"] = (
                            player.hero.hero_id
                        )
                        match_data[f"radiant_player_{i + 1}_hero_name"] = (
                            player.hero.name
                        )
                        match_data[f"radiant_player_{i + 1}_hero_winrate"] = (
                            player.hero.winrate
                        )
                        match_data[f"radiant_player_{i + 1}_winrate"] = (
                            player.player_data["win_rate"]
                        )
                        match_data[f"radiant_player_{i + 1}_kills"] = player.kills
                        match_data[f"radiant_player_{i + 1}_deaths"] = player.deaths
                        match_data[f"radiant_player_{i + 1}_assists"] = player.assists
                        match_data[f"radiant_player_{i + 1}_gold_per_min"] = (
                            player.gold_per_min
                        )
                        match_data[f"radiant_player_{i + 1}_xp_per_min"] = (
                            player.xp_per_min
                        )
                    # Add dire team player data (5 players)
                    for i, player in enumerate(dire_team.players):
                        match_data[f"dire_player_{i + 1}_id"] = player.account_id
                        match_data[f"dire_player_{i + 1}_name"] = player.name
                        match_data[f"dire_player_{i + 1}_hero_id"] = player.hero.hero_id
                        match_data[f"dire_player_{i + 1}_hero_name"] = player.hero.name
                        match_data[f"dire_player_{i + 1}_hero_winrate"] = (
                            player.hero.winrate
                        )
                        match_data[f"dire_player_{i + 1}_winrate"] = player.player_data[
                            "win_rate"
                        ]
                        match_data[f"dire_player_{i + 1}_kills"] = player.kills
                        match_data[f"dire_player_{i + 1}_deaths"] = player.deaths
                        match_data[f"dire_player_{i + 1}_assists"] = player.assists
                        match_data[f"dire_player_{i + 1}_gold_per_min"] = (
                            player.gold_per_min
                        )
                        match_data[f"dire_player_{i + 1}_xp_per_min"] = (
                            player.xp_per_min
                        )
                    print(match_data)
                    # Append match data to dataset
                    dataset.append(match_data)
    df = pd.DataFrame(dataset)
    # Write DataFrame to a CSV file
    df.to_csv("premium_league_matches.csv", index=False)
    print("Match dataset has been generated and saved to 'premium_league_matches.csv'.")
generate_dataset()

Гіперпосилання на увесь файл

6. Структурування та збереження набору даних

Остаточний набір даних зберігається у файлі CSV. Кожен рядок представляє один матч, фіксуючи дані як на рівні матчу, так і на рівні гравця, включаючи історичну статистику гравця та ефективність героя. Цей набір даних забезпечує вхідні дані для наших моделей машинного навчання для прогнозування результатів матчів.

Приклад датасету

Підготовка даних

Підготовка даних — один з найважливіших етапів у робочому процесі машинного навчання, оскільки він суттєво впливає на продуктивність і точність предиктивних моделей. У цьому розділі я розповім, як необроблені дані про матчі були перетворені на структурований набір даних, готовий до аналізу машинного навчання.

1. Інженерія ознак

Інженерія ознак передбачає створення нових ознак на основі наявних даних для покращення інсайтів моделі. Для цього проекту ми розробили функції на основі команд, які підсумовують ключові показники ефективності для команд Radiant і Dire:

  • Середній відсоток перемог героя: Ми розрахували середній відсоток перемог героїв, обраних гравцями в кожній команді. Це дає уявлення про те, наскільки історично успішним є склад героїв кожної команди.
  • Середній відсоток перемог гравців: Подібно до відсотка перемог героїв, ця функція відображає середній відсоток перемог гравців у команді, пропонуючи погляд на силу гравців команди.
  • Загальна кількість вбивств, смертей та асистів: Підсумовуючи кількість вбивств, смертей та асистів для всіх гравців у кожній команді, ми отримуємо загальну картину ефективності команди.
  • Середня кількість золота за хвилину (GPM) та досвіду за хвилину (XPM): Розрахунок середнього GPM і XPM для кожної команди дає змогу оцінити рівень накопичення ресурсів, що має вирішальне значення для розуміння того, наскільки добре команда розвивається і підвищує свій рівень.

Ці командні характеристики були створені за допомогою функції calculate_team_features(), яка обробляє DataFrame і обчислює нові метрики на основі заданого префікса команди («radiant» або «dire»). Після створення нових характеристик оригінальні стовпці, що використовувалися для їхнього обчислення, було вилучено, щоб уникнути надмірності.

def calculate_team_features(df, team_prefix):
    """
    Function to calculate team-based features for a given prefix (radiant or dire).
    """
    # Team Hero Win Rate: Average win rate of the heroes for the team
    hero_winrate_cols = [f"{team_prefix}_player_{i}_hero_winrate" for i in range(1, 6)]
    df[f"{team_prefix}_avg_hero_winrate"] = df[hero_winrate_cols].mean(axis=1)
    # Team Player Win Rate: Average win rate of the players for the team
    player_winrate_cols = [f"{team_prefix}_player_{i}_winrate" for i in range(1, 6)]
    df[f"{team_prefix}_avg_player_winrate"] = df[player_winrate_cols].mean(axis=1)
    # Team Kills, Deaths, Assists
    kills_cols = [f"{team_prefix}_player_{i}_kills" for i in range(1, 6)]
    deaths_cols = [f"{team_prefix}_player_{i}_deaths" for i in range(1, 6)]
    assists_cols = [f"{team_prefix}_player_{i}_assists" for i in range(1, 6)]
    df[f"{team_prefix}_total_kills"] = df[kills_cols].sum(axis=1)
    df[f"{team_prefix}_total_deaths"] = df[deaths_cols].sum(axis=1)
    df[f"{team_prefix}_total_assists"] = df[assists_cols].sum(axis=1)
    # Team GPM and XPM: Average GPM and XPM per team
    gpm_cols = [f"{team_prefix}_player_{i}_gold_per_min" for i in range(1, 6)]
    xpm_cols = [f"{team_prefix}_player_{i}_xp_per_min" for i in range(1, 6)]
    df[f"{team_prefix}_avg_gpm"] = df[gpm_cols].mean(axis=1)
    df[f"{team_prefix}_avg_xpm"] = df[xpm_cols].mean(axis=1)
    # Drop the original columns used to create these features
    df.drop(
        columns=hero_winrate_cols + player_winrate_cols + gpm_cols + xpm_cols,
        inplace=True,
    )
    return df

На додаток до командних характеристик, ми вирахували співвідношення «вбивство-смерть-асист» (KDA) для кожного гравця. KDA — це широко використовуваний показник у змагальних іграх, який допомагає оцінити індивідуальну ефективність гравців. Використовувана формула така:

Це дозволяє уникнути ділення на нуль, забезпечуючи надійність розрахунків. Після обчислення KDA для всіх гравців, оригінальні стовпці для вбивств, смертей та асистів були видалені.

Ось реалізація функції calculate_player_kda:

def calculate_player_kda(df, team_prefix):
    """
    Function to calculate KDA (Kill-Death-Assist ratio) for each player.
    """
    for i in range(1, 6):
        df[f"{team_prefix}_player_{i}_kda"] = (
            df[f"{team_prefix}_player_{i}_kills"]
            + df[f"{team_prefix}_player_{i}_assists"]
        ) / df[f"{team_prefix}_player_{i}_deaths"].replace(
            0, 1
        )  # Avoid division by zero
        # Drop kills, deaths, and assists for each player
        df.drop(
            columns=[
                f"{team_prefix}_player_{i}_kills",
                f"{team_prefix}_player_{i}_deaths",
                f"{team_prefix}_player_{i}_assists",
            ],
            inplace=True,
        )
    return df

2. Створення цільової змінної

Щоб перетворити набір даних для бінарної класифікації, ми перетворили стовпець radiant_win у цілочисельний формат, де:

  • 1 означає «Radiant victory»
  • 0 означає «Dire victory

Цей крок є важливим для моделі керованого навчання, де передбачення команди-переможця (Radiant або Dire) є основною метою.

3. Очищення даних

Очищення даних було виконано для видалення непотрібних стовпців, які не впливають на продуктивність моделі. Зокрема, ми видалили:

  • Ідентифікатори, специфічні для збігів: Такі як match_id, radiant_team_id, dire_team_id тощо, оскільки вони не є прогностичними ознаками.
  • Ідентифікатори гравців: Імена та ідентифікатори гравців були видалені, щоб анонімізувати дані і зосередитися виключно на показниках ефективності.

Ці кроки допомагають зменшити надмірне припасування і гарантують, що модель добре узагальнює невидимі дані.

4. Нормалізація

Щоб гарантувати, що всі ознаки однаково впливають на модель машинного навчання, ми застосували Min-Max нормалізацію для масштабування ознак між 0 і 1. Це особливо важливо для таких алгоритмів, як логістична регресія і нейронні мережі, які чутливі до величини вхідних ознак.

Ми використали MinMaxScaler для нормалізації всіх числових стовпців, окрім radiant_win (нашої цільової змінної). Нормалізація гарантує, що такі характеристики, як кількість вбивств, GPM та відсоток перемог героя, мають однакову шкалу.

def prepare_data(df):
    # Apply feature engineering for both Radiant and Dire teams
    df = calculate_team_features(df, "radiant")
    df = calculate_team_features(df, "dire")
    # Calculate KDA for each player (for both teams)
    df = calculate_player_kda(df, "radiant")
    df = calculate_player_kda(df, "dire")
    # Create a new column for the match target: 1 if radiant_win is True, else 0
    try:
        df["radiant_win"] = df["radiant_win"].astype(int)
    except KeyError:
        pass
    df.drop(
        columns=[
            "match_id",
            "radiant_team_id",
            "radiant_team_name",
            "dire_team_id",
            "dire_team_name",
            # Drop player names to anonymize data
            *[f"radiant_player_{i}_name" for i in range(1, 6)],
            *[f"radiant_player_{i}_id" for i in range(1, 6)],
            *[f"radiant_player_{i}_hero_name" for i in range(1, 6)],
            *[f"dire_player_{i}_name" for i in range(1, 6)],
            *[f"dire_player_{i}_id" for i in range(1, 6)],
            *[f"dire_player_{i}_hero_name" for i in range(1, 6)],
        ],
        inplace=True,
    )
    columns_to_normalize = df.columns.difference(["match_id", "radiant_win"])
    # Initialize the MinMaxScaler
    scaler = MinMaxScaler()
    # Apply Min-Max normalization
    df[columns_to_normalize] = scaler.fit_transform(df[columns_to_normalize])
    return df

Гіперпосилання на увесь файл

Розробка моделі машинного навчання

Для нашої першої ітерації розробки моделі ми обрали XGBoost як основний алгоритм. XGBoost, скорочення від Extreme Gradient Boosting, відомий своєю ефективністю та високою продуктивністю, особливо зі структурованими/табличними даними, що робить його популярним вибором на змаганнях з машинного навчання та в реальних додатках.

Чому XGBoost?

Є кілька причин, чому XGBoost було обрано для цієї початкової ітерації:

  1. Продуктивність на невеликих наборах даних: XGBoost є високоефективним навіть з обмеженими даними, що відповідає нашому відносно невеликому розміру набору даних. Він може забезпечити конкурентні результати, не вимагаючи великої кількості даних для навчання.
  2. Ефективність: XGBoost оптимізовано для швидкості та продуктивності. Його реалізація розроблена для ефективної роботи з великими наборами даних, але він також чудово справляється і з меншими наборами даних завдяки швидкому навчанню та здатності швидко ітерації над гіперпараметрами.
  3. Надійність: XGBoost за своєю суттю знижує ризик надмірного пристосування завдяки вбудованим методам регуляризації, таким як L1 (Лассо) і L2 (Ridge). Це робить його більш надійним при роботі з зашумленими або складними наборами даних.

Враховуючи ці переваги, ми використали можливості класифікації XGBoost для прогнозування результату матчу (промениста перемога або поразка). Наш робочий процес складався з кількох ключових кроків:

1. Поділ даних

Спочатку ми розділили наш набір даних на:

  • Характеристики: Сюди входила статистика гравців (наприклад, вбивства, смерті, передачі), вибір героїв та інші показники ефективності команди.
  • Цільова змінна: Цільовою змінною є результат матчу (radiant_win), який вказує на те, чи виграла команда Radiant, чи програла.

Далі ми розділили дані на навчальні та тестові набори, використовуючи розподіл 80-20, щоб гарантувати, що модель має невидимі дані для оцінки своєї продуктивності.

2. Навчання моделі

Для початкової ітерації ми використовували оригінальні параметри XGBoost, щоб створити базову модель. Хоча налаштування гіперпараметрів може покращити продуктивність, початок з усталених параметрів дозволяє нам оцінити базову продуктивність і визначити області для покращення.

3. Оцінка моделі

Після навчання моделі ми оцінили її продуктивність на тестовому наборі, використовуючи ключові метрики класифікації:

  • Точність: Вимірює загальну правильність прогнозів.
  • Точність, Recall та F1-Score: Ці метрики дають нам глибше розуміння того, наскільки добре модель розрізняє класи, особливо в сценаріях, де розподіл класів може бути незбалансованим.
  • Матриця плутанини: Надає детальну розбивку істинно позитивних, хибнопозитивних, істинних негативних і хибнонегативних спрацьовувань.

4. Збереження моделі

Після успішного навчання модель було збережено за допомогою бібліотеки joblib. Це дозволяє нам повторно використовувати модель для майбутніх прогнозів без необхідності щоразу її перенавчати.

5. Прогнозування на нових даних

З навченою моделлю ми можемо робити прогнози на нових, ще не бачених даних. Ця можливість дозволяє нам прогнозувати результати матчів на основі схожих наборів ознак, надаючи уявлення про потенційні результати гри.

Реалізація

Нижче наведено реалізацію пайплайну машинного навчання на Python, інкапсульовану в клас MainML:

from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
from xgboost import XGBClassifier
import joblib
class MainML:
    """
    Main class that orchestrates model training, evaluation, and prediction.
    """
    def __init__(self, df, model_path):
        self.df = df
        self.model_path = model_path
        self.xgb_model = XGBClassifier(random_state=42)
    def train_and_save_model(self, features, target):
        """
        Trains the XGBoost model and saves it to the specified path.
        """
        # Split the dataset into features (X) and target (y)
        X = self.df[features]
        y = self.df[target]
        # Split data into training and testing sets
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, random_state=42
        )
        # Train the model
        self.xgb_model.fit(X_train, y_train)
        # Save the model
        joblib.dump(self.xgb_model, self.model_path)
        print(f"Model saved to {self.model_path}")
        # Evaluate the model on the test set
        self.evaluate_model(X_test, y_test)
    def evaluate_model(self, X_test, y_test):
        """
        Evaluates the model on the test data and prints the classification report and confusion matrix.
        """
        # Make predictions on the test set
        y_pred = self.xgb_model.predict(X_test)
        # Print classification report
        print("XGBoost Classification Report:")
        print(classification_report(y_test, y_pred))
        # Print confusion matrix
        print("XGBoost Confusion Matrix:")
        print(confusion_matrix(y_test, y_pred))
    def load_model(self):
        """
        Loads the model from the specified path.
        """
        self.xgb_model = joblib.load(self.model_path)
        print(f"Model loaded from {self.model_path}")
    def predict(self, new_data):
        """
        Predicts the class for the new data point.
        """
        # Ensure that the new_data has the same features as the training set
        prediction = self.xgb_model.predict(new_data)
        return prediction

Гіперпосилання на увесь файл

Навчання моделі та прогнозування

У цьому розділі ми зосередимося на практичних кроках з навчання нашої моделі машинного навчання за допомогою класифікатора XGBoost, а потім використаємо навчену модель для прогнозування. Робочий процес включає завантаження набору даних, підготовку даних, навчання моделі, збереження її для подальшого використання і, зрештою, створення прогнозів на нових даних.

Ось покрокова інструкція:

1. Завантаження та підготовка набору даних

Ми починаємо з завантаження набору даних з файлу CSV. Сирі дані містять різноманітну статистику на рівні матчів та гравців, яку ми готуємо до машинного навчання, застосовуючи функціональну інженерію та нормалізацію. Функція prepare_data(), яку ми реалізували раніше, виконує ці завдання, перетворюючи сирі дані у формат, придатний для навчання моделі.

import os
import pandas as pd
from ml.model import MainML
from structure.helpers import prepare_data
# Define the file path to the dataset
file_path = os.path.join("..", "dataset", "train_data", "all_data.csv")
# Load the dataset into a DataFrame
df = pd.read_csv(file_path)
# Prepare the dataset for model training
df = prepare_data(df)

У функції prepare_data() ми застосували методи функціональної інженерії для створення метрик на основі команд та гравців, нормалізували характеристики та видалили непотрібні стовпці. Це підготувало дані для ефективного навчання моделі та забезпечило узгодженість.

2. Визначення ознак і мети

Далі ми визначаємо ознаки (вхідні змінні) та ціль (вихідна змінна). У нашому випадку метою є radiant_win, бінарна змінна, яка вказує, чи виграла команда Radiant (1), чи програла (0) матч. Характеристики складаються з усіх інших стовпців у наборі даних, які описують різні показники ефективності команди та гравців.

# Specify the target column (the variable we want to predict)
target = "radiant_win"
# Specify the features (all columns except the target)
features = df.columns.drop(target).tolist

Цей крок гарантує, що модель знає, які ознаки слід враховувати при прогнозуванні, і який стовпчик представляє результат матчу.

3. Ініціалізація та навчання моделі

Ми створюємо екземпляр класу MainML, який інкапсулює модель XGBoost, і переходимо до навчання моделі. Процес навчання включає в себе поділ даних на навчальні та тестові набори, настройку моделі на навчальних даних та оцінку її продуктивності на тестових даних.

# Path to save the trained model
model_path = "../xgb_model.pkl"
# Create an instance of MainML with the dataset and model path
main_ml = MainML(df, model_path)
# Train the model using the features and target, and save the model
main_ml.train_and_save_model(features, targe

На цьому кроці

  • Метод train_and_save_model() виконує весь процес навчання.
  • Після навчання модель зберігається у файл (xgb_model.pkl) за допомогою бібліотеки joblib, щоб її можна було перезавантажити для майбутніх прогнозів без перенавчання.

4. Завантаження збереженої моделі

Після того, як модель збережена, ми можемо завантажити її в будь-який час, щоб зробити прогнози на нових даних. Завантаження моделі з диска дозволяє уникнути перенавчання, що робить її більш ефективною для прогнозування в реальному часі або повторних прогнозів.

# Load the previously saved model
main_ml.load_model()

Цей рядок перезавантажує збережену модель XGBoost зі шляху model_path, роблячи її доступною для задач прогнозування.

5. Прогнозування на нових даних

Нарешті, ми готуємо вибірку нових даних, щоб продемонструвати, як модель можна використовувати для прогнозування. Тут ми моделюємо нові дані, взявши останні п’ять рядків нашого набору даних (за винятком цільового стовпчика). На практиці це можуть бути дані матчів у реальному часі або раніше невідомі дані.

# Prepare new data for prediction (excluding the target column)
new_data = df.tail(5).drop(columns=[target]) # Example: Using the last 5 rows as new data
# Make predictions using the loaded model
prediction = main_ml.predict(new_data)
# Output the predictions
print(f"Prediction for new data: {prediction}")

У цьому прикладі

  • Ми використовуємо метод .tail(5), щоб вибрати останні п’ять рядків даних і опустити стовпець radiant_win, оскільки ми робимо прогнози на основі цих невидимих даних.
  • Метод predict() з класу MainML виводить прогнозований результат матчу (1 для перемоги Radiant, 0 для поразки Radiant).

Гіперпосилання на увесь файл

Як запустити проект Dota 2 Predictor

Цей розділ містить покрокову інструкцію з налаштування та запуску проекту Dota 2 Predictor на вашому комп’ютері.

1. Клонування репозиторію

Для початку завантажте код проекту, клонувавши репозиторій GitHub. Відкрийте термінал і виконайте наступні команди:

git clone https://github.com/masterhood13/dota2predictor.git
cd dota2predictor
git checkout tags/1.0.1

Це створить локальну копію сховища і перемістить вас в каталог проекту.

2. Встановлення залежностей

Переконайтеся, що у вашій системі встановлено Python 3.9 або новішої версії. Далі вам потрібно буде встановити необхідні бібліотеки Python, виконавши

pip install -r requirements.txt

Ця команда встановить всі необхідні пакети, такі як pandas, xgboost, scikit-learn та інші, необхідні для обробки даних і завдань машинного навчання.

3. Налаштування змінних середовища

Вам знадобляться ключі API для OpenDota, Steam і Telegram, щоб увімкнути зчитування даних і функціонал бота. Створіть файл .env у каталозі проекту та додайте до нього ключі API наступним чином:

OPENDOTA_KEY=your_api_key
STEAM_API_KEY=your_steam_api_key
TELEGRAM_KEY=your_telegram_bot_token

Замініть your_opendota_api_key, your_steam_api_key та your_telegram_bot_token на ваші фактичні ключі API.

4. Запуск проекту

Після того, як все налаштовано, ви можете запустити бота, виконавши наступну команду:

python start.py

Це запустить бота-предиктора, що дозволить йому отримувати дані та взаємодіяти з API Dota 2 для прогнозування результатів матчів на основі навченої моделі машинного навчання.

Взаємодія з Telegram-ботом для прогнозування матчів Dota 2

Після того, як модель навчена і бот запущений, виконайте наступні кроки, щоб почати використовувати Telegram-бот для прогнозування матчів:

1. Отримайте доступ до Telegram-бота

Після налаштування бота в попередніх кроках відкрийте Telegram на своєму пристрої. Знайдіть свого бота за його іменем користувача або скористайтеся посиланням на бота (якщо ви поділилися ним із собою або іншими).

2. Почніть розмову

Надішліть боту будь-яке повідомлення, щоб почати взаємодію. Бот відповість меню з опціями на вибір.

3. Отримуйте прогнози для поточних матчів Dota 2

Щоб отримати прогнози на всі поточні матчі Dota 2, просто натисніть відповідну кнопку в меню бота (наприклад, «Отримати прогнози на матчі»). Після цього бот звернеться до API Dota 2 і покаже прогнози для кожної активної гри.

4. Перегляд результатів прогнозування

Для кожного активного матчу бот повертатиме прогнозованого переможця — команду Radiant або Dire — на основі оцінки даних матчу моделлю машинного навчання. Замість ймовірності перемоги ви отримаєте чіткий прогноз, який вказує, яка команда з більшою ймовірністю виграє матч, забезпечуючи розуміння поточної гри в реальному часі.

Ось приклад результату:

Високорівнева архітектура системи

Архітектура нашого бота-прогнозиста Dota 2 демонструє, як різні компоненти працюють разом, щоб надавати прогнози результатів матчів кінцевому користувачеві через Telegram-бот. Потік починається із запиту користувача і проходить через різні етапи збору даних, попередньої обробки, прогнозування моделі і, нарешті, надання відповіді.

Ось покрокова розбивка робочого процесу системи:

1. Взаємодія зкористувачем:

  • Процес починається з того, що користувач надсилає запит боту через Telegram. Це може бути прохання передбачити результат поточного матчу з Dota 2.

2. Телеграм-бот:

  • Телеграм-бот виступає інтерфейсом між користувачем і бекенд-системою. Отримавши запит, бот пересилає його до бекенду для подальшої обробки.

3. Бекенд-система:

  • Бекенд-сервер організовує основну логіку процесу прогнозування. Він збирає необхідні дані про матчі та гравців із зовнішніх API, таких як OpenDota та Steam.

4. Отримання даних із зовнішніх API:

  • Внутрішня система запитує дані про матчі та гравців з API OpenDota та, за необхідності, з API Steam, щоб збагатити дані про гравців додатковим контекстом.

5. Попередня обробка даних:

  • Перед тим, як завантажити дані в модель прогнозування, рушій попередньої обробки перетворює необроблені дані у формат, придатний для аналізу. Це включає очищення, розробку функцій і нормалізацію, щоб забезпечити відповідність даних вимогам навченої моделі машинного навчання.

6. Прогнозування моделі XGBoost:

  • Оброблені дані передаються в модель XGBoost, яка була навчена прогнозувати, чи переможе команда Radiant або Dire. Модель приймає рішення на основі історичних даних та поточного контексту матчу.

7. Повернення результатів:

  • Після того, як модель надає свій прогноз, бекенд обробляє результат і надсилає його назад у Telegram-бот, який потім показує користувачеві прогнозованого переможця (Radiant або Dire).

Блок-схема системи

Ця архітектура забезпечує плавний, автоматизований потік від взаємодії з користувачем до прогнозування фінального матчу, дозволяючи користувачам легко отримати доступ до прогнозів для поточних ігор Dota 2. Кожен компонент працює незалежно, але робить свій внесок у загальну функціональність системи прогнозування.

Висновок. I що далі?

На першому етапі ми успішно побудували модель машинного навчання для прогнозування результатів матчів Dota 2, досягнувши базової точності близько 60%. Це багатообіцяючий результат, але він залишає багато можливостей для вдосконалення. Складність Dota 2 означає, що більш досконалі дані та інженерія функцій можуть забезпечити ще кращі прогнози.

Але це тільки початок.

У другій частині ми заглибимося в інженерію функцій, введемо нові метрики для кращого відображення динаміки команди та продуктивності гравців. Ми також інтегруємо додаткові джерела даних, включаючи статистику матчів у реальному часі від нашого Telegram-бота. Найцікавіше, що ми розробимо та навчимо спеціальну кастомну модель глибокого навчання, адаптовану спеціально для цього збагаченого набору даних.

Залишайтеся з нами — майбутні кроки вже не за горами!

Щоб дізнатися більше деталей та ознайомитися з проектом в цілому, відвідайте Dota 2 Predictor GitHub Repository.

Підписуйтеся на Telegram-канал @gamedev_dou, щоб не пропустити найважливіші статті і новини про геймдев

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Так, скільки результатів ти вже вгадав і скільки бабла виграв? Бо якщо не виграв, то щось не так з алгоритмом

Мені здається, про гроші питати невідому людину в інтернеті, як мінімум недоречно. А так за отсанні 50 з копійками ігор точність 65 +.- відстотків.

Цікаво, якщо ці дані вигрузити в чатжпт, наскільки точний прогноз він зможе дати?)

Це трохи не так працює, мені здається ;) гпт це NLP на стероїдіх, а потрвбен класифікатор :) Вона вам просто росповість цікаву історію але результат буде зі стелі +/-

я по заголовку подумав що ви та людина з валв що в самій доті створює предікт для роботи матчмейкінгу

Хах, нажаль ні, але ,можливо, все ще по переду ;)

Ну а насправді тема дуже фінансово вигідна, і ця я зараз не про Доту.

Ну так і є. У моєму випадку це спортивний інертересб і спроба показати як працюють данні для передбачення майбутнього. Не рекомендую робити ставки основним джерелом доходу, це небезпечна річ. Але ви можете використовувати його у своїх цілях.
Дякую за коментар.

Алгоритм прогнозування.
Ви сурійозно ???

Найронку зробіть та хай вона постійно навчаєтся на реальних результатах

Ну так XGBoost видає кращій перформанс на малих датасетах, та вимагає меншого інженірінгу фіч, і він навчався на реальних данних. Я поріняв його зз нейронками і з однаковим датасетом, його перформан найкращій.

Я можу помилятись, щодо задач прогнозування результатів, але перфоманс там далеко не на першому місці.
Головне щоб ваш алгоритм/нейронка як можно точніше прогнозувала реальний результат.

Все вірно, я мав на увазі під перформансом власне точність

Так, після років у с++, у мене трохи інше розуміння цього слова, тож трохи непорозумілись, буває.

Чудова стаття! Дуже цікаво було дізнатися про ваш підхід до створення алгоритму прогнозування в Dota 2. Видно, що вкладено багато часу й зусиль, і результат вартий уваги. Було б цікаво побачити продовження ваших досліджень та вдосконалення алгоритму! Сподіваюся, ви продовжите писати та ділитися своїм досвідом.

Дякую за підтримку! Так, скоро буде наступна частина.

Велике дякую! Це дуже мотивує мене працювати далі.

Оце автор постарався!

Цікаво.

Замість ймовірності перемоги ви отримаєте чіткий прогноз, який вказує, яка команда з більшою ймовірністю виграє матч, забезпечуючи розуміння поточної гри в реальному часі.

А чому б не надавати ймовірності, якщо їх уже якось розрахували? І чому б не порівняти їх із лініями, які професійно розраховані букмекерами?

Гарне зауваження. Я планую випустити серію із трьох статей (включно з цією). Де буду додвати різні фічі, наприклад ймовірності а також порівнянея з букмейкерами. А також reinforcement learning який буде у третій частитні. Я зараз працюю над другою частино, яка буде сфокусована на візуалізаціі датасету, а такаож його удосконалення, і тако додання нових фіч, таких як аналіз драфту. У цій статті я сфокусувався на делатьному описі проекту.
Дякую за коментар!

буду додвати різні фічі, наприклад ймовірності а також порівнянея з букмейкерами

Ну ви ж розумієте, який засіб порівняння є найоб’єктивнішим?
Якщо ви просто скажете — на цей матч ми розрахували перемогу команди А в 60%, а букмекер дав 50%. Ну там потім хтось виграв, нічого не зрозуміло, хто був точніший.
А ось якщо ви виграєте у букмекера гроші на дістанції в 1000 матчів, це вже буде майже доказом. Рекомендую Pinnacle Sports, вони завжди славилися своїми лініями, найточнішими в галузі.

Велике дякую за пораду. Я подивлюсь чи пінакл дає якісь апі для цього. а з приводу 1000 матчів це можно перевіряти імперично. Про це і буде стаття номер триб у якій буде історя і також порівняння з фатичним результатом, і по можливості результатами букмекера.

API вони раніше мали, зараз він є лише на їхньому сірому зеркалі ps3838, але там умови щодо коштів на аккаунті та обсягу ставок. Чи не можна в наш час AI новому Claude Sonnet просто сказати, нехай дивиться їхню сторінку та зберігає odds в базу

можна написати будь яку нейронку під будь яку ціль, це питання часу ;) для цього питння це оверінженірінг. Я спробую знайти щось інше

Підписатись на коментарі