На прошлой неделе я сделал обзор ShotTrack от PGA Tour и о том, как они использовали его для измерения показателя количества штрихов. Теперь мы собираемся использовать данные курса и отдельных игроков, чтобы попытаться оценить шансы игрока на победу в турнире с учетом его навыков и положения.

Пример

Художники используют реальные жизненные события, чтобы вдохновить их на свои работы. Я не могу придумать более высокую форму искусства, чем статья Medium о моделировании турниров по гольфу на JavaScript, поэтому я сделаю то же самое: я возьму текущий турнир, The American Express (интересно, кто спонсирует?), И вычислю каждый шанс игрока на победу. Посмотрим, как все выглядит!

Вот наш план на игру:

  1. Получите статистику поля, чтобы создать распределение вероятностей для каждой лунки (например, вероятность выпадения птички 15%, номинала 75%, пугала 9%, двойного пугала 1%).
  2. Получите статистику игрока, а именно общее количество набранных ударов, чтобы определить уровень навыков игрока и скорректировать распределения вероятностей в зависимости от того, насколько выше / ниже среднего находится игрок (например, в указанной выше лунке, возможно, у игрока A вероятность выпадения птички составляет 16%, а вероятность выпадения птицы - 8%. шанс пугала).
  3. Напишите функцию для генерации случайного результата для каждого игрока на каждой лунке.
  4. Сделайте еще одну функцию для моделирования раунда каждого игрока и, таким образом, для определения победителя в данной модели (примечание: если турнир заканчивается вничью, мы случайным образом выбираем победителя из равных игроков, при этом все игроки имеют одинаковый вес. Не совсем так. реалистично, но достаточно близко!). Несмотря на то, что 71 игрок сделал сокращение и, таким образом, «мог» выиграть, мы собираемся ограничиться 12 лучшими в этом упражнении. На самом деле весьма вероятно, что один из этих игроков в конечном итоге выиграет, так что это не смехотворное предположение.
  5. Создайте одну последнюю, всеобъемлющую функцию, которая запускает столько симуляций, сколько пожелает игрок (например, 1000), и выводит # или% побед каждого игрока.

А теперь перейдем к делу!

Статистика курса

К счастью, на веб-сайте курса есть небольшие краткие обзоры для каждой лунки! Мы будем использовать это для создания базового распределения вероятностей. Мне, вероятно, следует использовать функции JS DOM, чтобы программно перебирать каждую дыру и извлекать числа, но (1) их всего 18 дырок, и (2) сейчас воскресенье, и мне лень, хорошо? Вот что мы получим через 5 кропотливых минут:

// Note: the Object keys are equal to the score under/over par.
-2 = eagle, -1 = birdie, 0 = par, 1 = bogey, 2 = double bogey
let holeProbs = {
    1: {
      "-2": 0,
      "-1": 0.21,
      "0": 0.67,
      "1": 0.12,
      "2": 0
    },
    2: {
      "-2": 0,
      "-1": 0.23,
      "0": 0.70,
      "1": 0.07,
      "2": 0
    },
    3: {
      "-2": 0,
      "-1": 0.21,
      "0": 0.67,
      "1": 0.12,
      "2": 0
    },
    4: {
      "-2": 0,
      "-1": 0.149,
      "0": 0.759,
      "1": 0.089,
      "2": 0.003
    },
    5: {
      "-2": 0.01,
      "-1": 0.375,
      "0": 0.425,
      "1": 0.145,
      "2": 0.045
    },
    6: {
      "-2": 0,
      "-1": 0.05,
      "0": 0.70,
      "1": 0.20,
      "2": 0.05
    },
    7: {
      "-2": 0,
      "-1": 0.24,
      "0": 0.64,
      "1": 0.07,
      "2": 0.05
    },
    8: {
      "-2": 0.0075,
      "-1": 0.4575,
      "0": 0.4875,
      "1": 0.0475,
      "2": 0
    },
    9: {
      "-2": 0,
      "-1": 0.20,
      "0": 0.62,
      "1": 0.14,
      "2": 0.04
    },
    10: {
      "-2": 0,
      "-1": 0.21,
      "0": 0.69,
      "1": 0.08,
      "2": 0.02
    },
    11: {
      "-2": 0.01,
      "-1": 0.3275,
      "0": 0.5775,
      "1": 0.0675,
      "2": 0.0175
    },
    12: {
      "-2": 0,
      "-1": 0.2875,
      "0": 0.6075,
      "1": 0.0975,
      "2": 0.0075
    },
    13: {
      "-2": 0,
      "-1": 0.10,
      "0": 0.71,
      "1": 0.15,
      "2": 0.04
    },
    14: {
      "-2": 0,
      "-1": 0.2575,
      "0": 0.6675,
      "1": 0.0575,
      "2": 0.0175
    },
    15: {
      "-2": 0,
      "-1": 0.0925,
      "0": 0.8025,
      "1": 0.1025,
      "2": 0.0025
    },
    16: {
      "-2": 0.01,
      "-1": 0.395,
      "0": 0.525,
      "1": 0.065,
      "2": 0.005
    },
    17: {
      "-2": 0.00,
      "-1": 0.1125,
      "0": 0.7025,
      "1": 0.0825,
      "2": 0.1025
    },
    18: {
      "-2": 0,
      "-1": 0.1425,
      "0": 0.6825,
      "1": 0.1325,
      "2": 0.0425
    }
  };

Несколько примечаний:

  • Я дал вероятность орла 1% для всех пар 5. Это может быть не очень реалистично, но и 0%!
  • Я пометил все «двойные пугала или того хуже» как двойные пугали. Это также нереально, поскольку тройные призраки и выше действительно случаются даже с профессионалами, но они достаточно редки, чтобы мы могли отказаться от них для целей этого упражнения.

Навыки игрока

Давайте найдем Общее количество набранных ходов (SGT) для каждого из лидеров:

// Note: I used 2019 data for Molinari, since 2020 was unavailable due to lack of PGA Tour tournaments played
const playerAdjustments = {
  "Max Homa": 0.279,
  "Si Woo Kim": 0.267,
  "Tony Finau": 1.243,
  "Richy Werenski": 0.484,
  "Russell Knox": 0.030,
  "Brian Harman": 0.767,
  "Emiliano Grillo": -0.182,
  "Cameron Davis": 0.560,
  "Rory Sabbatini": 0.071,
  "Chase Seiffert": 0.196,
  "Francesco Molinari": 0.038,
  "Doug Ghim": -0.520
}

Обратите внимание, что 10 из 12 игроков имеют положительный SGT, то есть выше среднего. Неудивительно, что лучшие игроки чаще соревнуются и побеждают в турнирах!

Далее внесем следующие корректировки:

  • SGT игрока за раунд делится на 18 и применяется к каждой лунке. Например, SGT Макса Хомы составляет 0,279, поэтому мы предположим, что у него 0,279 / 18 = 0,0155 гребка на лунку лучше.
  • Регулировка для каждой лунки будет выполняться путем увеличения% птички и уменьшения% пули. В идеале мы бы также изменили среднее значение орла / дабл-богги, но опять же, в воскресенье днем, так что я делаю здесь несколько сокращений! Вот пример:

SGT игрока A = 0,01 за лунку

// Hole 1
{
 “-2”: 0,
 “-1”: 0.21,
 “0”: 0.67,
 “1”: 0.12,
 “2”: 0
},

Поэтому мы увеличим вероятность -1 (птичка) на 0,01 / 2 = 0,005 и уменьшим вероятность 1 (пугала) на 0,005. Итак, распределение лунки 1 игрока А теперь будет выглядеть следующим образом:

// Max Homa Hole 1
{
 “-2”: 0,
 “-1”: 0.215,
 “0”: 0.67,
 “1”: 0.115,
 “2”: 0
},

Моделирование отверстий

Во-первых, давайте возьмем объекты с отверстиями и преобразуем их в массивы, используя сложный фрагмент кода:

holeProbs = Object.values(holeProbs).map(hole=>{
    const sortedKeys = Object.keys(hole).sort((a,b)=>parseInt(a)>parseInt(b)?1:-1);
    let sum=0;
    const sums=[];
    for (const key of sortedKeys) {
        sum+=hole[key];
        sums.push(sum);
    }
    return sums;
});

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

function randomHoleScore(holeDist) {
  const randNum = Math.random();
  return [...Array(holeDist.length).keys()].find(ix=>holeDist[ix]>random)-2;
}

Это вернет число от -2 до 2 (от орла до двойного пугала), основанное на случайном числе и распределении вероятностей.

Игрок Раунд

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

function playerRound(playerObj, courseObj) {
  Object.values(courseObj).forEach(holeDist=>{
    holeDistCopy = JSON.parse(JSON.stringify(holeDist));
    holeDistCopy[1] += playerObj.SGT / (18 * 2);
    holeDistCopy[2] += playerObj.SGT / (18 * 2);
    playerObj.score += randomHoleScore(holeDistCopy);
  })
}

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

function simulateRound(playersArray, courseObj) {
  playersArray.forEach(player=>playerRound(player, courseObj));
  const bestScore = Math.max(...playersArray.map(player=>player.score));
  const winners = players.filter(player=>player.score=bestScore);
  return winners;
}

Моделировать N турниров

Выполняя это упражнение, я нашел способ сделать это немного лучше. Вот моя последняя функция, которая создает копию ввода playerArray и моделирует N турниров. Каждый игрок начинает с 0 побед. В конце каждого турнира выигрыши победителей / ковиннеров увеличиваются. Функция возвращает объект, где ключи - это имя каждого игрока, а значения - это количество побед, разделенное на N, то есть ожидаемый процент выигрыша игрока:

function winProbSimulator(playersArray, courseObj, N=100) {
    const winners = {};
    playersArray.forEach(player=>winners[player.name]=0);
    for (let i=0; i<N; i++) {
        const playersCopy = JSON.parse(JSON.stringify(playersArray));
        const bestScore = {score: 1000, count: 1};
        playersCopy.forEach(player=>{
            playerRound(player, courseObj);
            if (player.score < bestScore.score) {
                bestScore.score = player.score;
                bestScore.count = 1;
            }
            else if (player.score === bestScore.score) {
                bestScore.count += 1;
            }
        });
        playersCopy.forEach(player=>{
            if (player.score===bestScore.score) {
                winners[player.name] += 1 / bestScore.count;
            }
        })
    }
    Object.keys(winners).forEach(key=> winners[key] /= N);
    return winners;
}

Вот пример выполнения с N = 10 000:

Этот вывод определенно проходит «тест на обнюхивание»! У трех игроков, вышедших в лидеры в начале дня, самый высокий процент побед, а у Тони Финау, сильнейшего из трех игроков, на сегодняшний день самые высокие шансы из трех (30% против ~ 18% у двух других игроков). ). Похоже, мы создали симулятор разумного вида!

Вывод

На этом завершается еще одно упражнение по статистике гольфа. Надеюсь, вам понравилось ... Шучу, я знаю, что не понравилось, но мне понравилось!

P.S. Победителем с разницей в 1 выстрел стал… Си У Ким!