Counter Strike соответствует прогнозу результатов

Часть 1: данные веб-скрейпинга

Некоторым моим друзьям нравится проводить целые ночи, играя в Counter Strike на своем компьютере. В эту игру также играют профессиональные электронные игроки, и в ней очень организованы турниры и матчи, данные о которых доступны в Интернете. Когда я только начинал изучать науку о данных, мне показалось хорошей идеей разработать модель машинного обучения, предсказывающую результат матчей на основе статистики игроков.

Согласно его странице в Википедии, Counter-Strike — это многопользовательский шутер от первого лица, основанный на целях. Две противоборствующие команды — Террористы и Контртеррористы — соревнуются в игровых режимах для выполнения задач, таких как обеспечение безопасности места для установки или обезвреживания бомбы, а также спасение или охрана заложников.

HLTV.org — это веб-сайт, на котором собраны все результаты, турниры, историческая статистика игроков и команд, расписание будущих матчей, а также прямые трансляции матчей. Итак, это будет источник для нашего проекта по науке о данных.

Это первая из трех статей о сборе данных, разработке модели машинного обучения и ее развертывании. В этом посте я опишу, как я собирал исторические данные об игроках и матчах через веб-сайт hltv.org с использованием методов Python.

В разделе результатов hltv.org мы можем найти результаты матчей, сыгранных до 2012 года. Когда мы нажимаем на ссылку матча, мы видим такие данные, как дата матча, окончательный результат, команды, час, сыгранные карты, а также как индивидуальная статистика игроков в этом матче. Перейдя по ссылке каждого игрока, мы можем увидеть общую статистику из истории матчей игроков. Итак, идея этого веб-проекта по сбору данных состоит в том, чтобы получить доступ к каждому матчу со страницы результатов, собрать данные из матча, затем получить доступ к статистике каждого игрока, сыгравшего этот матч, и собрать их историческую статистику до дня, непосредственно предшествующего дате матча. Думая о перспективе модели машинного обучения, данные до дня, предшествующего матчу, будут доступны для прогнозирования будущих матчей. К концу этого проекта у нас должна быть база данных матчей и база данных игроков, структурированная следующим образом:

Этот проект был разработан в блокноте Jupyter, и если вы не хотите читать весь этот пост, вы можете скачать блокнот с полным блокнотом из моего репозитория github здесь. Теперь мы пройдемся по сеансам кода, используемого для веб-скрапинга наших данных.

from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd

Начиная с импорта необходимых ресурсов и библиотек, webdriver позволит нам открыть браузер и управлять им через наш код Python. С BeautifulSoup мы можем читать содержимое веб-страницы, контролируемой Webdriver, и перебирать ее содержимое. Нам также понадобятся панды для работы с фреймами данных.

driver=webdriver.Chrome()
date=[]
team1=[]
team2=[]
finalResult1=[]
finalResult2=[]
tournament=[]
linkStatsList=[]
playersPlayers=[]
kdPlayers=[]
adrPlayers=[]
kastPlayers=[]
ratingPlayers=[]
datePlayers=[]
team1Players=[]
team2Players=[]
finalResult1Players=[]
finalResult2Players=[]
tournamentPlayers=[]
playerteamPlayers=[]
mapPlayers = []
overallKillsPlayers = []
overallDeathsPlayers = []
overallKill_DeathPlayers = []
overallKill_RoundPlayers = []
overallRoundsWithKillsPlayers = []
overallKillDeathDiffPlayers = []
openingTotalKillsPlayers = []
openingTotalDeathsPlayers = []
openingKillRatioPlayers = []
openingKillRatingPlayers = []
openingTeamWinPercentAfterFirstKillPlayers = []
openingFirstKillInWonRoundsPlayers = []
months = {
    'January':'01',
    'February':'02',
    'March':'03',
    'April':'04',
    'May':'05',
    'June':'06',
    'July':'07',
    'August':'08',
    'September':'09',
    'October':'10',
    'November':'11',
    'December':'12'
}

Прежде чем приступить к надлежащему удалению веб-страниц, мы подготавливаем все данные, которые потребуются в процессе. Мы запускаем инструмент веб-драйвера, определяя доступ через Google Chrome и назначая его переменной с именем «драйвер». Затем мы создаем пустые списки всех столбцов, которые нам нужны в наших окончательных кадрах данных, по одному для каждых данных, которые мы хотим собрать. По мере прохождения кода по страницам собранные данные будут добавляться к каждому из списков. К концу процесса они будут объединены в фреймы данных. Все списки, которые заканчиваются словом «Игроки» в названии, относятся к данным, которые будут собираться со страниц статистики игроков, а остальные — из сыгранных матчей. Мы также создаем словарь с названиями месяцев и их номерами, когда-то он нам понадобится для чтения даты совпадения, а затем для поиска статистики за предыдущий день, который использует месяц в числовом формате.

page=0
while page <=99:
  matchesLinks=[]
  driver.get('https://www.hltv.org/results?offset='+str(page))
  content=driver.page_source
  soup=BeautifulSoup(content)
  for div in soup.findAll('div', attrs={'class':'results'}):
          for a in div.findAll('a', attrs={'class':'a-reset'}):
              link = a['href']
              matchesLinks.append(link)
  |
  |
  | 
  page+=100

Мы начинаем с определения переменной страница и присвоения ее значения нулю. Поскольку есть много страниц результатов с данными за 2012 год, мы можем перебирать эти страницы, добавляя 100 к переменной страницы на каждой итерации, используя цикл while. В этом случае мы ограничили код остановкой, когда страница больше 99, потому что мы хотим, чтобы данные веб-скрапинга были только с первой страницы. Весь код с этого момента будет находиться внутри этого цикла while. Во-первых, создается список под названием matchesLinks, в котором будут храниться все ссылки каждой страницы соответствия, к которым можно будет получить доступ позже. Мы используем нашу переменную драйвер и ее метод get для доступа к веб-сайту hltv в Google Chrome. Здесь мы формируем адрес веб-сайта, объединяя https://www.hltv.org/results?offset= и значение из переменной страницы, преобразованное в строку. Таким образом, каждый раз, когда мы добавляем 100 к нашей переменной страницы, мы будем открывать другую страницу результатов, которую мы удаляем.

Затем мы сохраняем содержимое страницы в переменной «content» и читаем его с помощью BeautifulSoup, сохраняя его в переменной «soup». Анализируя страницу с помощью инспектора браузера, мы видим, что результаты находятся в контейнере с классом «результаты», а каждая ссылка имеет класс «a-reset». Итак, наш код находит все дивы с классом 'results' в содержимом, хранящемся в нашей переменной 'суп', и для каждого находит все ссылки с классом 'a-reset', а потом мы собираем 'href' атрибут каждого элемента «a», который является правильной ссылкой, и добавьте к списку «matchesLinks».

for link in matchesLinks:
  if (link[:8]=="/matches"):
    url='https://www.hltv.org/'+link
            
            
    driver.get(url)
    content=driver.page_source
    soup=BeautifulSoup(content)
    for div in soup.findAll('div', attrs={'class':'match-page'}):
        pageDate=div.find('div',attrs={'class':'date'}) 
        pageTournament=div.find('div',attrs={'class':'event text-ellipsis'})
        date.append(pageDate.text)
        tournament.append(pageTournament.text)
    for div in soup.findAll('div', attrs={'class':'team1-gradient'}):
        pageTeam1=div.find('div',attrs={'class':'teamName'})
        pageResult1=div.find('div',attrs={'class':['won','lost','tie']})
        team1.append(pageTeam1.text)
        finalResult1.append(pageResult1.text)
    for div in soup.findAll('div', attrs={'class':'team2-gradient'}):
        pageTeam2=div.find('div',attrs={'class':'teamName'})
        pageResult2=div.find('div',attrs={'class':['won','lost','tie']})
        team2.append(pageTeam2.text)
        finalResult2.append(pageResult2.text)

С этого момента мы перебираем ссылки, хранящиеся в matchLinks, открываем каждую из них с помощью веб-драйвера и BeautifulSoup, как на первом этапе, и собираем нужные нам данные со страницы. Наш код найдет div с классом «match-page», где мы можем найти данные о дате матча и турнире. Эти данные будут добавлены к датам и турнирным спискам. Тот же процесс выполняется в следующих строках для сбора информации о командах в матче и окончательных результатах.

for div in soup.findAll('div', attrs={'id':"all-content"}):
    team = pageTeam1.text
    for table in div.findAll(class_='table totalstats'):
        rows = table.find_all('tr')[1:]
        for row in rows:
            cell = [i.text for i in row.find_all('td')]
            playersPlayers.append(cell[0].split('\n')[2])
            kdPlayers.append(cell[1])
            adrPlayers.append(cell[2])
            kastPlayers.append(cell[3])
            ratingPlayers.append(cell[4])
            datePlayers.append(pageDate.text)
            team1Players.append(pageTeam1.text)
            team2Players.append(pageTeam2.textt)
            finalResult1Players.append(pageResult1.text)
            finalResult2Players.append(pageResult2.text)
            tournamentPlayers.append(pageTournament.text)
            playerteamPlayers.append(team)
            mapPlayers.append(maps[j])
    team = pageTeam2.text

Теперь, когда мы собрали общую информацию о матче, мы можем погрузиться в таблицу со статистикой игроков по этому матчу. Процесс тот же, но разница здесь в том, что элемент таблицы является строкой, поэтому мы должны использовать другой метод для фильтрации его класса, как показано в приведенном выше коде. После этого мы добавляем статистику каждого игрока в соответствующий список.

for divl in soup.findAll('div',attrs={'class':'small-padding stats-detailed-stats'}):
    for a in divl.findAll('a'):
        link_stats = a['href']
        break
    url='https://www.hltv.org/'+link_stats
    linkStatsList.append(url)
    driver.get(url)
    content=driver.page_source
    soup=BeautifulSoup(content)

После сбора всех данных от каждого игрока для этого конкретного матча мы начинаем искать исторические данные каждого игрока до непосредственной даты матча. На странице каждого матча есть ссылка «Подробная статистика», где мы можем найти ссылки на историческую статистику игроков. Приведенный выше код находит эту ссылку на странице соответствия и направляет на нее контролируемую страницу, чтобы код мог начать сбор этих данных на следующих шагах.

for table in soup.findAll(class_='stats-table'):
    rows = table.find_all('tr')[1:]
    for row in rows:
        stats_auxiliary = {}
        link_player = [i['href'] for i in row.find_all('a')]
        dateStats = pageDate.text
        dateSplit = dateStats.split(' ')
        year = dateSplit[-1]
        month = months[dateSplit[-2]]
        if len(dateSplit[0])==3:
           toInt = int(dateSplit[0][0])
           day_aux = toInt-1
           day = '0'+str(day_aux)
        else:
           toInt = int(dateSplit[0][0:2])
           day_aux = toInt-1
           day = str(day_aux)
        url='https://www.hltv.org'+link_player[0][:15]+'individual/'+link_player[0][15:]+'?startDate=2013-01-01&endDate='+year+'-'+month+'-'+day
        driver.get(url)
        content=driver.page_source
        soup=BeautifulSoup(content)

На странице «Подробная статистика» появилась новая таблица со ссылками на историческую статистику каждого игрока. Мы следуем тому же процессу поиска ссылки в каждой строке. Разница здесь в том, что мы указываем дату начала и дату окончания для фильтрации периода статистики каждого игрока. Датой начала всегда будет 01.01.2013, а датой окончания будет предыдущий день матча. Чтобы настроить дату окончания, мы должны манипулировать датой совпадения, собранной на предыдущих шагах кода. Затем мы разделим его на пробелы, чтобы разделить день, месяц и год, и будем работать над ним, чтобы дата была в формате ГГГГ/ММ/ДД.

driver.get(url)
content=driver.page_source                                   soup=BeautifulSoup(content)
for divpl in soup.findAll('div',attrs={'class','standard-box'}):
    for divst in divpl.findAll('div',attrs={'class','stats-row'}):
        stat = []
        for span in divst.findAll('span'):
            if (span.text != 'K - D diff.'):
               stat.append(span.text)s
               stats_auxiliary[stat[0]]=stat[1]

                                    overallKillsPlayers.append(stats_auxiliary['Kills'])                               overallDeathsPlayers.append(stats_auxiliary['Deaths'])                             overallKill_DeathPlayers.append(stats_auxiliary['Kill / Death'])                                  overallKill_RoundPlayers.append(stats_auxiliary['Kill / Round'])                                    ocerallRoundsWithKillsPlayers.append(stats_auxiliary['Rounds with kills'])                                    overallKillDeathDiffPlayers.append(stats_auxiliary['Kill - Death difference'])                                   openingTotalKillsPlayers.append(stats_auxiliary['Total opening kills'])                                   openingTotalDeathsPlayers.append(stats_auxiliary['Total opening deaths'])                                    openingKillRatioPlayers.append(stats_auxiliary['Opening kill ratio'])                                    openingKillRatingPlayers.append(stats_auxiliary['Opening kill rating'])                                    openingTeamWinPercentAfterFirstKillPlayers.append(stats_auxiliary['Team win percent after first kill'])                                    openingFirstKillInWonRoundsPlayers.append(stats_auxiliary['First kill in won rounds'])

Как только код получает доступ к исторической статистике каждого игрока, мы следуем тому же процессу, чтобы добавить данные в список всех данных, которые мы создали в начале кода.

players_auxdf=pd.DataFrame({'Date':datePlayers,'Team1':team1Players,'Team2':team2Players,'Final Result 1':finalResult1Players,'Final Result 2':finalResult2Players,'Tournament':tournamentPlayers,'Player Team':playerteamPlayers,'Player':playersPlayers,'KD':kdPlayers,'ADR':adrPlayers,'KAST':kastPlayers,'Rating':ratingPlayers,'Map':mapPlayers, 'Overall Kills':overallKillsPlayers,'Overall Deaths':overallDeathsPlayers,'Overal Kill / eath':overallKill_DeathPlayers,'Overall Kill / Round':overallKill_RoundPlayers,'Overall Rounds with Kills':overallRoundsWithKillsPlayers,'Overall Kill - Death Diff':overallKillDeathDiffPlayers,'Opening Total Kills':openingTotalKillsPlayers,'Opening Total Deaths':openingTotalDeathsPlayers,'Opening Kill Ratio':openingKillRatioPlayers,'Opening Kill rating':openingKillRatingPlayers,'Opening Team win percent after 1st kill':openingTeamWinPercentAfterFirstKillPlayers,'Opening 1st kill in won rounds':openingFirstKillInWonRoundsPlayers})
playersdf=pd.concat([playersdf,players_auxdf])
playersPlayers=[]
kdPlayers=[]
adrPlayers=[]
kastPlayers=[]
ratingPlayers=[]
datePlayers=[]
team1Players=[]
team2Players=[]
finalResult1Players=[]
finalResult2Players=[]
tournamentPlayers=[]
playerteamPlayers=[]
mapPlayers = []
overallKillsPlayers = []
overallDeathsPlayers = []
overallKill_DeathPlayers = []
overallKill_RoundPlayers = []
overallRoundsWithKillsPlayers = []
overallKillDeathDiffPlayers = []
openingTotalKillsPlayers = []
openingTotalDeathsPlayers = []
openingKillRatioPlayers = []
openingKillRatingPlayers = []
openingTeamWinPercentAfterFirstKillPlayers = []
openingFirstKillInWonRoundsPlayers = []

После сбора всех данных от игроков, участвующих в этом матче, код создает кадр данных с именем «players_auxdf», объединяющий все созданные списки, а затем объединяет его с общим кадром данных «playersdf», который будет построен по циклам. После этого мы можем очистить все списки, как только данные уже сохранены в Dataframe, и поэтому мы можем собирать данные от игроков следующего матча.

Весь этот код будет перебирать все совпадения на одной странице согласно нашему списку под названием «matchesLinks.

df=pd.DataFrame({'Date':date,'Team1':team1,'Team2':team2,'Final Result 1':finalResult1,'Final Result 2':finalResult2,'Tournament':tournament,'Link Stats':linkStatsList})
    df.to_csv('csMatches_nd_'+str(page)+'.csv',index=False)
    
    date=[]
    team1=[]
    team2=[]
    finalResult1=[]
    finalResult2=[]
    tournament=[]
    linkStatsList=[]
    
    playersdf.to_csv('csplayers_nd_'+str(page)+'.csv',index=False)
    
    playersdf = playersdf[0:0]
    
    page+=100

Наконец, мы можем хранить списки с общими данными совпадений в кадре данных, а затем сохранять их в файле .csv для последующего использования. Мы также сохраняем кадры данных игроков в файл .csv. Код также очищает все списки и кадр данных «playersdf», чтобы перезапустить задание на новой странице. Фреймы данных могли быть сохранены в другой форме хранения, например в базе данных SQL, вместо файлов .csv, используемых здесь.

На этом завершается этап веб-скрейпинга этого проекта. Теперь мы можем перейти к следующему шагу и проанализировать собранные данные, чтобы использовать их для построения модели машинного обучения для прогнозирования победителя матчей Counter Strike. Продолжение проекта в следующем посте.