通過聚類分析吃雞亞洲,北美,歐洲前百名玩家的行為
代碼地址
應評論區知友要求,我已將整個項目的代碼與資源文件都放入了GayHub,附上地址:
monlie/cluster_for_pubg
廢話寫在前面
近日來被學生會要求給17級的學弟學妹講一講python語言。在前言部分我準備了一些例子,以簡單展示python的功能,標題所述的程序即是其中之一。
程序的整體思路很簡單,分三步走:
- 爬取數據:爬取https://pubg.me/上的各伺服器前百名數據。
- 分析:分析爬取到數據的內容,選擇合適的特徵參數。
- 聚類:通過選取的特徵參數對樣本點集做kmeans聚類,獲取各樣本點的label,最後按不同的label畫圖。
最後結果是這樣:
下面詳細說說各階段具體的實現。
爬取數據
這部分唯一值得注意的是,PUBG.ME在你第一次訪問的時候,會丟給你一些cookie:
cookie:__cfduid=d44db92eab1474bde64206da8c5e0f5b11507046474; language=en-US; cdmu=1508154346400; cdmblk2=0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0; cdmblk=0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0,0:0:0:0:0:0:0:0:0:0:0:0:0:0; cdmtlk=0:0:0:0:0:0:0:0:0:0:0:0:0:0; cdmgeo=us; cdmbaserate=2.1; cdmbaseraterow=1.1; cdmint=0
並且伺服器會在你接下來每次訪問的時候驗證它們,已確定你是真的瀏覽器(然而並不可以)。其中,__cfduid的值會每隔一段時間(大概10min?)更新一次。
在寫爬蟲的時候,如果你的請求里不包含或者包含了過期的cookie,伺服器會丟給你一個寫著稍等5秒鐘blabla的網頁,當然這樣就什麼都爬不到了(攤手┑( ̄Д  ̄)┍
分析
爬好的數據可以按行排列成一個300*37的二維數組,其中每一個樣本點包含37個attr,如下圖:
這些attr分別是:
["as solo Rating" "Season High" "Rank" "Win Rate" "Wins" "Win Rank" "Kill / Death Ratio" "Kills" "Kill Rank" "Matches Played" "Daily Logins" "AVG Distance Travelled" "Total Distance Travelled" "AVG Distance On Foot" "AVG Distance In Vehicle" "Weapons Acquired" "Vehicles Destroyed" "Total Damage Dealt" "Top 10 Rate" "Top 10"s" "AVG Survival Time" "Longest Time Survived" "Total Heals" "Total Boosts" "Team Kills" "Suicides" "Revives" "AVG Damage Per Match" "Most Kills In Match" "Assists" "KDA" "Headshots" "Headshot Kill Ratio" "Longest Kill" "Road Kills" "Best Kill Streak" "DBNO"]
可以合理地認為,只有平均數據才有對比的價值。再看看平均數據里,有例如 AVG Distance On Foot,AVG Distance In Vehicle,AVG Survival Time一類描述玩家除槍法之外屬性的數據(或許 AVG Survival Time跟槍法有相關性?)。這些數據受玩家的風格影響,並不能很好的刻畫玩家水平的優劣和是否使用外掛,故他們也不是我們要找的特徵參數。
考慮到可視化的直觀與方便,不妨從這37個數據里挑出3個作為特徵參數。最後我選擇了 Win Rate,KDA,Headshot Kill Ratio,原因是:
- Win Rate是最能體現玩家水平的數據,直截了當。
- KDA和Headshot Kill Ratio與玩家的槍法相關性很強,外掛玩家(指自瞄)與正常玩家的這兩項數據一般也有較大差異。
聚類
考慮到前100的正常玩家水平不會有太大差異,可以合理的估計,對300個樣本點的聚類事實上是分開了正常玩家以及外掛玩家。
先來看一看樣本點的分布:
可以看到,左下角一片區域明顯有一個密度較大的點簇,可以認為正常玩家都在這個點簇里(你們看看其他點數據多麼不正常,尤其右上角,85%吃雞率,177KDA!!! 177!!!)
我們期望選擇一個合適的聚類演算法,把正常玩家和外掛規到不同的類里。由於正常玩家集和掛逼集的密度(目測)相差很大,所以像DBSCAN這樣基於密度的聚類演算法肯定無法得到良好的結果。於是我們選擇基於距離的聚類演算法中最簡單的kmeans。
先試探性聚為3類(因為有右上角那個離群點,聚2類沒什麼屁用...):
from sklearn.cluster import KMeansfrom get_data import open_exlm = open_exl("pubg_3.xls", 0)kmeans = KMeans(n_clusters=3).fit(m)labels = kmeans.labels_
然後分成不同的顏色畫出來:
from mpl_toolkits.mplot3d import Axes3Dimport matplotlib.pyplot as pltcolors = ["#E4846C", "#19548E", "#E44B4E", "#197D7F", "#0282C9"]c_list = [colors[labels[i]] for i in range(m.shape[0])]plt.figure(figsize=(12, 7))ax1 = plt.subplot(111,projection="3d")x,y,z = m[:,0],m[:,1],m[:,2]ax1.scatter(x, y, z, s=15, color=c_list)ax1.set_title("Data of PUBG without Labels")ax1.set_zlabel("Headshot Kill Ratio")ax1.set_ylabel("KDA")ax1.set_xlabel("Win Rate")plt.show()
如圖,橙色點可以認為是正常玩家;紅色槍法好過正常玩家,但是勝率卻與正常玩家無異;而藍色點槍法和勝率都明顯好過正常玩家(尤其右上角那個點...他id好像是加某某群買掛什麼的...)
更進一步的,正常玩家由於數據分布過於密集,不太容易區分具體的打法風格,但是外掛玩家的行為可以看到相差甚遠。不妨先來估計一下外掛的行為:
- Aimbot:自瞄,還可以根據KDA跟爆頭率的對比區分出是暴力鎖頭還是僅僅是自瞄(懶得寫)
- Waller:透視,坊間傳言有些主播不開自瞄但是會用雙屏,其中一個屏正常另一個透視。
- Both:顧名思義既透視又自瞄。
- Motherf**ker:專指右上角那個點,我是真的不知道怎麼形容。
現在我們將樣本點集聚為5類,看一看kmeans演算法是不是如我分析的一樣,能將整個點集分為正常玩家與上述四類:
from sklearn.cluster import KMeansfrom get_data import open_exlm = open_exl("pubg_3.xls", 0)kmeans = KMeans(n_clusters=5).fit(m)labels = kmeans.labels_
畫圖:
如上圖,橙色當然是正常玩家,紅色則可以認為是自瞄,天藍是透視,深藍是透視+自瞄,右上角綠色...emmm...是死媽東西。
加上標籤即可得到序言里的圖:
from mpl_toolkits.mplot3d import Axes3Dimport matplotlib.pyplot as pltcolors = ["#E4846C", "#19548E", "#E44B4E", "#197D7F", "#0282C9"]label_list = ["Normal", "Both", "Waller", "Aimbot", "MotherF**ker"]plt.figure(figsize=(19, 7))ax = plt.subplot(122,projection="3d")for i in range(5): c = m[labels == i] x,y,z = c[:,0],c[:,1],c[:,2] ax.scatter(x, y, z, s=15, color=colors[i], label=label_list[i])ax.legend()ax.set_title("Data of PUBG with Labels Built by KMeans")ax.set_zlabel("Headshot Kill Ratio")ax.set_ylabel("KDA")ax.set_xlabel("Win Rate")ax1 = plt.subplot(121,projection="3d")x,y,z = m[:,0],m[:,1],m[:,2]ax1.scatter(x, y, z, s=15)ax1.set_title("Data of PUBG without Labels")ax1.set_zlabel("Headshot Kill Ratio")ax1.set_ylabel("KDA")ax1.set_xlabel("Win Rate")plt.show()
2017/10/17 更新
後續分析
看到評論區有部分知友提出了一些建議,例如想看判別,質疑小樣本容量下聚多類會不會效果不佳,還有說建議考慮avg dis on foot以甄別瞬移掛,也有建議爬100名以後的數據。我覺得這些建議都很coooool(Jeff:???偷我台詞?),所以在這個部分我會把建議的內容挑一些寫出來。
由於我在分析的部分只是武斷地選擇了3個attr,沒有用特徵工程的方法對原數據進行仔細的預處理,所以在這一部分,我將會回到原始數據,分析它們各自的相關性與分布。最後將會結合它們來評估聚類結果。
為了數據集更具有統計價值,我們擴大樣本容量,考慮亞洲,歐洲,北美,大洋洲,東南亞合計499個樣本點(大洋洲有一條垃圾數據清洗掉了):
import pandas as pdimport numpy as npfrom get_data import open_exlasia = open_exl("pubg_as.xls", 0)eu = open_exl("pubg_eu.xls", 0)na = open_exl("pubg_na.xls", 0)oc = open_exl("pubg_oc.xls", 0)sea = open_exl("pubg_sea.xls", 0)data = np.vstack((asia, eu[1: ], na[1: ], oc[1: ], sea[1: ]))df = pd.DataFrame({data[0, i]:data[1:, i] for i in range(data.shape[1])})df
在全部的37個attr中,刻畫玩家某項平均數據的attr即以上7個。應知友 @燃之煤 的建議,我們先來看看avg dis on foot的分布:
from pubg import asfloatavg_dis_on_foot = asfloat(data[1:, -2:])fig = plt.figure(figsize=(7,7))max = avg_dis_on_foot[:, 0].max()plt.hist(avg_dis_on_foot[:, 0], bins=100, color="steelblue")plt.xlabel("AVG Distance On Foot")plt.ylabel("Ratio")plt.title("$max=%.2fkm$"% max)plt.show()
?????大佬這一手秀得我頭皮發麻,地圖一共是10km*10km,遊戲內玩家不嗑藥空手行進速度大概是27km/h。一局撐死40分鐘,就算一局一直跑,還活到最後,滿打滿算最多能跑18km(還要不停兜圈子)。這老鐵居然平均能跑15.59km???平均啊!!!這是神仙騰雲駕霧的節奏。
我們可以看到,正常玩家(未必正常,我感覺跑不到1km的人很可疑)跑路的分布還是比較符合預期的,大概是均值在2附近的正態分布(看圖,像不像二項分布,像不像啊?),有心人可以做個回歸看看,因為和文章主題關係不大,這裡就不畫了。
回來算一算吃雞率(我覺得這是唯一反應玩家能力的數據)與各項數據的相關性,先去上課orz
2017/10/19更新
orz拖了兩天,本來說中午一過就更,微分幾何老師找人帶話給我說再不去上課就沒成績了嚶嚶嚶~
射擊類遊戲,老生常談的東西就是意識和槍法,吃雞當然也不例外。我們先來看看槍法的影響:
前面說過,反應玩家水平的指標就是吃雞率win rate,槍法可以看成是由kd和爆頭率headshot kill ratio共同表徵,不妨來看看相關性(換了seaborn庫,好不好看好不好看?):
光從相關係數上看,好像槍法和吃雞率沒有很高的線性相關性,是簡單的正相關。
果真是這樣嗎?我們去掉由外掛玩家造成的雜訊,只考慮聚類後密度最大的一類,再來看一看:
import matplotlib.pyplot as pltimport seaborn as sns#m = m[labels==3]df = pd.DataFrame({data[0, i]:m[:, i] for i in range(3)})# 繪圖sns.JointGrid("KDA","Win Rate",df).plot(sns.regplot, sns.distplot)plt.xlim((0, None))plt.ylim((0, 100))plt.show()
import matplotlib.pyplot as pltimport seaborn as sns#m = m[labels==3]df = pd.DataFrame({data[0, i]:m[:, i] for i in range(3)})# 繪圖sns.JointGrid("Headshot Kill Ratio","Win Rate",df).plot(sns.regplot, sns.distplot)plt.xlim((0, None))plt.ylim((0, None))plt.show()
可以看到去噪後kd和吃雞率的相關係數有一定的提升,但是爆頭率和吃雞率的相關係數驟降到0,既可以認為毫無關係。
這個結果怎麼理解呢?當然不是說明爆頭不重要。看爆頭率分布圖,峰在20%左右,而且方差很小。這也就是說高分段正常玩家的爆頭率都很接近(不橫向對比的話,都不算高),向scream那樣有爆頭癖好的人,在吃雞高分段是不常見啦~
然後說說意識:
想用寥寥幾個數據刻畫意識的好壞,本身就是一件不切實際的事情。我們只好先武斷地拿生存時間來刻畫意識,來生存時間和吃雞率的相關性:
從相關係數上看,關係是有,但不顯著。或者這也是數據缺乏區分度的關係,並不是說意識在這遊戲里不重要。看存活時間分布圖,方差也比較小,大家平均都能活進決賽圈啊...
我也分析不下去了...歡迎評論區給出寶貴意見...
要說難點的話,一是樣本容量實在太小了(我只能爬到前100名的數據啊),二是或許吃雞真的是運氣占很大的部分吧...
推薦閱讀:
※pyecharts 更新至 v0.2.6 版本啦
※【scikit-learn文檔解析】集成方法 Ensemble Methods(上):Bagging與隨機森林
※我的詞典我做主!python3.5生成自己的詞性詞典
TAG:Python | 聚类分析 | 绝地求生:大逃杀PUBG |