Использование статистики, Pandas, BeautifulSoup и AWS для определения выгодных ставок

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



В этом году я перестроил систему с нуля, чтобы найти возможности для ставок в шести различных лигах (EPL, La Liga, Bundesliga, Ligue 1, Serie A и RFPL).

Завершив свою последнюю модель в конце декабря 2019 года, я начал испытывать ее, делая ставки по 25 фунтов стерлингов каждую неделю. К сожалению, мне удалось уместиться только за восемь недель ставок, прежде чем COVID-19 сократил EPL.

Хорошая новость заключается в том, что в этот период я ​​оказался безубыточным, я поставил 200 фунтов и получил обратно 200 фунтов стерлингов.

Но я здесь не для того, чтобы окупиться.

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

Методология и кодекс

В новой модели используется много кода, который использовался в предыдущей модели, и ее можно упростить до четырех этапов:

  1. Рассчитайте среднее ожидаемое количество голов каждой команды
  2. Настройте это значение в зависимости от формы, счета дома / на выезде и защиты соперников
  3. Преобразуйте это скорректированное среднее значение ожидаемых голов в предполагаемые вероятности и коэффициенты для ставок больше / меньше
  4. Сравните эти коэффициенты с лучшими коэффициентами, предлагаемыми букмекерскими конторами на большее / меньшее количество ставок, и делайте ставки, исходя из вероятности исхода

1. Ожидаемые цели

«Ожидаемые голы» (xG) гораздо лучше отражают результативность команды, чем удары по воротам. Вместо того, чтобы рассматривать каждый «выстрел в цель» одинаково, xG учитывает качество каждого сделанного выстрела, глядя на то, где он был сделан, какой ногой он был сделан, и «стиль игры».

Если вам интересно узнать больше об этой революционной метрике, я расскажу о ней подробнее в этой статье:



На этом этапе рассматриваются прогнозируемые составы каждой команды и их индивидуальные значения xG за последние n (обычно 6) игр. Ранее я брал составы из Fantasy Football Scout:



Но чтобы собрать составы из лиг со всего мира, мне нужно собрать составы с веб-сайта, посвященного играм со всего мира!

Я беру HTML для каждого приспособления в данном сезоне:

html = requests.get(f"https:/X/leagues?id={league_id}&type=league")
all_season_fixtures = [x["pageUrl"] for x in html.json()["fixtures"]]

Затем я получаю ID каждого прибора из этого списка, который есть на текущей игровой неделе (current_fixtures, взятые из API Football):

final_fixture_ids = []
for fixture in all_season_fixtures:
    if fixture.split('/')[4] in current_fixtures:
        final_fixture_ids.append(fixture.split('/')[2])

Я извлекаю HTML из каждого приспособления:

for fixture in final_fixture_ids:
    url = https:/X/?matchId={fixture}'
    data = requests.get(url, headers=headers, allow_redirects=False).json()
    lineups.update(parse_lineups(data))

И я разбираю HTML:

def parse_lineups(raw_data: Dict) -> Dict:
    lineups = {}
    for team in raw_data['content']['lineup']['lineup']:
        team_name = team['teamName']
        lineup = {}
        for positions in team['players']:
            for player in positions:
                lineup[player['name']] = player['id']
        lineups[team_name] = lineup
    return lineups

Затем программа суммирует средние ожидаемые голы каждого игрока (avG) из базы данных атрибутов игроков и делит его на количество игроков (11). Другими словами, это среднее ожидаемое количество голов всей команды за определенный период.

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

2. Корректировки

После расчета xG для каждой команды, играющей в определенный день, вносятся следующие корректировки:

  • Дома / на выезде. Я предполагаю, что команды, играющие дома, в среднем будут забивать на 10% больше голов, а команды, играющие на выезде, будут забивать в среднем на 5% меньше.
for fixture in fixtures:
    df.loc[df['team_name'] == fixture['homeTeam']['team_name'], 'home_away_adjustment'] = master_params()['HOME_ADVANTAGE']
    df.loc[df['team_name'] == fixture['awayTeam']['team_name'], 'home_away_adjustment'] = master_params()['AWAY_ADVANTAGE']
df['avG_adjusted'] = df.av_xG * df.home_away_adjustment
  • Защита из оппозиции. Сравнивая предыдущие xG соперника с их фактическими голами, забитыми в игре, я создаю «фактор защиты», который использую. для корректировки их среднего xG. Например. если у команды был коэффициент xG 2.35 и они забили только один гол, то соперник должен был хорошо защищаться.
for result in recent_results:
    xG_diff = float(result['xG'][opponents_side]) - float(result['goals'][opponents_side])
    xG_diff_total = xG_diff_total + xG_diff

xG_diff_avg = xG_diff_total / fixture_history
  • Форма - эта корректировка учитывает, сколько из последних n игр команда выиграла / сыграла вничью / проиграла дома или на выезде. Также учитывается их недавняя «серия», будь то серия побед, ничьих или поражений.
# Home side wins
if fixture['goalsHomeTeam'] > fixture['goalsAwayTeam']:
    if fixture['awayTeam']['team_name'] in list(df_team.index.values):
        update_dataframe('a', 'loss', df_team, fixture)
    elif fixture['homeTeam']['team_name'] in list(df_team.index.values):
        update_dataframe('h', 'win', df_team, fixture)

Это создает окончательный фрейм данных перед вычислением вероятностей и шансов.

3. Создание предполагаемых вероятностей и шансов

Теперь, когда у нас есть средние ожидаемые голы команды в данной игре, мы можем поместить это в распределение Пуассона.

for goals_scored in range(largest_goals_scored):
    for team in teams:
        avG = df_Poisson.at[goals_scored, team]
        df_Poisson.at[goals_scored, team] = ((math.exp(-avG) * math.pow(avG, goals_scored)) / math.factorial(goals_scored))

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

Я исхожу из предположения, что, поскольку забить более пяти голов в данной игре маловероятно, я рассчитываю только вероятность от нуля до пяти голов (хотя в этом сезоне ЗАПОЛНЕНО головами ИСТОЧНИК). Итак, теперь у нас есть вероятность забить гол для каждой команды, пришло время создать собственные коэффициенты.

3.5 Создание «самодельных» коэффициентов

Моя стратегия ставок в настоящее время сосредоточена на ставках «больше и меньше», которые касаются команд, забивающих больше x или меньше y голов. Например. Готов поспорить, что «Манчестер Юнайтед» забьет более двух голов «Астон Вилле».

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

Например. для голов «больше 1,5» я бы суммировал вероятности того, что команда забьет 2, 3, 4 и 5 голов.

df_implied_probability.at[team, 'O1_5'] = df.at[team, '2'] + df.at[team, '3'] + df.at[team, '4'] + df.at[team, '5']

Шансы - это просто обратная величина предполагаемой вероятности, поэтому инвертирование этого числа дает мне мои собственные самодельные шансы на «больше и меньше»!

Если вам интересно, я просмотрел часть кода, который использовал для создания этих фреймов данных в предыдущей статье:



4. Поиск лучших шансов и стратегии

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

Эти данные отформатированы точно так же, как и мой предыдущий фрейм данных, поэтому я получаю следующее:

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

df_betting_alpha = df_bookies_odds - df_over_unders

Когда значения в моем фрейме данных «df_betting_alpha» положительны, это означает, что в этой ставке есть «ценность», потому что букмекеры имеют лучшие шансы, чем предполагают мои домашние шансы:

Поэтому, когда шансы букмекеров выше, чем домашние шансы, это означает, что это потенциально хорошая ставка. На изображении выше модель правильно определила, что ставка на то, что Ливерпуль не забьет столько голов, как обычно, была бы выгодной ставкой (U1_5 и U2_5).

4.5 Стратегия ставок

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

Для каждой ставки, которая потенциально может быть сделана, есть три пороговых значения, которые должны быть превышены, чтобы ставка была сделана:

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

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

Сумма каждой ставки определяется критерием Келли:

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

Сохранение данных в AWS S3

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

Этот фрагмент кода демонстрирует, как построчно сохранять фрейм данных:

for index, row in df.iterrows():
    single_data_row = {}
    for series_index, series_value in row.items():
        single_data_row[series_index] = series_value
    team = index.replace(' ', '_')
    csv_buffer = StringIO()
    headers = list(single_data_row.keys())
    writer = csv.DictWriter(csv_buffer, fieldnames=headers)
    writer.writeheader()
    writer.writerow(single_data_row)
    filepath = f'data/{season}/overunders/{league_name}/{team}/{current_round_formatted}/{filename}'
    logger.info(f'Saving to {filepath}')
    s3_response = s3_resource.Object(s3_bucket, filepath).put(Body=csv_buffer.getvalue())

Заключение

Теперь алгоритм может рассчитывать и сравнивать шансы на результаты в шести различных футбольных лигах.

Возможности для ставок определяются путем анализа четырех показателей (xG, форма, дома / в гостях и защита соперника) для создания «домашних» коэффициентов, которые затем сравниваются с лучшими коэффициентами, предлагаемыми букмекерами.

Расширение этого алгоритма на шесть различных лиг позволяет определять больше возможностей для ставок в течение сезона.

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