Введение:

Каждый день почти в каждой отрасли мы ищем способы измерения производительности, сравнивая более свежие данные с той или иной формой исторических данных, чтобы проверить наличие существенных различий. Хотя это может не дать нам точного ответа на наши вопросы, это позволяет нам измерять изменения с течением времени и использовать эти изменения в качестве некоторого порога для будущих решений. Очевидно, мы знаем, что это относится и к спорту.

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

В мире ставок, скажем, кто-то хочет разместить рынок на голах, которые команда может забить в игре. Логично посмотреть, может быть, последние 5-10 игр команды, чтобы увидеть количество забитых ими голов и использовать это как критерий количества голов, которые они забьют в данной игре, но является ли среднее количество голов в последних 10 матчах нормальным для этой команды, или это может быть случайно?

На эти и многие другие вопросы можно ответить с помощью проверки гипотез, и в этой статье мы покажем, как мы можем использовать проверку гипотез (в частности, однократный выборочный тест), чтобы ответить на них, используя футбольную команду в качестве примера использования и исследуя конкретную статистику.

Из нашего предыдущего примера, скажем, среднее количество голов, забитых командой за игру в их последних 30 играх, равно 2, и мы также знаем, что их обычное среднее количество голов, забитых за игру, составляет около 1,5, мы можем узнать больше о шансах команды забить определенный средний гол, проведя проверку гипотез, используя эти значения в качестве параметров.

В качестве иллюстрации мы будем изучать некоторые спортивные данные с помощью спортивного API. Есть много источников для получения футбольных данных. В этой статье я буду использовать API, предоставленный football-data.org, для получения нашей статистики по интересующей нас команде. Получение наших данных — это первый шаг, за которым последует манипулирование нашими данными перед дальнейшим анализом и тестированием. Мы будем использовать однократный выборочный тест, хотя мы также смоделируем нулевое распределение средних целей, чтобы понять, как может выглядеть наша популяция. В этом тематическом исследовании я предоставлю код из работы по мере написания, но я не буду утомлять вас крошечными деталями, такими как точные URL-адреса API для определенных запросов, глубокое погружение в код и то, как или что содержится в наших возвращенных объектах JSON. Цель состоит в том, чтобы поделиться идеей, а также своими выводами из анализа.

Импорт библиотек и настройка API:

Как упоминалось ранее, я использовал API, предоставленный Football-data.org, чтобы получить наши данные. После создания учетной записи на сайте мне был предоставлен API-ключ для включения в последующие запросы. Для проекта я выбрал команду «Арсенал» (здесь, кстати, наводчик 😄). В приведенном ниже коде показано, как были импортированы библиотеки, а также объявленные переменные, используемые в наших запросах.

import requests
import csv
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
# api setup

api_url = "https://api.football-data.org/v4/"
date_start = "2021-08-01"
date_end = "2023-06-11"
api_key = "*****4fef1e90a4**************"
headers = { 'X-Auth-Token': api_key }
team_to_find = "Arsenal"
league = "PL"

Из приведенного выше кода вы можете увидеть импортированные библиотеки, и, как мы увидим дальше, я рассмотрю, как каждая из них использовалась. Я также объявил URL-адрес нашего API, временные рамки событий, которые мы будем запрашивать, ключ API, заголовок запроса, а также команду, которую мы будем изучать, и интересующее нас соревнование, которым в данном случае является английская премьер-лига.

Извлечение данных:

Чтобы получить данные, я сделал два запроса. Первый запрос использовался для получения команд, участвующих в соревновании (EPL), который затем использовался для получения идентификатора команды (Арсенал). Да, возможно, не идеально, но это было предусмотрено API. Для второго запроса я использовал идентификатор команды, чтобы получить их матчи в течение указанного периода времени. Это вернуло их последние 70 игр, что мне и было нужно.

build_teams_url = "{}/competitions/{}/teams".format(api_url, league)
teams_request = requests.get(build_teams_url, headers=headers)
teams = teams_request.json()["teams"]
# get the team to search for from the list
team = [team for team in teams if team['shortName'] == team_to_find][0]
team_name = team['name']
team_id = team['id']

Затем я сделал запрос на события для этой команды. Я ограничил поиск периодом времени, который мы указали при определении наших переменных. Я обозначил эти данные как «исторические», поскольку практически столько времени, сколько можно отследить, чтобы иметь возможность измерить реальное среднее количество голов, забитых командой за игру. Любая дата меньше указанной начала может быть не идеальной для включения, так как это может повлиять на реальное количество голов, забитых командой за игру (количество голов, которое мы ожидаем, что команда забьет в игре).

build_historical_matches_url = "{}/teams/{}/matches?dateFrom={}&dateTo={}".format(api_url, team_id, date_start, date_end)
matches_request = requests.get(build_historical_matches_url, headers=headers)
historical_matches = matches_request.json()

Анализ:

Отличное место для начала — узнать количество голов, забитых «Арсеналом» за игру, используя последние исторические данные. Мы можем предположить, что это среднее число голов, которое мы ожидаем, что Арсенал забьет в игре. Основываясь на том, как был разработан API, мне пришлось немного поработать, чтобы получить список домашних и выездных голов команды, просуммировать их и найти среднее значение. Фрагмент кода показан ниже:

historical_matches = [match for match in historical_matches["matches"]]

h_away_goals = [match['score']['fullTime']['away'] for match in historical_matches if match['awayTeam']['id'] == team_id ]
h_home_goals = [match['score']['fullTime']['home'] for match in historical_matches if match['homeTeam']['id'] == team_id ]

# Check for average home goals
h_avg_home_goals = np.mean(h_home_goals)
h_avg_away_goals = np.mean(h_away_goals)

h_avg_goals_per_game = (h_avg_home_goals + h_avg_away_goals) / 2
print("Historical Average Goals Per Game: " + str(round(h_avg_goals_per_game, 1)))

Это хорошая отправная точка, хотя разумнее будет проанализировать более свежие данные (скажем, последние 60 игр).

# get recent data

recent_home_goals = h_home_goals[-30:]
recent_away_goals = h_away_goals[-30:]

goals_combined = recent_home_goals + recent_away_goals

avg_goals_combined = sum(goals_combined)/len(goals_combined)
# from the more recent games, let's select random scores for our hypothesis test
print("Recent Goals Average: " + str(avg_goals_combined))

Эта популяция казалась отличным источником для моей гипотезы. Из более свежих данных я получил в среднем 2,1 гола. Это означает, что мы ожидаем, что «Арсенал» будет забивать 2,1 гола за игру в Премьер-лиге. Теперь давайте возьмем более свежие командные цели (с достаточно большим размером выборки). Хорошим регионом в этом случае будет использование данных за текущий сезон (если он достаточно большой) или достаточно больших данных после крупных изменений в команде (обычно > 30). В моем случае я получил количество голов в прошлом сезоне (поскольку мы сейчас в отличной форме), то есть в общей сложности в 38 играх, и получил среднее значение.

# Get data from last season (last 38 games)
ls_home_goals = h_home_goals[-19:]
ls_away_goals = h_away_goals[-19:]

ls_goals_combined = ls_home_goals + ls_away_goals

ls_avg_goals_combined = sum(ls_goals_combined)/len(ls_goals_combined)
print("Average Goals Last 38 games: " + str(sum(ls_goals_combined)/len(ls_goals_combined)))

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

Проверка гипотез

Теперь мы можем задать вопрос; Если среднее количество голов за игру из последних 38 игр составляет 2,3, а ожидаемое количество голов за игру равно 2,1, существует ли значительная разница между этими двумя средними значениями и можем ли мы сделать вывод, что наше недавнее среднее значение получено от другой группы населения, чем ожидаемое количество голов за игру? Другими словами, является ли средняя цель 2,3 случайной?

Задав этот вопрос, я определил свою нулевую и альтернативную гипотезы, используя порог значимости 0,05 и доверительный интервал 95 %, как указано ниже:
Нулевая гипотеза: среднее количество голов, забитых «Арсеналом» за игру, равно 2,1
Альтернативная гипотеза: среднее количество голов, забитых «Арсеналом» за игру, больше 2. 1

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

# Let's simulate repeated random samples from our recent data
avg_goals_list = []
for i in range(500):
    goals = np.random.choice(goals_combined, size=38, replace=False)
    goals_avg = np.mean(goals)
    avg_goals_list.append(goals_avg)

И мы используем список для отображения нашего нулевого распределения:

plt.hist(avg_goals_list)
plt.title("Null Distribution of Sample Means for Goals Scored")
plt.xlabel("Goals")
plt.ylabel("Freq")
plt.show()
plt.clf()

Большой. У меня есть нормально распределенная гистограмма. Глядя на нулевое распределение, мы видим, что средняя цель 2,3 появляется в очень сжатой области нашей гистограммы, что подтверждает, что наша нулевая гипотеза верна. После этого я приступил к проверке доверительного интервала 95%.

interval = np.percentile(avg_goals_list, [2.5, 97.5])
print(interval)
#returns array([1.86842105, 2.42105263])

Следовательно, исходя из результатов, полученных выше, мы на 95% уверены, что команда забивает от 1,8 до 2,4 голов за игру. Наконец, мы находим P-значение, используя нашу альтернативную гипотезу (средние цели > 2,1), как показано ниже:

vals = [val for val in avg_goals_list if val > 2.316]
p_val = np.sum(vals)/len(avg_goals_list)

print(round(p_val, 2))
# 0.32

Из полученного значения p (0,32) выше мы можем видеть, что значение p выше нашего значимого порога 0,05, поэтому мы принимаем нулевую гипотезу и делаем вывод, что существует 32% вероятность того, что среднее количество голов, забитых за игру, равно 2,1 или выше. Таким образом, несмотря на то, что среднее значение отличалось от ожидаемого среднего, оно было недостаточно значительным, чтобы говорить о том, что на самом деле оно было получено от другого населения. Да, мы можем согласиться с тем, что «Арсенал» забил больше голов, но благодаря нашим тестам мы теперь знаем, что они забивают это число случайно, и фактическое количество голов, которые они забивают за игру, составляет 2,1.

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

Время для этого кофе :)