CoderCastrov logo
CoderCastrov
Beautifulsoup - Парсер

Однофакторный дисперсионный анализ: Влияет ли стойка на соотношение побед бойца UFC нокаутом?

Однофакторный дисперсионный анализ: Влияет ли стойка на соотношение побед бойца UFC нокаутом?
просмотров
5 мин чтение
#Beautifulsoup - Парсер

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

На этот раз мы будем проводить те же тесты (почти), но уже не на значимые удары, а на соотношение побед нокаутом, то есть на то, сколько побед боец получает техническим или прямым нокаутом (тко/ко), чтобы увидеть, есть ли связь между количеством значимых ударов, нанесенных в минуту (SLpM), и признаком tko_in_ratio. К сожалению, такого прямого числа нет на ufcstats.com, ни в виде соотношения, ни в виде процентов. Поэтому, при сборе данных, нам придется подойти к этому алгоритмически и сами создать этот признак.

Так выглядит структура веб-сайта для одного бойца:


Как видите, нам понадобятся столбец 'w/l' (победы/поражения) и столбец 'method', но только там, где значение столбца 'w/l' - победа, так как мы ищем не общее количество тко/ко, а победы нокаутом. Однако получить это не так просто, так как сначала нужно получить ссылки на всех бойцов (около 3741), чтобы затем пройтись по каждому бойцу и извлечь нужные данные. Если у вас возникли проблемы с этим, пожалуйста, ознакомьтесь с этим репозиторием, где я подробно объясняю этот процесс.

Если вы справились с этой частью, продолжим. Изучив исходный код, мы замечаем, что каждое значение столбца, независимо от столбца, соответствует этому тегу <p>, который находится внутри тега <td> для размещения текста:


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

import requests 
from bs4 import BeautifulSoup
fighter_win = {} 
for link in fighter_links: 
    pages = requests.get(f'{link}') 
    soup = BeautifulSoup(pages.text, 'lxml') 
    fighter_win[link] = soup.find_all('p', attrs = {'class': "b-fight- details__table-text"})

Теперь у нас есть все значения в таблице каждого бойца. Однако нам нужны только данные столбцов 'method' и 'w/l'. Как же мы их получим? Я заметил паттерн (который, должно быть, есть, так как это таблица) среди значений столбца 'w/l', где его значения соответствуют 0-му, 17-му, 34-му и так далее. Таким образом, мы можем спарсить все значения 'w/l', верно? Однако есть два исключения. Во-первых, у некоторых бойцов назначен новый бой, что означает, что их 0-е значение не является победой или поражением, а следующим. Вот этот парень:

Видите? Вот почему нам нужно написать дополнительное условие 'if' для этого случая. Вторая ошибка, которая возникает при реализации вышеописанного, выглядит так:

У этого парня может быть 17 боев в его резюме, но в самом UFC у него нет записанных боев, что в терминах программирования означает, что len(fighter_win[<url>]==0). Поэтому мы должны сделать исключение для этого случая:

win_loss = [[0 for columns in range(0)] for rows in range(len(fighter_win))] 
for key, value in enumerate(fighter_links): 
    try: 
        if fighter_win[value][0].text.split('\n')[1]=='next': 
            for i in np.arange(6, len(fighter_win[value]), 17):    
                win_loss[key].append(fighter_win[value][i].text.split('\n')[1])   
        else: 
            for j in np.arange(0, len(fighter_win[value]), 17):   
                win_loss[key].append(fighter_win[value][j].text.split('\n')[1])  
    except: 
        win_loss[key].append('') 

Кстати, вы можете найти весь код парсинга для этой проблемы здесь. Затем мы получаем тот же паттерн для столбца 'method' и извлекаем его с помощью практически того же способа:

method = [[0 for columns in range(0)] for rows in range(len(fighter_win))] 
for key, value in enumerate(fighter_links): 
    try: 
        if fighter_win[value][0].text.split('\n')[1]!='next': 
            for i in np.arange (13, len(fighter_win[value]), 17):  
                method[key].append(fighter_win[value][i].text.split('\n')[4][10:])   
        elif fighter_win[value][0].text.split('\n')[1]=='next': 
            for i in np.arange (19, len(fighter_win[value]), 17): 
                try: 
                    method[key].append(fighter_win[value][i].text.split('\n')[4][10:]) 
                except: 
                    method[key].append('No info') 
    except: 
        method[key].append('No info')

После этого мы создаем новый многомерный список 'methods' и добавляем в него только значения из списка 'method', где 'w/l' равно 'win':

methods = [[0 for columns in range(0)] for rows in range(len(method))] 
for i in range(len(method)): 
    for key, value in enumerate(win_loss[i]): 
        if value=='win': 
            methods[i].append(method[i][key])

Затем мы исключаем бойца из исследования, если он провел менее 5 боев. Пожалуйста, не допустите ошибку, боец может иметь рекорд из 40 боев в своей карьере, но очень мало боев в самом UFC! Это важно.

for fighter in range(len(methods)): 
    if len(methods[fighter])<5: 
        methods[fighter] = 'Insufficient'

Наконец, давайте получим информацию о соотношении тко/победы для каждого бойца в одном многомерном списке:

tko = [[0 for columns in range(0)] for rows in range(len(methods))] 
for f in range(len(methods)): 
    for key, value in enumerate(methods[f]): 
        if value=='KO/TKO': 
            tko[f].append(methods[f][key]) 
tko_ratio = [] 
for f in range(len(methods)): 
    try: 
        if methods[f]=='Insufficient': 
            tko_ratio.append('Insufficient wins') 
        else: 
            tko_ratio.append(round(len(tko[f])/len(methods[f]),2)) 
    except: 
        tko_ratio.append('Insufficient wins')

Теперь мы объединяем это с нашим исходным набором данных с новым именем.

df['tko_win_ratio'] = pd.Series(tko_ratio).to_frame()

Статистические тесты (тест Левена, тест Шапиро-Уилка, однофакторный дисперсионный анализ и тест Тьюки HSD)

Вот как выглядят наши данные на данный момент. Хорошая работа, часть с парсингом веб-страниц выполнена! Теперь перейдем к тестам.

Сначала мы исключаем бойцов с открытым стойкой (так как их очень мало) и исключаем бойцов с недостаточным количеством побед в UFC. Таким образом, мы исключаем множество записей, но цель состоит в том, чтобы провести тесты только на бойцах, у которых есть надежная запись в UFC. Итак, давайте начнем с получения боксплотов для каждой стойки в зависимости от соотношения нокаутов:

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

Все предположения были выполнены, включая равные дисперсии и нормальность. Для равных дисперсий я провел тест Левена и получил p-значение больше 0,05. Для нормальности я провел тест Шапиро-Уилка и получил то же значение для всех категорий. Я также проверил визуальные данные для нормальности с помощью боксплота (как на рисунке выше), гистограммы и QQ-графика. Фактические квантили не сильно отклонялись от теоретических.

homoscedasticity_test = levene(df[df['stance']=='Orthodox']['tko_win_ratio'], df[df['stance']=='Southpaw']['tko_win_ratio'], df[df['stance']=='Switch']['tko_win_ratio']) print(f'''Levene test p-value: {homoscedasticity_test[1]}''')

После выполнения всех предположений мы проводим однофакторный дисперсионный анализ, чтобы определить, какие группы имеют значительные различия между собой, и используем тест Тьюки HSD в качестве пост-хок теста. Результаты следующие:

Попарно:

В целом, нулевая гипотеза не может быть отвергнута.

Если у вас есть какие-либо рекомендации, я готов сотрудничать. Спасибо за ваше время!


Оригинальная публикация на https://www.linkedin.com.