秀,用NBA球员数据学透K-Means聚类
這次我們用 NBA 球員賽季表現聚類來探討下 K-Means 算法,K-Means 是一個清晰明白的無監督學習方法,和 KNN 有很多相似點,例如都有超參數 K,前者是 K 個類別,后者是 K 個鄰居。
聚類算法是不需要標簽的,直接從數據的內在性之中學習最優的分類結果,或者說確定離散標簽類型。K-Means 聚類算法是其中最簡單、最容易理解的。
簡單即高效。我們的目標是學習一個東西,然后把它的思想應用到我們想要探索的場景,以加深對算法的理解。
最優的聚類結果需要滿足兩個假設:
“簇中心點”(cluster center)是屬于該簇的所有的數據點坐標的算術平均值
一個簇的每個點到該簇中心點的距離,比到其他簇中心點的距離短
于是 K-Means 聚類的工作流為
隨機猜測一些簇中心點
將樣本分配至離其最近的簇中心點上去
將簇中心點設置為所有點坐標的平均值
重復 2 和 3 直至收斂
我們會編碼實現 K-Means 算法,并用于 NBA 控衛 2020-21 常規賽季的表現分析中。
我們知道過去剛剛結束的這個賽季得分王是萌神庫里,我愛他。我們這里只關注控衛,一是因為控衛線上星光熠熠,二是數據分析要細分,這本身是一個原則。
我們開始吧!
01
學習原理方法
找到數據
我們可以從https://www.basketball-reference.com找到我們想要的數據,那就是NBA球員在過去一個賽季的統計數據。數據獲取流程如下
看看數據
#?三劍客來一遍 import?pandas?as?pd import?numpy?as?np import?matplotlib.pyplot?as?pltnba?=?pd.read_csv("nba_2020.csv") nba.head()我們發現姓名列有個\,后面跟著不知道是什么的簡稱,可以處理掉。另外字段名全是簡寫,我一個球迷有很多都看不懂,尷尬。整理了下字段含義,我們看一遍,這樣就大概了解了這個數據集有什么了。
Age:年齡(別告訴我你知不知道)
TM(team):球隊
Lg(league):聯盟
Pos(position):位置
PG(Point Guard):組織后衛
GS(Games Started):首發出場次數
MP(Minutes Played Per Game):場均上場時間
FG(Field Goals Per Game):場均命中數
FGA(Field Goal Attempts Per Game):場均投籃數
FG%(Field Goal Percentage):命中率
3P(3-Point Field Goals Per Game):三分球命中率
3PA (3-Point Field Goal Attempts Per Game):場均三分球投籃數
3P% (3-Point Field Goal Percentage):三分球命中率
2P (2-Point Field Goals Per Game):兩分球命中數
2PA (2-Point Field Goal Attempts Per Game):場均兩分投籃數
2P% (2-Point Field Goal Percentage):兩分球命中率
eFG% (Effective Field Goal Percentage):有效命中率
FT (Free Throws Per Game):場均罰球命中數
FTA (Free Throw Attempts Per Game):場均罰球數
FT% (Free Throw Percentage):罰球命中率
ORB (Offensive Rebounds Per Game):場均進攻籃板數
DRB (Defensive Rebounds Per Game):場均防守籃板數
TRB (Total Rebounds Per Game):總籃板數
AST (Assists Per Game):場均助攻
STL (Steals Per Game):場均搶斷
BLK (Blocks Per Game):場均蓋帽
TOV (Turnovers Per Game):場均失誤
PF (Personal Fouls Per Game):場均個人犯規
PTS (Points Per Game):場均得分
處理數據
#?球員名字取\前面的字符 nba['Player']?=?nba['Player'].apply(lambda?x:?x.split('\\')[0]) #?取控衛PG的樣本進行分析 point_guards?=?nba[nba['Pos']?==?'PG'] #?剔除0失誤的控衛數據,0tov意味著場次太少,且0不能被除 point_guards?=?point_guards[point_guards['TOV']?!=?0] #?定義ATR為助攻失誤比,并計算出來 point_guards['ATR']?=?point_guards['AST']?/?point_guards['TOV']處理后的數據變成了下面的樣子,我們剩下了 124 個控衛數據。
判斷一個控衛優不優秀,最重要的兩個指標是場均得分和場均助攻失誤比,也就是PTS和ATR。下面我們就用這兩個關鍵特征對球員聚類。
探索數據
看下這些球員的場次得分與助攻失誤比散點圖,這往往是數據分析和建模的第一步。
#?改善下繪圖風格 import?seaborn?as?sns sns.set()# nba聯盟控衛場次得分與助攻失誤比散點圖 plt.scatter(point_guards['PTS'],?point_guards['ATR'],?c='y') plt.title("Point?Guards") plt.xlabel('Points?Per?Game',?fontsize=13) plt.ylabel('Assist?Turnover?Ratio',?fontsize=13) plt.show()最右邊得分超過 30 的你不用猜,就是這個男人。
確認下數據,場均得分大于 25 的過濾出來看下,果然是庫里,東契奇、歐文、利拉德等人赫然在列。這些超巨得分是高,但看起來助攻失誤比差不多都在平均線上下。這是合理的。其實有點干的越多錯的越多的意思,工作上亦是如此。
看了下助攻失誤比超高或超低的,我都不認識就不說了。
我們聚類吧
從散點圖來看,其實并沒有明顯的簇,但我們可以人為定義任意個簇。我們還是分成 5 類。于是我們開始做聚類的第一步。
STEP1:隨機認定 5 個樣本點作為簇的中心點
num_clusters?=?5 #?從樣本里隨機選5個出來作為5個簇的起始中心點 random_initial_points?=?np.random.choice(point_guards.index,?size=num_clusters) centroids?=?point_guards.loc[random_initial_points] #?畫出散點圖,包括5個隨機選取的聚類中心點 plt.scatter(point_guards['PTS'],?point_guards['ATR'],?c='y') plt.scatter(centroids['PTS'],?centroids['ATR'],?c='red') plt.title("Centroids") plt.xlabel('Points?Per?Game',?fontsize=13) plt.ylabel('Assist?Turnover?Ratio',?fontsize=13) plt.show()隨后就是不斷迭代優化簇中心點的過程,為了方便,我們將中心點的坐標存在一個字典里。
def?centroids_to_dict(centroids):dictionary?=?dict()#?iterating?counter?we?use?to?generate?a?cluster_idcounter?=?0#?iterate?a?pandas?data?frame?row-wise?using?.iterrows()for?index,?row?in?centroids.iterrows():coordinates?=?[row['PTS'],?row['ATR']]dictionary[counter]?=?coordinatescounter?+=?1return?dictionarycentroids_dict?=?centroids_to_dict(centroids)上圖我們看到,隨機選出來的centroids,我們把它存在了一個centroids_dict里面。
STEP2:將樣本分配至離其最近的簇中心點上去
這里涉及兩個計算,一個是距離的度量,一個是最小元素的查找。前者我們采用歐拉距離,后者是選擇排序的精髓。
先定義好這兩個計算函數,如下。
import?mathdef?calculate_distance(centroid,?player_values):root_distance?=?0for?x?in?range(0,?len(centroid)):difference?=?centroid[x]?-?player_values[x]squared_difference?=?difference**2root_distance?+=?squared_differenceeuclid_distance?=?math.sqrt(root_distance)return?euclid_distancedef?assign_to_cluster(row):lowest_distance?=?-1closest_cluster?=?-1for?cluster_id,?centroid?in?centroids_dict.items():df_row?=?[row['PTS'],?row['ATR']]euclidean_distance?=?calculate_distance(centroid,?df_row)if?lowest_distance?==?-1:lowest_distance?=?euclidean_distanceclosest_cluster?=?cluster_idelif?euclidean_distance?<?lowest_distance:lowest_distance?=?euclidean_distanceclosest_cluster?=?cluster_idreturn?closest_cluster執行這兩個函數,我們就完成了 STEP2,我們可視化看下完成后的結果。
point_guards['cluster']?=?point_guards.apply(lambda?row:?assign_to_cluster(row),?axis=1)def?visualize_clusters(df,?num_clusters):colors?=?['b',?'g',?'r',?'c',?'m',?'y',?'k']for?n?in?range(num_clusters):clustered_df?=?df[df['cluster']?==?n]plt.scatter(clustered_df['PTS'],?clustered_df['ATR'],?c=colors[n-1])plt.xlabel('Points?Per?Game',?fontsize=13)plt.ylabel('Assist?Turnover?Ratio',?fontsize=13)plt.show()visualize_clusters(point_guards,?5)以上,5 個類別分別以不同的顏色標識出來了,但顯然這是隨機簇的結果,劃分的 5 類結果并沒有太好。我們還需要 STEP3。
STEP3:將簇中心點設置為所有點坐標的平均值
def?recalculate_centroids(df):new_centroids_dict?=?dict()for?cluster_id?in?range(0,?num_clusters):values_in_cluster?=?df[df['cluster']?==?cluster_id]#?Calculate?new?centroid?using?mean?of?values?in?the?clusternew_centroid?=?[np.average(values_in_cluster['PTS']),?np.average(values_in_cluster['ATR'])]new_centroids_dict[cluster_id]?=?new_centroidreturn?new_centroids_dictSTEP4:重復 2 和 3
#?多次迭代,試試100輪吧 for?_?in?range(0,100):centroids_dict?=?recalculate_centroids(point_guards)point_guards['cluster']?=?point_guards.apply(lambda?row:?assign_to_cluster(row),?axis=1)visualize_clusters(point_guards,?num_clusters)最終的結果如上,我們看到效果已經得到了很好的優化。高得分的在一個簇,高助攻失誤比的在一個簇。
以上,我們寫了很多代碼。
手寫算法是為了學習和理解。工程上,我們要充分利用工具和資源。
sklearn 庫就包含了我們常用的機器學習算法實現,可以直接用來建模。
from?sklearn.cluster?import?KMeanskmeans?=?KMeans(n_clusters=num_clusters) kmeans.fit(point_guards[['PTS',?'ATR']]) point_guards['cluster']?=?kmeans.labels_visualize_clusters(point_guards,?num_clusters)短短五行代碼就完成了我們從零開始寫的百來行代碼,效果看起來也很合理。值得說明的是,聚類受起始點的影響,可能不會達到全局最優結果。細心的朋友一定看出來了,上面兩個最終聚類結果是有差異的。
02
風控中的應用
如果學習了一項技能,但是不知道怎么用,那就毫無意義。
想想風控中聚類可以用來干什么呢?風控中我們有什么數據,關注什么結果呢?
一個有意思的課題是,用戶手機安裝的 app 能不能區分用戶的風險。答案顯然是肯定的,
除了必要的社交、支付、生活和工具類 app 外,那些差異化的 app 偏好顯然刻畫不同類型的用戶。安裝了很多小貸平臺的用戶,就很可能是一個多頭客戶,在想著辦法擼口子。
于是我們可以用 app 的安裝情況來給用戶聚類,假設采集了用戶在 100 個 app 的安裝情況,就可以對這 100 個 0、1 變量聚類。
聚成兩類后,我們可以采用其中有標簽的用戶進行驗證,如果好壞用戶絕大部分都正確地被劃分開了,那么有理由相信那些沒有標簽的用戶大概率就等于簇的標簽。
我沒有對應的數據可以舉例,但有類似的數據來說明問題。下面是美國國會的數據,party 代表了議員的黨派,R 是共和黨 Republican,D 是民主黨 Democratic,I 是中間黨派。其余數據均為 0、1 變量。這里的 party 就類似風控中的好壞用戶。
import?pandas?as?pd votes?=?pd.read_csv("114_congress.csv") votes.head()于是我們開始無監督聚類。注意以下并未用到 party 這個標簽。
#?用k-means?clustering方法 from?sklearn.cluster?import?KMeanskmeans_model?=?KMeans(n_clusters=2,?random_state=1) senator_distances?=?kmeans_model.fit_transform(votes.iloc[:,?3:])labels?=?kmeans_model.labels_我們需要用標簽去對聚類結果做驗證。
可以看到,分成了 0 和 1 兩個簇中,0 簇中 43 個議員有 41 個都是民主黨,2 是無黨派人士;1 簇中 57 個議員有 54 個都是共和黨,3 個是民主黨。數據總有異常,或者用戶總有一部分比較奇怪,我們不追求 100%準確。
不完美才是人生的最完美。
推薦閱讀
Pandas處理數據太慢,來試試Polars吧!
懶人必備!只需一行代碼,就能導入所有的Python庫
絕!關于pip的15個使用小技巧
介紹10個常用的Python內置函數,99.99%的人都在用!
可能是全網最完整的 Python 操作 Excel庫總結!
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的秀,用NBA球员数据学透K-Means聚类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 硬核图解,再填猛将!
- 下一篇: 再见,Visio!