Можно ли предсказать, у кого из игроков НБА будет сезон прорыва, еще до его начала? Это вопрос, на который я пытался ответить здесь, и есть некоторые удивительные результаты. Для специалиста по данным, фаната НБА (Go Kings!) и баскетболиста-фантаста, объединение всех этих увлечений в одном проекте было интересной возможностью создать очень веселый проект. Есть некоторые интригующие идеи, и если вам нужен мой прогноз на предстоящий сезон 2023–2024 годов, вам следует прочитать его до конца (или просто перейти туда).

В этой статье я дал довольно подробное описание этапов проекта, включая мыслительный процесс и примеры кодирования. Он включает в себя как концепции науки о данных, так и концепции и идеи баскетбола. Если вам не интересно читать и вы просто хотите посмотреть код и данные, вы можете посетить репозиторий проекта на GitHub здесь: https://github.com/royyanovski/nba_players_breakout_project

Оглавление

· Введение
· Получение данных
· Добавление функций
· Определение прорывного сезона
· Последние приготовления
· Моделирование — Алгоритмы и результаты
· Сравнение производительности
· Обсуждение важности характеристик
· Прогнозирование прорывных игроков на следующий (2023–24) сезон

Введение

Любой фэнтезийный игрок НБА скажет вам, что в лигах с передрафтом самым важным фактором успеха является ваш выбор в ночь драфта. Выбор не лучших игроков. Эти игроки — проверенные звезды, которых будут выбирать там, где они должны. Большие деньги в кандидатах на прорыв, в игроках, которые проведут сезон значительно лучше, чем в прошлом году, они действительно меняют правила игры. Так как же их идентифицировать? Многие аналитики фэнтези пытаются ответить на этот вопрос перед началом каждого сезона. Это нелегко сделать, и есть много переменных, которые могут повлиять на эти прогнозы. Некоторые из них поддаются определению или измерению, в то время как другие труднее определить. Я подробно расскажу о них чуть позже, но давайте сначала определим цель этого проекта:

В этом проекте я попытаюсь предсказать лучшие сезоны игроков НБА на основе их данных за предыдущий сезон.
Это было сделано с использованием классических методов машинного обучения (EDA, очистка данных, предварительная обработка, разработка функций, выбор функций и т. д.) и моделей (логистическая регрессия, дерево решений, случайный лес, AdaBoost, XGBoost, LightGBM). ).
**Если вы новичок в области науки о данных, вы можете столкнуться с некоторыми новыми терминами и понятиями. Хотя во многих случаях я их не объясняю, я стараюсь в этих случаях добавлять ссылки на пояснения.

Получение данных

Отлично, у нас есть определенная цель, теперь нам нужны данные. Я получил данные из надежного Python API НБА. Для установки библиотеки используйте:

pip install nba_api

Вы можете найти документацию здесь: https://github.com/swar/nba_api/tree/master

Я решил взять данные только об активных игроках НБА (примерно 500 игроков), потому что, не вдаваясь в подробности для тех, кто не является фанатами НБА, игра значительно изменилась за последние несколько десятилетий, и я предположил, что игроки прошлого данные будут вносить шум, который снизит производительность моделей, поэтому они были исключены (извинения перед MJ и ребятами). Данные, которые я использовал, можно получить из конечной точки playercareerstats, которая будет возвращать сезонную статистику игрока, но для этого требуется идентификатор игрока в качестве входного параметра, и он может получать данные по одному игроку за раз. Идентификаторы игроков можно получить из players.get_active_players. Вот как я это сделал:

import pandas as pd
from nba_api.stats.endpoints import playercareerstats
from nba_api.stats.static import players

active_players = players.get_active_players()

players_data = pd.DataFrame()
for player in active_players:
    player_df = playercareerstats.PlayerCareerStats(player_id = player['id']).get_data_frames()[0]
    player_df['player_name'] = [player['full_name'] for i in range(len(player_df))]
    players_data = pd.concat([players_data, player_df])
players_data.reset_index(drop=True, inplace=True)

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

 PLAYER_ID  SEASON_ID  LEAGUE_ID  TEAM_ID  TEAM_ABBREVIATION  PLAYER_AGE  GP  GS  MIN   FGM  ...  OREB  DREB  REB  AST  STL  BLK  TOV  PF  PTS  player_name
0 203500     2013-14      00    1610612760       OKC             20.0     81  20 1197.0  93  ...  142   190   332  43   40   57   71   203 265  Steven Adams
1 203500     2014-15      00    1610612760       OKC             21.0     70  67 1771.0  217 ...  199   324   523  66   38   86   99   222 537  Steven Adams
2 203500     2015-16      00    1610612760       OKC             22.0     80  80 2014.0  261 ...  219   314   533  62   42   89   84   223 636  Steven Adams
3 203500     2016-17      00    1610612760       OKC             23.0     80  80 2389.0  374 ...  281   332   613  86   89   78   146  195 905  Steven Adams
4 203500     2017-18      00    1610612760       OKC             24.0     76  76 2487.0  448 ...  384   301   685  88   92   78   128  215 1056 Steven Adams
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
4 1627826    2018-19      00    0                TOT             22.0     59  37 1039.0  212 ...  115   247   362  63   14   51   70   137 525  Ivica Zubac
5 1627826    2019-20      00    1610612746       LAC             23.0     72  70 1326.0  236 ...  197   346   543  82   16   66   61   168 596  Ivica Zubac
6 1627826    2020-21      00    1610612746       LAC             24.0     72  33 1609.0  257 ...  189   330   519  90   24   62   81   187 650  Ivica Zubac
7 1627826    2021-22      00    1610612746       LAC             25.0     76  76 1852.0  310 ...  217   427   644  120  36   77   114  203 785  Ivica Zubac
8 1627826    2022-23      00    1610612746       LAC             25.0     30  30 886.0   119 ...  98    226   324  33   12   47   64   93  302  Ivica Zubac

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

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

Добавление функций

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

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

new_team = []
for i in range(len(players_data)-1):
    if players_data.loc[i, 'PLAYER_ID'] == players_data.loc[i+1, 'PLAYER_ID'] and players_data.loc[i, 'TEAM_ID'] != players_data.loc[i+1, 'TEAM_ID']:
        new_team.append(1)
    else:
        new_team.append(0)
new_team.append(0)
players_data['NEW_TEAM'] = new_team

Здесь нужно объяснить два важных момента:
1. Мы хотим убедиться, что это тот же игрок, который сменил команду, а не просто разные игроки.
2. Мы хотим добавить смену команды в строку предыдущий сезон (до изменения) для нашего прогноза. Это может показаться сложным для предсказаний в реальном времени, и это действительно так, но во многих случаях мы можем узнать, меняет ли игрок команду во время летнего перерыва, до начала сезона.

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

years_in_league = [1]
year = 1
for i in range(1, len(players_data)):
    if players_data.loc[i, 'PLAYER_ID'] == players_data.loc[i-1, 'PLAYER_ID']:
        if players_data.loc[i, 'SEASON_ID'] != players_data.loc[i-1, 'SEASON_ID']:
            year += 1
            years_in_league.append(year)
        else:
            years_in_league.append(year)
    else:
        year = 1
        years_in_league.append(year)
players_data['YEARS_IN_LEAGUE'] = years_in_league

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

# First create the dataset (per player):
# Remember that you might need to seperate it to a few groups due to the API's restrictions.
from nba_api.stats.endpoints import commonplayerinfo

position = []
height = []
weight = []
country = []
draft_round = []
draft_pick = []

for i in range(len(players_ids)):
    p_id = players_ids[i]
    p_info = commonplayerinfo.CommonPlayerInfo(player_id = str(p_id)).get_data_frames()[0]
    position.append(p_info.POSITION.values[0])
    height.append(p_info.HEIGHT.values[0])
    weight.append(p_info.WEIGHT.values[0])
    country.append(p_info.COUNTRY.values[0])
    draft_round.append(p_info.DRAFT_ROUND.values[0])
    draft_pick.append(p_info.DRAFT_NUMBER.values[0])

pers_info_dict = {'ID': players_ids, 'POSITION': position, 'HEIGHT': height, 'WEIGHT': weight, 'COUNTRY': country, 'DRAFT_ROUND': draft_round, 'DRAFT_PICK': draft_pick}
pers_info_df = pd.DataFrame.from_dict(pers_info_dict)

# Next, assign the data for each row in our full dataset by matching it to the player's ID:
# (remember that each player can have more than on season- hence, more then one row in the dataset)

position2 = []
height2 = []
weight2 = []
country2 = []
draft_round2 = []
draft_pick2 = []

for i in range(len(players_data)):
    player_id = players_data.loc[i, 'PLAYER_ID']
    player_inf = pers_info_df[pers_info_df['ID'] == player_id]

    position2.append(player_inf.POSITION.values[0])
    height2.append(player_inf.HEIGHT.values[0])
    weight2.append(player_inf.WEIGHT.values[0])
    country2.append(player_inf.COUNTRY.values[0])
    draft_round2.append(player_inf.DRAFT_ROUND.values[0])
    draft_pick2.append(player_inf.DRAFT_PICK.values[0])
    
players_data['POSITION'] = position2
players_data['HEIGHT'] = height2
players_data['WEIGHT'] = weight2
players_data['COUNTRY'] = country2
players_data['DRAFT_ROUND'] = draft_round2
players_data['DRAFT_PICK'] = draft_pick2

Четвертой добавленной функцией стал рейтинг команд. Рейтинг команды может повлиять на стиль игры и стратегию команды, особенно на более поздних этапах сезона. Например, команды с более низким рейтингом обычно находятся в режиме перестроения и с большей вероятностью возлагают больше ответственности и внимания на молодых игроков. Это связано с лотерейными шансами лиги на драфте, из-за чего команды забиваются. И если это последнее предложение не имело для вас никакого смысла, ничего страшного, оно не очень важно (но если вы хотите узнать об этом больше, вы можете прочитать об этом здесь). В любом случае, чтобы получить эти данные, я использовал конечную точку teaminfocommon. Вот как это было сделано:

from nba_api.stats.endpoints import commonplayerinfo, leaguestandings, teaminfocommon

# First we need the team's ID and the season ID for each combination that existsin our dataset:
team_ids = players_data['TEAM_ID'].unique().tolist()
team_ids.remove(0)
season_ids = []
s_ids = players_data['SEASON_ID'].unique().tolist()
for s_id in s_ids:
    season_ids.append(int(s_id[:4]))

# Now we can use the API to get the conference rankings:
conf_ranks = []
for team_id in team_ids:
    for season_id in season_ids:
        rank_df = teaminfocommon.TeamInfoCommon(team_id=team_id, season_nullable=season_id).get_data_frames()[0]
        try:
            conf_ranks.append(rank_df['CONF_RANK'].values[0])
        except IndexError:
            conf_ranks.append(np.nan)

# Create a data set from it:
rank_dict = {'team_id': sum([[x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x] for x in team_ids], []), 'season_id': season_ids * 30, 'conf_ranks': conf_ranks}
rank_df = pd.DataFrame.from_dict(rank_dict)

# And add it to our full dataset as a feature:
conf_ranks_full = []
for i in range(len(players_data)):
    team_id = players_data.loc[i, 'TEAM_ID']
    season_id = int(players_data.loc[i, 'SEASON_ID'][:4])
    try:
        rank = int(rank_df[(rank_df['season_id'] == season_id) & (rank_df['team_id'] == team_id)]['conf_ranks'].values[0])
    except IndexError:
        rank = -100
    conf_ranks_full.append(rank)
players_data['CONF_RANKS'] = conf_ranks_full

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

tank_spots = [13, 14, 15]
moved_to_tank = []
moved_from_tank = []
for i in range(1, len(players_data)):
    curr_rank = players_data.iloc[i]['CONF_RANKS']
    prev_rank = players_data.iloc[i-1]['CONF_RANKS']
    curr_player = players_data.iloc[i]['PLAYER_ID']
    prev_player = players_data.iloc[i-1]['PLAYER_ID']
    if prev_rank in tank_spots and curr_rank not in tank_spots and curr_player == prev_player:
        moved_from_tank.append(1)
        moved_to_tank.append(0)
    elif prev_rank not in tank_spots and curr_rank in tank_spots and curr_player == prev_player:
        moved_from_tank.append(0)
        moved_to_tank.append(1)
    else:
        moved_from_tank.append(0)
        moved_to_tank.append(0)
players_data['TO_TANKING'] = [0] + moved_to_tank
players_data['FROM_TANKING'] = [0] + moved_from_tank

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

Определение пикового сезона

Теперь, когда у нас есть готовые функции, следующая задача — определить, как определить "сезон пиковых результатов". Как мы измеряем производительность игрока? и какой размер изменения (между сезонами) можно считать прорывом?
Я не вижу ни одного правильного ответа на этот вопрос, но я решил использовать 'фантазийные баллы', чтобы представить уровень игры игроков. Очки фэнтези — это система подсчета очков, которая используется в фэнтези-лигах, использующих этот метод (существуют и другие системы подсчета очков, но они не имеют отношения к этому проекту). Он использует 11 статистических категорий и конвертирует их в баллы. Их сумма является мерой того, насколько хорошо игрок выступил (больше очков = лучше производительность). По умолчанию используется следующая система подсчета очков:
Заброшенный бросок с игры = +1 очко
Заброшенный бросок с игры 3 очка = +3 очка
Выполненный штрафной бросок = +1 очко
Подбор = +1,5 очка
Assist = +2 очка
Steal = +3 очка
Block = +3 очка
Point = +0,5 очка
Turnover = -2 очка
Field Попытка гола = -0,45 балла
Попытка штрафного броска = -0,75 балла
Руководители фэнтези могут возразить, что это не лучший способ распределения очков, и в разных лигах используются разные методы подсчета очков, но я выбрал наиболее общая настройка для этого проекта, о которой упоминалось выше.
Чтобы определить, что следует считать сезоном прорыва, я взял значение 95-го процентиля разницы в баллах между сезонами. Результат был разумным, так как в большинстве сезонов было всего несколько прорывных игроков (как это обычно бывает). Затем был создан столбец бинарных целей, чтобы определить, у каких игроков был сезон прорыва в следующем сезоне. Как вы теперь понимаете, прогноз будет основываться только на одном сезоне игрока, чтобы определить, станет ли следующий сезон для него прорывным. Это означает, что он не принимает во внимание предыдущие сезоны и историю игрока. Это может затруднить прогнозирование таким образом, но это позволило мне рассматривать каждую запись как независимую выборку, что было важно, поскольку база данных была довольно маленькой.

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

Последние приготовления

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

from sklearn.model_selection import train_test_split

Y = clean_data['BREAKOUT_POINTS']
X = clean_data.drop(['BREAKOUT_POINTS'], axis=1)

train_X, test_X, train_Y, test_Y =train_test_split(X, Y, stratify=Y, train_size=.8, random_state=0)

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

Прежде чем обсуждать модели, давайте обсудим показатели для оценки эффективности моделей. Как уже упоминалось, данные очень несбалансированы, что означает, что в этом случае точность будет плохой метрикой. Мы будем использовать Отзыв и точность в качестве метрик, а также f1-score для сравнения моделей. Но что мы должны предпочесть, Отзыв или Точность? Это сложный вопрос, и он зависит от значения результатов для вас. Например, если вы фэнтези-игрок, вы, вероятно, предпочитаете Precision, поскольку у вас не так много возможностей рискнуть во время драфта, и вы хотите убедиться, что те, которые вы используете, хороши. Если вы аналитик фэнтези, возможно, вы хотите предоставить как можно больше хороших прогнозов прорыва, даже если многие из них будут ошибочными. Это означает, что вы предпочтете Recall. Если вы не из этих людей, вы можете просто предпочесть оценку F1, которая в некотором смысле представляет собой баланс между этими двумя.

Теперь, в качестве отправной точки для сравнения производительности, давайте проверим нашу базовую производительность. Поскольку только 5% данных помечены как сезоны прорыва (по определению), если бы мы вернули все метки как 1, наше отклонение от курса отзыва было бы 1, но наша точность была бы 0,05, а наша оценка F1 была бы 0,095. Мы можем рассматривать эти оценки в качестве нашего базового уровня. Как уже упоминалось, точность не является хорошей метрикой для этого случая и поэтому здесь не обсуждается.

Моделирование — алгоритмы и результаты

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

Второй моделью был Классификатор дерева решений.
Он начал с довольно низкой производительности при настройках по умолчанию:

from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import recall_score, precision_score

clf = DecisionTreeClassifier(random_state=0).fit(train_X, train_Y)
pred_Y = clf.predict(test_X)
print(f'Recall: {round(recall_score(test_Y, pred_Y), 3)}  Precision:{round(precision_score(test_Y.values, pred_Y), 3)}')
Recall: 0.114  Precision:0.098

Но использование поиска по сетке помогло внести небольшие улучшения с некоторой настройкой гиперпараметров. Поиск по сетке выполнялся три раза, оптимизируя различные показатели отзыва, точности и оценки f1. Если вы хотите увидеть, как это было сделано и по каким параметрам производился поиск, вы можете посетить страницу проекта на GitHub. Результаты:

Metric optimized    Recall    Precision    f1-score
Recall              0.773     0.061        0.112
Precision           0.023     0.167        0.04
f1-score            0.341     0.127        0.185

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

Во-первых, Классификатор случайного леса. С параметрами по умолчанию он имел производительность: полнота 0,0 и точность 0,0. Но поиск по сетке и некоторая настройка гиперпараметров улучшили результаты следующим образом:

Metric optimized    Recall    Precision    f1-score
Recall              0.682     0.1          0.175
Precision           0.023     0.333        0.043
f1-score            0.477     0.112        0.181

Результаты очень похожи на результаты, полученные с помощью деревьев решений.

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

Следующей моделью была XGBoost, которая, возможно, дала наилучшие результаты. После некоторой настройки его результаты составили показатель отзыва 0,705, показатель точности 0,133 и показатель F1 0,224. Для этого требовалась не только настройка параметров, но и манипулирование пороговыми значениями вероятности.
Если говорить в реальных числах, то в тестовом наборе было 44 сезона прорыва, из которых он идентифицировал 31. Всего было выявлено 233 случая, из них 31 был верным, а 202 — ложным (см. матрицу путаницы ниже). Это не очень впечатляющий результат, но он лучше, чем у предыдущих моделей, и намного лучше, чем базовый уровень.

Последней тестируемой моделью была lightGBM. После некоторой настройки он фактически дал лучший показатель F1 (0,239), чем XGBoost, из-за более высокого показателя точности (0,178), но показатель отзыва был намного ниже (0,364). По сравнению с XGBoost, он предсказывал гораздо меньше сезонов прорыва (90) и определял меньше истинных сезонов прорыва (16), но, очевидно, имел меньше ложных срабатываний.

Сравнение эффективности

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

Как показано выше, XGBoost имеет лучший показатель AUC (0,734) и лучшую кривую ROC, что делает его лучшей моделью из рассмотренных.

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

Обсуждение важности функции

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

Мы видим, что 28 из 31 функции имели некоторую степень важности. Возраст игрока кажется наиболее важной характеристикой, и это имеет большой смысл, поскольку у молодых игроков гораздо больше шансов на прорыв, чем у игроков старшего возраста. Следующие важные функции включают в себя в основном статистику, а также такие функции, как сыгранные игры (GP), начатые игры (GS) и минуты (MIN), которые связаны со статусом игрока в его команде. Последние несколько функций в списке — это особенности физических характеристик (рост и вес), положение на драфте и изменения в команде.

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

Результат здесь очень похож. функция Field Goals Made опережает функцию возраста, но мы можем ясно видеть, что это связано с тем, что алгоритм дерева решений исказил ее. Выбор драфта и континент здесь относительно важнее, но это не очень важно.

Некоторые интересные моменты:

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

Прогнозирование выдающихся игроков на следующий сезон (2023–2024 г.)

Теперь самое интересное. Я хочу попытаться предсказать прорывных игроков следующего года (2023–2024). В конце сезона я надеюсь обновить эту статью и посмотреть, насколько хорошо я справился.
**Несколько важных замечаний:

  1. Новички не могут считаться сезонами прорыва и поэтому не будут включены в прогнозы.
  2. До начала сезона еще много времени, и этим летом, вероятно, будут какие-то переезды и обмены. Сейчас сложно сказать, каких игроков собираются разменять, а какие команды сменят тактику и перейдут на танкование.
  3. Пока что единственные ходы были сделаны Washington Wizards, которые явно собирались танковать, поэтому я соответствующим образом изменил функцию «TO_TANKING».

Итак, после получения полных данных за этот год из NBA API, я снова обучил свою лучшую модель, модель XGBoost, на всех имеющихся у меня данных и сделал прогноз на основе новых данных.
Но XGBoost предсказал, что около 150 игроков получат сезон прорыва (примерно из 550 игроков). Как обсуждалось ранее, это может быть хорошо для оценки отзыва, но я хотел представить меньший список, поэтому решил попробовать с LightGBM.

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

Некоторые имена могут быть разумными (например, Паоло Банкеро, Дэвион Митчелл, Джош Кристофер), другие кажутся маловероятными (например, Андре Драммонд, Кхем Берч), третьи веселые (Удонис Хаслем), а многие не очень известны. Насколько хороши эти прогнозы? Думаю, нам придется подождать и посмотреть.