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

K-Means — это неконтролируемый алгоритм машинного обучения, который группирует или кластеризует данные вместе. Он делает это, случайным образом выбирая некоторые точки, назначая им другие точки в зависимости от расстояния, а затем итеративно оптимизируя, пока не найдет лучшие кластеры. Смотрите здесь для гораздо более подробного объяснения[1]. Эти группировки могут дать нам представление об отношениях, о которых мы раньше не знали. С этого начнем:

Часть 1. Сбор данных

Есть несколько способов получить нужные нам данные, но все они так или иначе связаны с достижением конечных точек. Конечная точка — это место, где мы получаем доступ к данным непосредственно с сервера. Вот варианты доступа к данным:

Один из них — сбор данных об игроках из Баскетбольного справочника. Это хороший вариант, если вам нужны данные до сезона 1996–97 (насколько далеко уходит сайт НБА). Сегодня нам ничего не нужно, кроме сезона 2020–21.

Другой вариант — использовать страницу статистики НБА. Если вы перейдете к сортируемой статистике игрока/команды, вы найдете то, что вам нужно. Нажав на вкладки, вы получите доступ ко всем их данным, включая некоторые действительно продвинутые вещи, такие как типы воспроизведения и отслеживание. Благодаря этой расширенной статистике я предпочитаю использовать веб-сайт НБА, но есть и недостаток, заключающийся в том, что иногда веб-сайты блокируют запросы, если вы делаете слишком много запросов. Хорошее исправление, которое я узнал, — добавить 3-секундный сон во все, что вы зацикливаете.

Наконец, вы можете использовать библиотеки Python, созданные другими людьми, чтобы абстрагироваться от необходимости очистки. Я не люблю пользоваться библиотеками, потому что они могут устаревать, а визуальные таблицы на nba.com облегчают поиск того, что я ищу. Тем не менее, вот несколько отличных вариантов:

  • nba_api — это самая обширная API-библиотека для данных nba.com, в ней есть документация по всем конечным точкам. Для получения информации о том, как его использовать, я рекомендую эту статью.
  • py-goldsberry — названа в честь знаменитого статиста Кирка Голдсберри, эта библиотека также использует nba.com, но с точки зрения синтаксиса ее немного легче подобрать, чем nba_api, на мой взгляд.
  • basketball_reference_scraper — как следует из названия, этот API получает статистику из Basketball Reference. Как говорит автор Вишаала Гарта, использование этого имеет то преимущество, что позволяет запрашивать большие объемы данных без блокировки запросов.

Сегодня мы будем вручную собирать информацию с nba.com, так как это мой метод выбора.

Во-первых, вам нужно найти нужные данные. Players — General — Traditional — это страница для простых данных, таких как PTS, FG%, 3PA, REB, STL и т. д. Нам нужны эти базовые данные для нашего классификатора позиций, но я использую сезон 2020–21. Теперь не так просто скопировать ссылку; вам нужно немного покопаться. Сначала это может показаться запутанным, но на самом деле это не так, как только вы это поймете.

Откройте Инструменты разработчика (ctrl-shift-I в Chrome и Windows), перейдите на вкладку «Сеть» и обновите страницу. Вы увидите кучу сложных ссылок на вкладке «Имя», но то, что мы ищем, начинается с «leaguedashplayerstats» (это будет сказано для каждой категории статистики). Сортировка по имени упрощает поиск, но убедитесь, что вы выбрали вариант с типом «xhr», если видите два. Скопируйте URL-адрес запроса.

Это то, на что вы будете отправлять запрос GET — обязательно установите и импортируйте пакет requests, чтобы получить данные. Мы также собираемся использовать панд, так что импортируйте их, пока вы это делаете. При каждом запросе с nba.com включайте эти заголовки, которые позволяют выполнить запрос.

import requests
headers  = {
    'Connection': 'keep-alive',
    'Accept': 'application/json, text/plain, */*',
    'x-nba-stats-token': 'true',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6)       AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
    'x-nba-stats-origin': 'stats',
    'Sec-Fetch-Site': 'same-origin',
    'Sec-Fetch-Mode': 'cors',
    'Referer': 'https://stats.nba.com/',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'en-US,en;q=0.9',
}
basic_url = 'https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2020-21&SeasonSegment=&SeasonType=Regular+Season&ShotClockRange=&StarterBench=&TeamID=0&TwoWay=0&VsConference=&VsDivision=&Weight='

Теперь мы делаем запрос и используем .json() для преобразования информации объект ответа на запрос в данные json[2]:

json_basic = requests.get(basic_url,headers=headers).json()

Если вы просмотрите данные json, вы заметите, что все, что нам нужно (категории статистики и сами данные), находится в разделе ["resultSets"][0]. Функции (например, PLAYER_NAME, FG%) поступают из ["заголовков"] и данных из ["rowSet"]. Мы должны преобразовать данные в DataFrame pandas, чтобы мы могли лучше манипулировать ими.

features = json_basic['resultSets'][0]['headers']
data = pd.DataFrame(json_basic['resultSets'][0]['rowSet'])
data.columns = features

Обратите внимание, что я устанавливаю столбцы данных = функции, это приводит столбцы в соответствие с их фактическим именем, а не в порядке [0,1,2,3,…]

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

Мы собираемся оставить это на потом, чтобы немного упростить ситуацию. На данный момент, я думаю, нам нужно больше данных. Хотя эта базовая статистика о чем-то говорит нам, ей не хватает сложности, которая помогла бы отличить одних игроков от других. % подборов на самом деле дает нам больше информации о способности игрока к подборам, чем чистые доски (Thinking Basketball очень хорошо объясняет это здесь). Чтобы получить все, что мы хотим, давайте получим некоторые дополнительные данные, используя ту же технику парсинга в Players — General — Advanced. Наконец, я хочу провести различие между скорерами внутри и шутерами с периметра. Players — Shot Dashboard — General дает проценты в зависимости от расстояния. В целом это дает нам следующий код (я завернул процесс очистки в функцию, потому что нам нужно повторять его несколько раз):

import requests
headers  = {
    'Connection': 'keep-alive',
    'Accept': 'application/json, text/plain, */*',
    'x-nba-stats-token': 'true',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6)  AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130  Safari/537.36',
    'x-nba-stats-origin': 'stats',
    'Sec-Fetch-Site': 'same-origin',
    'Sec-Fetch-Mode': 'cors',
    'Referer': 'https://stats.nba.com/',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'en-US,en;q=0.9',
}
basic_url = 'https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2020-21&SeasonSegment=&SeasonType=Regular+Season&ShotClockRange=&StarterBench=&TeamID=0&TwoWay=0&VsConference=&VsDivision=&Weight='
advanced_url = 'https://stats.nba.com/stats/leaguedashplayerstats?College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&GameScope=&GameSegment=&Height=&LastNGames=0&LeagueID=00&Location=&MeasureType=Advanced&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2020-21&SeasonSegment=&SeasonType=Regular+Season&ShotClockRange=&StarterBench=&TeamID=0&TwoWay=0&VsConference=&VsDivision=&Weight='
shotdash_url = 'https://stats.nba.com/stats/leaguedashplayerptshot?CloseDefDistRange=&College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&DribbleRange=&GameScope=&GameSegment=&GeneralRange=Overall&Height=&LastNGames=0&LeagueID=00&Location=&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2020-21&SeasonSegment=&SeasonType=Regular+Season&ShotClockRange=&ShotDistRange=&StarterBench=&TeamID=0&TouchTimeRange=&VsConference=&VsDivision=&Weight='
def get_data(url):
    json = requests.get(url,headers=headers).json()
    features = json['resultSets'][0]['headers']
    data = pd.DataFrame(json['resultSets'][0]['rowSet'])
    data.columns = features
    
    return data
data = get_data(basic_url)
advanced_data = get_data(advanced_url)
shotdash_data = get_data(shotdash_url)
data_shotdash = data_shotdash[['PLAYER_NAME','FG2A_FREQUENCY', 'FG2M', 'FG2A', 'FG3A_FREQUENCY']]
data_adv = data_adv[['PLAYER_NAME','USG_PCT','OFF_RATING','DEF_RATING','AST_PCT','AST_TO','AST_RATIO','OREB_PCT','REB_PCT','E_TOV_PCT','EFG_PCT','TS_PCT',]]

Как видите, я каждый раз сохраняю «PLAYER_NAME». Мы хотим объединить эти данные в один DataFrame, но должен быть какой-то общий идентификатор. Имена хорошо работают. Вот как их комбинировать:

data = data.merge(data_adv,on='PLAYER_NAME')
data = data.merge(data_shotdash,on='PLAYER_NAME')

«Данные» имеют 81 функцию, поэтому давайте сократим их до самого необходимого. Сразу же мы можем удалить все, что не основано на статистике, например, имена игроков, названия команд, возраст или статистику, связанную с фэнтези. Мы также хотим удалить игроков, сыгравших менее 20 игр и прошедших 10 минут, поскольку все игроки с малым размером выборки «искажаются». Есть также определенные статистические данные, которые нам не нужны. Например, выигрыши не должны влиять на то, к какой группе принадлежит игрок. Я покажу, что я вынул ниже, но вы должны подумать, что поможет модели работать лучше. Я также собираюсь хранить имена в DataFrame, потому что они понадобятся нам позже.

#This is a quick way of eliminating all the rank-based stats that we don't want in our model.
data.drop(data.filter(regex='RANK').columns, axis=1, inplace=True)
data = data[(data['GP'] > 20) & (data['MIN'] > 10)]
names = data['PLAYER_NAME']
data.drop(['PLAYER_NAME','W_PCT','PLAYER_ID','PFD','NICKNAME','TEAM_ID','TEAM_ABBREVIATION','AGE','GP','W','L','MIN','NBA_FANTASY_PTS','DD2','TD3','CFID','CFPARAMS', 'PLUS_MINUS','DREB'], axis=1, inplace=True)

Последний шаг части данных: масштабирование. Вы захотите импортировать StandardScaler из sklearn.preprocessing. Это стандартизируется путем преобразования среднего значения в 0 и дисперсии в 1. Стандартизация является необходимым шагом перед запуском PCA.

data = StandardScaler().fit_transform(data)

Со всем этим давайте перейдем к хорошей части.

Часть 2: Снижение размерности с помощью PCA

Анализ основных компонентов (PCA) — это метод визуализации данных и уменьшения размерности, который использует собственные значения и некоторую матричную алгебру для вычисления ортогональных компонентов. Ортогональность означает, что эти векторы некоррелированы, поэтому мы получаем максимум информации из наименьшего количества компонентов. Эти компоненты будут упорядочены по объясненной дисперсии, поэтому первый компонент будет «содержать больше всего информации». Если это не имеет смысла, просто знайте, что PCA — это способ сжатия наших данных в сжатой форме.

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

Во-первых, мы хотим выяснить, сколько измерений нужно уменьшить. Существует компромисс между получением данных более низкого измерения и сохранением информации. Объясненная дисперсия — хороший способ измерить, сколько информации мы сохранили от оригинала до нашего набора данных PCA. Если вы когда-либо проходили курс статистики, он действительно похож на R².

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

from sklearn.decomposition import PCA
variances = []
for n_components in range(2,25):
    pca = PCA(n_components = n_components)
    components = pca.fit_transform(data)
    print('Components: {} Variance: {}'.format(n_components,         sum(pca.explained_variance_ratio_)))
    variances.append(sum(pca.explained_variance_ratio_))

Здесь мы запускаем fit_transform с нашей моделью PCA для каждого n_компонентов от 2 до 25. Затем мы суммируем объясненное_отношение_дисперсии_, чтобы получить желаемое число, поскольку это объясненная дисперсия для каждого отдельного компонента.

Вот что я получил.

А график…

Он приближается к горизонтальной асимптоте, поэтому в какой-то момент мы действительно теряем выгоду. Я возьму 16 компонентов.

pca = PCA(n_components=16)
components = pca.fit_transform(data)

Часть 3: Запуск K-средних

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

Силуэт — это метрика для оценки производительности кластеризации. Он использует как внутрикластерные расстояния (между точками в кластере), так и межкластерные расстояния (между кластерами). 1 означает идеальный кластер, в то время как все, что ближе к 0, показывает все большее и большее перекрытие. -1 означает, что у вас есть ошибка с образцами в кластере, но это не должно быть проблемой. Инерция — это еще одно измерение, в котором используется расстояние от точек до их центра масс.

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

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
silhouettes = []
sse = []
for n_clusters in range(2,20):
    
    kmeans = KMeans(n_clusters=n_clusters,random_state=84)
    
    cluster_labels = kmeans.fit_predict(components)
    
    silhouette = silhouette_score(components, cluster_labels)
    silhouettes.append(silhouette)
    sse.append(kmeans.inertia_)
    print("n_clusters = {}, silhouette score: {} - sum of squared  distance: {}".format(n_clusters, silhouette, kmeans.inertia_))

Исход:

Графики:

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

Мы сейчас на финишной прямой. Поскольку мы знаем, что 11 — это хорошее число, все, что нам нужно сделать, это снова запустить K-Means.

kmeans = KMeans(n_clusters=11,random_state=42)
kmeans.fit(components)
km = kmeans.predict(components)

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

Часть 4: Кластерный анализ

Я хочу организовать это в аккуратный DataFrame и поместить кластерные группы вместе с игроками. Это сделает все намного проще для просмотра.

final = pd.DataFrame()
final['PLAYER_NAME'] = names
final['Cluster'] = km

Это должно выглядеть примерно так:

Классный способ визуализации алгоритма кластеризации — графически изобразить кластеры вдоль первых двух компонентов PCA — вот так:

#Getting unique labels
label = km
u_labels = np.unique(label)
centroids = kmeans.cluster_centers_
#plotting the results:
 
for i in u_labels:
    plt.scatter(components[label == i , 0] , components[label == i , 1] , label = i)
    
plt.scatter(centroids[:,0] , centroids[:,1] , s = 40, color = 'k')
plt.legend()
plt.show()

Хм, выглядит не очень…

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

Стили игры более изменчивы, чем когда-либо, поэтому многие игроки находятся где-то посередине. Кластеры 3, 8 и 4 действительно выделяются.

Кластер 0 — Квалифицированные крупные компании:

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

PTS: +1.6219015701607393 from average
USG_PCT: +0.013398622230497176 from average
E_TOV_PCT: -0.20089368832619492 from average
FG3A_FREQUENCY: -0.06461211444175508 from average
OFF_RATING: -0.6336684664556458 from average
DEF_RATING: +0.7761310742878322 from average

Кластер 1 — Наступательная ответственность:

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

PTS: -5.726617593602327 from average
OFF_RATING: -4.706752090149038 from average
DEF_RATING: +0.783056161395848 from average
TS_PCT: -0.05806302253725937 from average

Yikes, посмотрите на эту эффективность.

Группа 2 — Трехочковые:

Совершенно очевидно, на что мы смотрим: специалисты по трем очкам. Джо Харрис, Джо Инглз, Джей Джей Редик. Эти игроки должны иметь более высокую частоту трехочковых бросков по сравнению с другими игроками, а также более высокую эффективность. Глядя на объем, можно ввести в заблуждение, потому что звездные стражи легко выбьют этих парней.

FG3A_FREQUENCY:+0.22087321606372384 from average
FG3_PCT: +0.06679156986392298 from average

Кластер 3 — классические крупные компании:

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

EFG_PCT: +0.07113903307888048 from average
OREB_PCT: +0.07840447837150125 from average
FG3A_FREQUENCY: -0.3746004071246819 from average

Обратите внимание на трехочковые справа вверху клавиши — классика.

Кластер 4 — крупные звезды:

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

Примечание: не странно ли, что Йокич здесь? Я думал, что он будет более «сторожевым».

AST_PCT: +0.12610839694656487 from average
OFF_RATING: +3.3401526717557317 from average
USG_PCT: +0.09076284987277355 from average
EFG_PCT: +0.00812569974554711 from average
OREB_PCT: +0.0172178117048346 from average
REB_PCT: +0.04435674300254451 from average
FGA: +7.756895674300255 from average

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

Кластер 5 — Плохие крупные компании:

Эти игроки похожи на классических больших игроков по своему набору навыков; они хуже.

EFG_PCT: +0.06151617593602343 from average
OREB_PCT: +0.0518630498000727 from average
FG3A_FREQUENCY: -0.33127326426753906 from average

Здесь все выглядит хорошо, но они расходятся с кластером 3 по объему.

PTS: -5.015903307888041 from average
REB: +0.1503089785532543 from average
OFF_RATING: -1.896037804434755 from average
DEF_RATING: -1.5258724100327186 from average

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

Кластер 6 — Эффективные плеймейкеры:

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

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

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

Cluster: 0 - ASSIST TO USAGE PCT: 0.4795830778663396
Cluster: 1 - ASSIST TO USAGE PCT: 0.6552994296577946
Cluster: 2 - ASSIST TO USAGE PCT: 0.6000000000000001
Cluster: 3 - ASSIST TO USAGE PCT: 0.5561824165491301
Cluster: 4 - ASSIST TO USAGE PCT: 0.9732142857142857
Cluster: 5 - ASSIST TO USAGE PCT: 0.5202834799608993
Cluster: 6 - ASSIST TO USAGE PCT: 1.3574868651488619
Cluster: 7 - ASSIST TO USAGE PCT: 1.029279568511622
Cluster: 8 - ASSIST TO USAGE PCT: 0.9058823529411765
Cluster: 9 - ASSIST TO USAGE PCT: 0.6472417251755265
Cluster: 10 - ASSIST TO USAGE PCT: 1.140148392415499

6 явно выделяется.

Кластер 7 — Volume Guards:

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

PTS: +6.82159669211196 from average
AST_PCT: +0.10771048027989824 from average
DEF_RATING: +0.6201097328244174 from average
USG_PCT: +0.05783993320610689 from average
TS_PCT: -0.014888915394402069 from average
FGA: +6.040020674300253 from average

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

Кластер 8 — Звездные бомбардиры:

В этом скоплении отчетливо видны звездные стражи. Я ожидаю высокой производительности в атаке с большим объемом.

PTS: +15.053144311159578 from average
AST: +3.443475099963651 from average
OFF_RATING: +5.5230098146128626 from average
USG_PCT: +0.1180676117775355 from average
TS_PCT: +0.03465721555797874 from average
FGA: +10.294038531443112 from average

Группа 9 — Трехочковые нападающие:

Интересно, что K-Means смог уловить различия между тремя разыгрывающими защитниками и нападающими, поскольку статистические различия незначительны.

3PT Форвард:

PTS: +2.4590966921119612 from average
FGA: +2.3660623409669217 from average
AST_PCT: -0.00832076972010179 from average
FG3A_FREQUENCY: +0.10482792620865139 from average
FG3A: +2.1830629770992362 from average
FG3_PCT: +0.04487309160305347 from average
USG_PCT: +0.02220451653944025 from average
EFG_PCT: -0.01050763358778617 from average

Охранники 3PT:

PTS: -2.8659033078880416 from average
FGA: -2.3254231662794567 from average
AST_PCT: -0.05328000885053655 from average
FG3A_FREQUENCY: +0.22087321606372384 from average
FG3A: +0.5690231220267727 from average
FG3_PCT: +0.06679156986392298 from average
USG_PCT: -0.0363733820112844 from average
EFG_PCT: +0.033630047571634125 from average

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

Кластер 10 — Низкий объем… Низкая эффективность… Хорошая защита???:

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

PTS: -5.349774275629977 from average
OFF_RATING: -2.943503242222775 from average
DEF_RATING: -2.7031765574981534 from average
TS_PCT: -0.06265000410407962 from average

Наблюдения:

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

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

Общие выводы:

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

Будущие идеи:

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

Использованная литература:

[1] Дж. Стармер, StatQuest: кластеризация K-средних (2018), https://www.youtube.com/watch?v=4b5d3muPQmA

[2] W3 School, запросы Python. Объект ответа, https://www.youtube.com/watch?v=4b5d3muPQmA