Однофакторный дисперсионный анализ: Влияет ли стойка на соотношение побед бойца UFC нокаутом?
В прошлый раз мы обсуждали влияние стойки на значимые удары, и был сделан вывод, что существует значительная разница в среднем между бойцами, меняющими стойку, и остальными. Таким образом, нулевая гипотеза была отвергнута.
На этот раз мы будем проводить те же тесты (почти), но уже не на значимые удары, а на соотношение побед нокаутом, то есть на то, сколько побед боец получает техническим или прямым нокаутом (тко/ко), чтобы увидеть, есть ли связь между количеством значимых ударов, нанесенных в минуту (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.