音ゲーのクリア難度推定なんかデータサイエンスの入門にいいんじゃないかな?とふと思い立ってやることにしました。3記事くらいに分けて公開します。初回はデータ収集のためのWebスクレイピングについて。
やること
- LR2IRのデータをもとに、BMSの譜面のクリア難度を推定します。
- 譜面とプレイヤーの相性が要因として大きいので、Eloレーティングのような1軸評価ではない手法について検討します。
データの準備
LR2IRにはプレー人数の非常に少ない譜面や、アクティブでないアカウントが多く存在します。
そこで、まず集計対象プレイヤーについて「
BMS発狂難易度表の収録曲のうち125曲以上スコア登録済み」という基準を設けます。これは
Stairwayの基準を引用したものです。
そして、「集計対象プレイヤー(現在12000人くらい)のうち、100人以上がスコアをつけている」曲を集計対象曲とします。
import re
from urllib.request import urlopen
from bs4 import BeautifulSoup
player_ids=[]
bs = BeautifulSoup(urlopen("http://stairway.sakura.ne.jp/bms/LunaticRave2/?contents=player&p=1"))
for player_link in bs.find_all("a", {"href" : re.compile("/?contents=player&page=\d")}):
player_id = int(player_link["href"].split("=")[-1])
player_ids.append(player_id)
with open("playerid","w") as f:
f.write(str(player_ids[0]))
for player_id in player_ids[1:]:
f.write(",")
f.write(str(player_id))
全ユーザーに対してプレー履歴の全取得をするとちょっとサーバーに負荷をかけてしまいそうなので、ユーザーリストはStairwayから拝借しました。
aタグのうち、プレーヤーのmypageへのリンクの形になっているものを選び出し、splitすることでplayer idを得ています。
これは単一ページのパースなのですぐできると思います。
次に、譜面ごとにプレー人数を集計します。譜面ID(bmsid)をkey、プレー人数をvalにした辞書で数えていきます。keyがないとき勝手に0を入れてくれるdefaultdict(int)が便利。負荷をかけないようsleepを入れながら、丸一日くらいで取得しました。
このうち、100人以上がプレーした譜面を残します。意外なことに、10000譜面ちょっとありました。
import re
from collections import defaultdict
from urllib.request import urlopen
from bs4 import BeautifulSoup
num_player = defaultdict(int)
for player_id in player_ids:
for i in range(1, 10000):
bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=mylist&playerid={player_id}&sort=recent&filter=7&page={i}"))
findresult=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=ranking&bmsid=\d")})
if not findresult:
break
for bms_link in findresult:
bms_id = int(bms_link["href"].split("=")[-1])
num_player[bms_id]+=1
time.sleep(0.01)
print(f"player{player_id} done")
bms_ids=[]
for k in num_player.keys():
if num_player[k]<min_playcount:
continue:
else
bms_ids.append(k)
with open("bmsid","w") as f:
f.write(str(bms_ids[0]))
for bms_id in bms_ids[1:]:
f.write(",")
f.write(str(bms_id))
最後に、この集計対象譜面について全プレーヤーのスコアを取得し、集計対象ユーザーのものだけ残してファイルに保存します。1譜面1ファイルで書き出しています。
丸二日くらいかかりました。ファイルサイズは合計で770 MBでした。
import re
import time
import os
from collections import defaultdict
from urllib.request import urlopen
from bs4 import BeautifulSoup
with open("playerid","r") as f:
player_ids=set([int(i) for i in f.read().split(",")])
with open("bmsid","r") as f:
bms_ids=set([int(i) for i in f.read().split(",")])
for bms_id in bms_ids:
if os.path.exists(str(bms_id)):
print(f"bmsid {bms_id} ... skipped")
continue
with open(str(bms_id), "w") as f:
print(f"bmsid {bms_id} ... ",end="")
for i in range(1, 10000):
bs=BeautifulSoup(urlopen(f"http://www.dream-pro.info/~lavalse/LR2IR/search.cgi?mode=ranking&page={i}&bmsid={bms_id}"))
find_result=bs.find_all("a", {"href" : re.compile("search.cgi\?mode=mypage&playerid=\d")})
if not find_result:
break
for player_link in find_result:
player_id = int(player_link["href"].split("=")[-1])
if player_id not in player_ids:
continue
playdata = player_link.parent.parent.contents[3:]
f.write(str(player_id))
for item in playdata:
f.write(",")
f.write(item.get_text())
f.write("\n")
time.sleep(0.01)
print("done")
例えばBMS界のAAこと星の器(bmsid=15)に対する出力ファイルの冒頭はこんな感じ。各行で一番左の列はplayer id、残りがプレーデータです。
37188,FULLCOMBO,AAA,4012/4012(100%),2006/2006,0,2006,0,0,0,2,難,乱,KB,LR2
83367,FULLCOMBO,AAA,4011/4012(99.97%),2006/2006,0,2005,1,0,0,0,難,乱,BM,LR2
202,★FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2
105711,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,易,乱,KB,LR2
120831,FULLCOMBO,AAA,4010/4012(99.95%),2006/2006,0,2004,2,0,0,0,難,乱,KB,LR2
160637,FULLCOMBO,AAA,4009/4012(99.92%),2006/2006,0,2003,3,0,0,2,難,乱,BM,LR2
15499,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,2,2002,4,0,0,4,難,乱,BM,LR2
72359,FULLCOMBO,AAA,4008/4012(99.9%),2006/2006,0,2002,4,0,0,0,易,乱,KB,LR2
94774,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
3906,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
36488,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,KB,LR2
2725,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2
4314,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2002,3,0,0,1,難,乱,BM,LR2
95222,FULLCOMBO,AAA,4007/4012(99.87%),2006/2006,0,2001,5,0,0,0,難,乱,BM,LR2
139053,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,1,難,乱,BM,LR2
22923,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,0,難,乱,KB,LR2
4812,★FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,13,難,乱,BM,LR2
51433,FULLCOMBO,AAA,4006/4012(99.85%),2006/2006,0,2000,6,0,0,8,普,乱,BM,LR2
25546,FULLCOMBO,AAA,4005/4012(99.82%),2006/2006,0,2000,5,0,0,4,普,乱,BM,LR2
...
次回はMarix Factorizationという方法を用いて、これらのデータから難度推定を行っていきます。
→すでに同等の成果が公表されていたのでこの件は保留とします。難度の他に推定したい量が思いついたら続けます。