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

Возникает очевидный вопрос, есть ли способ использовать статистику для поиска скрытой ценности в других видах спорта, а именно в крикете. Цель этой статьи - изучить этот вопрос и посмотреть, как это можно сделать. Статья вдохновлена ​​и адаптирована из этой исследовательской работы: Моделирование и симуляция для однодневного крикета. Газета будет интересна всем, кто интересуется крикетом, а также статистикой.

Большая часть подхода напрямую заимствована из статьи, но я решил внести некоторые небольшие изменения в свою методологию:

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

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

Данные

Чтобы применить этот подход, вам нужны данные по шару; вам нужен игрок с битой, который отбивал мяч, игрок, который играл в боулинг, количество шаров, сыгранных в подаче, а также результат каждого шара. К счастью, набор данных был доступен на https://cricsheet.org/. Они хранят данные в виде файлов yaml, вы можете выбрать любой набор данных, учитывая, что вы можете извлекать файлы yaml в фрейм данных. Если вы не знаете, как это сделать, вы можете загрузить набор данных, который я использовал, отсюда: https://github.com/ArslanS1997/Cricket

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

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

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

Модель

Модель начинается с создания дискретного распределения вероятностей по результатам. Для каждого мяча учитывается 7 исходов. Как показано ниже

Он рассматривает 1 как результат калитки, 2 как результат, когда мяч приводит к нулю заездов, а 3–7 соответствует результату 1–6 заездов, при этом 5 заездов исключаются, поскольку это очень редкое событие и может повлиять на результаты. Это полиномиальное распределение. По сути, нам нужно найти вероятность того, что любой шар b приведет к k-му исходу. Вероятность зависит от игрока с битой, боулера, взятых калиток и результатов сыгранных мячей b-1 до мяча b. Что в математической записи

где i - игрок с битой, j - боулер, w - калитка, b - мяч, а k - результат. Эти вероятностные оценки, по сути, и есть то, что мы пытаемся найти.

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

def outcome(row):
if(row['The_wicket']==False):
     return 1
elif(row['batsmen runs']==0):
     return 2
elif(row['batsmen runs']==1):
     return 3
elif(row['batsmen runs']==2):
     return 4
elif(row['batsmen runs']==3):
     return 5
elif(row['batsmen runs']==4):
     return 6
elif(row['batsmen runs']==6):
     return 7
else:
     return np.nan
#You can use pandas apply function to assign this to each row
df['outcome'] = df.apply(outcome, axis=1)

В документе говорится, что для оценки этих вероятностей вы можете использовать байесовскую модель скрытых переменных, основанную на упорядоченной логистической регрессии. Упорядоченная логистическая регрессия отличается от обычной логистической регрессии тем, что выводит порядковый результат или вероятность (X≤k), а не простую категоризацию или вероятность (X = k). Скрытая переменная, по сути, означает переменную, которая не наблюдается в данных, но распределение переменной можно найти с помощью наблюдаемых переменных. В документе переменная U описывается как «качество результатов отбивания» и говорится, что каждый мяч приводит к результату k, если U принимает значение между определенными пороговыми значениями.

Что такое U?

В документе эта скрытая переменная определяется четырьмя ранее упомянутыми вещами: игрок с битой, боулер, калитки (ситуация) и шар, с которым в данный момент сталкивается игрок. Для простоты в статье сначала делается этот простой вывод, где мы рассматриваем только игрока с битой и боулера.

Здесь mu_i описывает способность игрока с битой, а mu_j описывает способность игрока с битой, плюс термин ошибки. Чем выше значение mu_i, mu_j, тем лучше игрок. Чтобы найти вероятность X≤k, мы следуем этой математической процедуре. Где a_k - порог для k-го результата, а функция F () - логистическая / сигмоидальная функция. Сигмоидальная функция выводит число от 0,1, поэтому ее можно использовать для создания распределения вероятностей по всем значениям.

Все это кажется очень запутанным, особенно если вы впервые имеете дело с байесовским моделированием высокого уровня, но ключевой вывод, который вам нужно сделать из вышеприведенного вывода, заключается в том, что когда у нас есть три вещи mu_i, mu_j и порог, мы можем получить вероятность X≤k, просто подставив их в логистическую функцию. И чтобы вычислить вероятность X = k, нам нужно вычесть Prob (X≤k) с Prob (X≤k-1).

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

Это можно понять следующим образом: если в иннингах выбрано 1–15 оверов и потеряны 0–3 калитки, ситуация в матче равна 1, аналогично, если выбито 36–50 оверов и потеряно 7–9 калиток, то мы находятся в 9. Вы можете получить представление.

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

# Index variable denotes the overs played
def assign_situation(row):
  if(row['Wickets_lost']>=0 and row['Wickets_lost'] <=3):
    if(row['index']>0 and row['index']<=5):
      return 1
    elif(row['index']>5 and row['index']<=15):
      return 2
    elif(row['index']>15):
      return 3
  elif(row['Wickets_lost']>3 and row['Wickets_lost'] <=6):
    if(row['index']>0 and row['index']<=5):
      return 4
    elif(row['index']>5 and row['index']<=15):
      return 5
    elif(row['index']>15):
      return 6
  elif(row['Wickets_lost']>6 and row['Wickets_lost'] <=10):
    if(row['index']>0 and row['index']<=5):
      return 7
    elif(row['index']>5 and row['index']<=15):
      return 8
    elif(row['index']>15):
      return 9

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

Где нижний индекс l обозначает ситуацию, а a_lk - порог для l-й ситуации и k-го результата, delta_l - это переменная, которая включает «давление», поскольку ситуация совпадения становится более сложной.

Эти delta_1, delta_2 - два новых параметра, которые нам нужно оценить, которые дадут нам значение delta_l для каждой ситуации. Настоятельно рекомендуется сделать паузу в этот момент, чтобы подумать над каждым упомянутым моментом, а также прочитать бумагу вместе с моим закодированным сообщением, чтобы лучше понять. Как и раньше в этой новой модели, если мы хотим вычислить Prob (X = k), нам нужно следующее: нам нужно вычесть Prob (X≤k) и Prob (X≤k-1) или в форме логистической функции

Оценка параметров

Теперь мы знаем, что нам нужно делать, если у нас есть все mu, a_lk и deltas, но как их оценить. Поскольку это байесовская настройка, нам необходимо задать этим параметрам некоторые начальные распределения. В документе эти априорные распределения используются для всех наших параметров.

Чтобы вычислить апостериорные оценки из этих априорных значений, вам понадобится пакет статистического программного обеспечения. Вы можете использовать любое программное обеспечение, способное делать это, но я предпочел использовать stan, а в статье используется WINBUGS. Стэн вычисляет это апостериорное распределение, используя методы Монте-Карло цепи Маркова (MCMC), очень стандартный способ вычисления байесовских параметров.

Вы можете использовать stan в качестве библиотеки как в Python, так и в R. Все, что вам нужно, это пакеты Pystan и Rstan, а также вам нужно будет указать априорные значения и отношение модели к stan на собственном языке статистического программирования, который очень похож на C ++.

Если вы новичок в STAN, вы можете просто использовать STAN-код, который я использовал для указания модели. Или, если вы хотите узнать, как это работает, ознакомьтесь со стандартной документацией: https://mc-stan.org/users/documentation/

import pystan 
modelcode = """

data {
    int<lower=1> N; 
    int<lower =1> BatNum;
    int<lower =1> BowlNum;
    int BatIndex[N];
    int BowlIndex[N];
    int<lower=1,upper=9> l[N];
    int<lower=1,upper=7> F[N];
    int<lower=1> index[N];
    int matchballs;
}
parameters {
ordered[6] alk_tilda[9];
    real<lower=0> sigma;
    real<lower=0> tau;
    real mu_1_tilda[BatNum];
    real mu_2_tilda[BowlNum];
    
    
    
   real<lower=0,upper=1> delta_1;
   real<lower=0,upper=1> delta_2;
}
transformed parameters{
real delta[9];
real mu_1[BatNum];
real mu_2[BowlNum];
ordered[6] alk[9];
   
   
for(n in 1:9){
   delta[n] = D(l[n],delta_1,delta_2);
   alk[n] = sqrt(1/sigma)*alk_tilda[n];
   }
for (n in 1:BatNum){
mu_1[n] = sqrt(1/tau)*mu_1_tilda[n];
}
for (n in 1:BowlNum){
mu_2[n] = sqrt(1/tau)*mu_2_tilda[n];
}
}
model {
  // 
   vector[N] s;
    delta_1 ~uniform(0,1);
    delta_2 ~uniform(0,1);
      
    sigma ~ gamma(1,1);
     tau ~ gamma(1,1);
    mu_1_tilda ~ normal(0,1);
    mu_2_tilda ~ normal(0,1);
   
for(t in 1:9){
      alk_tilda[t] ~ normal(0,1);
    }
 for(i in 1:N){
  s[i] =  mu_1[BatIndex[i]] + delta[l[i]] - mu_2[BowlIndex[i]];
   
   
 }
 for(i in 1:N){
F[i]~ ordered_logistic(s[i]',alk[l[i]]);
 }
}
"""
sm = pystan.StanModel(model_code=modelcode)

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

# First creating a list of all the batsman and bowlers
batsman =complete_data_train['batsman'].unique()
bowler = complete_data_train['bowler'].unique()
#Assigning an index to each bowler and batsman respectively
bowler = [[bowler[i-1],i] for i in range(1,len(bowler)+1)]
batsman = [[batsman[i-1],i] for i in range(1,len(batsman)+1)]
#Situation of each ball
l = [int(x) for x in complete_data_train['situation']]
#the actual outcome
F = [int(x) for x in complete_data_train['outcome']]
#Functions that assign each batsman and bowler with their respective index in our dataframe
complete_data_train['BatIndex'] = complete_data_train.apply(assign_num, theList= batsman, string = 'batsman',axis=1)
complete_data_train['BowlIndex'] = complete_data_train.apply(assign_num, args=(bowler,'bowler',),axis=1)
#converting that into a list
BatIndex = [int(x) for x in complete_data_train['BatIndex2']]
BowlIndex = [int(x) for x in complete_data_train['BowlIndex2']]
BatNum = int(len(batsman))
BowlNum = len(bowler)
N = int(len(complete_data_train))
matchballs = 240
index = [int(x) for x in complete_data_train['balls']]
data = {'N':N,'l':l,'F':F,'BatIndex':BatIndex,'BowlIndex':BowlIndex,'BatNum':BatNum,'BowlNum':BowlNum,'matchballs':matchballs,'index':index}
#Stan fit object will have all the trained parameters.
fit = sm.sampling(data, iter=2000, chains=4,n_jobs=1,verbose=True,refresh=100,control={'max_treedepth': 10})

В наборе данных было около 25000 мячей, 86 уникальных игроков с битой и 76 уникальных боулеров. Таким образом, общие параметры для оценки равны 1 + 1 (дельты) + 9 (6) (a_lk) + 86 (игрок с битой) и 76 (боулеры), что равняется 218 параметрам для оценки. При текущих настройках итерации на обучение набора данных ушло 3-4 часа.

По окончании обучения мы берем среднее значение таких результатов, как это

# Extract output 
alk=fit.extract()['alk']
mu1 = fit.extract()['mu_1']
mu2 = fit.extract()['mu_2']
del1 = fit.extract()['delta_1']
del2 = fit.extract()['delta_2']
# Assign mean 
#using previously made lists with name and index of bowler & batsman # you can assign the mu_i and mu_j
batsman = pd.DataFrame(batsman, columns=['name','number'])
bowler = pd.DataFrame(bowler, columns=['name','number'])
batsman['mean'] = [x for x in mu1.mean(axis=0)]
bowler['mean'] = [x for x in mu2.mean(axis=0)]
# estimates for deltas
d1 = del1.mean(axis=0)
d2 = del2.mean(axis=0)
# estimates for thresholds 
alk=pd.DataFrame(alk.mean(axis=0),columns=['mean_'+str(x) for x in range(1,7) ])

Вывод каждого фрейма данных должен быть таким:

Быть в курсе

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