【IIDX】曲データの統計分析を試す【2: IRT編】
前回は曲データの収集を行った。
今回は早速得られたデータの難易度推定を行っていきたいと思う。
といっても、この記事はその準備が大半なのでせっかちな人は適当に読み飛ばしてどうぞ。
次: 【IIDX】曲データの統計分析を試す【3: 難易度表編】 - analytics-arkのブログ
introduction
IRT
今回使用する手法は項目応答理論(IRT)と呼ばれるものである。
よく知られた例だとTOEFLなどの試験に使われている手法である。
BMSの難易度推定(リコメンド)もこれが使われている。というかそれの真似です。
こちらの記載やWikipediaなどが詳しい*1。
リンクだけ貼るのも味気ないので自分なりの理解をここに書いておく。
一般的な試験におけるIRT
一般的な試験で例えよう。
試験にはいろいろな難易度の問題がある。100問とか200問とかあるわけである。
この時、以下の2つを同時に考えることができる。
優秀な人間
大体の問題を正解できる人間。9割の得点ができる人というのは言うまでもなく5割得点の人より優秀である。
難易度の高い問題
上記の優秀な人間でも正解できない、あるいは正答率が低い問題というのは難しい問題ということになる。
この2つは相補的である。
つまり【難しい問題を正解できれば→優秀な人間】ともいえるし、
【優秀な人間でも正解できなければ→難しい問題】ともいえる。
さて、こういう状況のときに得点をどうやって決めるか、という問題が発生する。
一律同じ配点の場合、難しい問題を解けた恩恵というのは全くないことになる。
これでは「優秀な人間」には面白くないだろう。
やはり難しい問題を解けた場合はその人間の評価も高めてほしいものである。
こういう問題を解決するのがIRTである。
殆どの人が正解できる問題の配点は下げ、難しい問題の配点を上げることで個人個人の評価をより正確に行うことができる。
そして、その副産物として「問題の難易度」も導かれる(先述したように、この2つは相補的であるため)。
また、実際には更にもう一個「個人差の激しい問題」というカテゴリーも追加される。
こういった状況についてもパラメータを追加することでうまく記述できるのがIRTの利点である。
欠点としては、直感的ではないのと計算が面倒なことが挙げられる。
method
データ整理
ID割り振り
前回取得したデータには曲IDとユーザーIDが振られていなかったので、これを指定しておく。
まずは曲ID。
曲データを格納するsongsテーブルを作成する。
SET @id := 0 CREATE TABLE songs AS SELECT @id := @id + 1 AS id, tmp.songName, tmp.songDifficultyId, tmp.playLevel FROM ( SELECT songName, songDifficultyId, playLevel FROM playstates GROUP BY songName, songDifficultyId, playLevel ) AS tmp
次はユーザーID。これはAUTO_INCREMENTなID列を新しく作るだけである。
SQLは省略。
CSV作成
解析はpython上で行うが、事前処理はできる限りSQL上で終わらせておきたい。
ので、データを入れるのに都合の良いCSVを予め作成しておく。
具体的には、ユーザーID、曲ID、クリア状況*5(easy, clear, hard, exh, fc)のboolean値を表にする。
こんな感じのSQLを発行した。
CREATE TABLE clearlist AS SELECT users.id AS playerId, songs.id AS songId, IF(playstates.clearStateId >= 3, 1, 0) AS easyCleared, IF(playstates.clearStateId >= 4, 1, 0) AS normalCleared, IF(playstates.clearStateId >= 5, 1, 0) AS hardCleared, IF(playstates.clearStateId >= 6, 1, 0) AS exhCleared, IF(playstates.clearStateId >= 7, 1, 0) AS fullComboed FROM `playstates` LEFT JOIN users ON users.iidxId = playstates.iidxId LEFT JOIN songs ON playstates.songName = songs.songName AND playstates.songDifficultyId = songs.songDifficultyId WHERE 1
今回の注意点として、NO-PLAYの場合は標本に含めていない。
これによって「やってないだけ」のケースを防げる反面、
実際よりも難易度が低めに見積もられる(特にEASY)ことに留意が必要。
これを実行するとこんな感じのtableが出来上がる。
後はこのテーブルをCSV出力する。phpmyadminのcsv出力を使用した。
この際、【1 行目にカラム名を追加する】にチェックを入れておくこと。
Google Driveにアップロード
先程出力したCSVをGoogle Drive上にアップロードする。
後述するColaboratoryで使用するために必要なので、適当にディレクトリを作成して上げておく。
pythonでIRT計算
Google Colaboratoryを使う
解析処理にはそれなりにマシンパワーが必要だが、私はノートPCしか持ち合わせていない。
そこで、実マシン上で処理するのではなくGoogle Colaboratoryを使用する。
colab.research.google.com
使用ライブラリ
【pyirt】
IRT計算を簡易に行うためのpythonライブラリ。
使用するために、colab上で
!pip install pyirt
が必要。インストール後は
from pyirt import irt
で読み込み完了。
【pandas】
CSVを読み込むのに使う。便利。
【matplotlib, numpy】
グラフ描画のためにimport。
データの整形、そして推定
ここまでくれば後は計算するだけである。
pyirtの仕様として「(userId, itemId, ans_boolean)の3要素からなるタプル」のリストを渡す必要がある。
itemIdというのはすなわち曲ID、ans_booleanというのはクリアの有無である。
このために先程clearlistの出力をBOOLEANでやっておいた。
つまり、いらない部分を消すだけでOKである。
実行したコードを以下に示す。これはeasyの場合なので、HARDで取得する場合は一部書き直せばOKである。
# pyirtをインストールしておく !pip install pyirt from pyirt import irt import pandas as pd # Google Driveからcsvを読み込むために必要 from google.colab import drive drive.mount('/content/drive') # 読み込み clearDatas = pd.read_csv("/content/drive/My Drive/py_datas/iidx26_clearlist.csv") # easyの状況のみにしておく easyed = clearDatas[["playerId", "songId", "easyCleared"]] # 推定開始 # .to_numpy()でタプルのリストに変換できる # (theta|alpha|beta)_bndsはθ, a, bの範囲 標準の値だと狭いのでこんな感じで指定した。 ie, ue = irt(easyed.to_numpy(), theta_bnds=[-20, 20], alpha_bnds=[0.00, 10], beta_bnds=[-20, 20])
result
pyirtにおけるalpha, betaの値
結果として、例えばこのような値が得られる。
# 曲ID:1のクリア指数 # ID:1 → (This Is Not) The Angels ih[1] # {'alpha': 0.3756938922019267, 'beta': 1.221954591873332, 'c': 0.0}
alpha, betaの値が出てきたが、そもそもこの値は何を示すのか?
これは、IRTの式に出てくるa, bに対応している。
aは個人差要素である。値が大きいほど地力譜面となる。
bは譜面難易度である。値が大きいほど難易度が高い。
pyirtの出力はbの符号がどうやら逆のようなので、マイナスをつけることで対応する。
幾通りかプロットしたものを下に示そう。横軸がθ(地力要素)である。
import matplotlib as plt import numpy as np abs = [(0.5,4),(0.5,-4),(0.2,4),(0.2,-4)] x = np.linspace(-15, 15, 400) plt.figure(0) for ab in abs: plt.plot(x, 1/(1+np.exp(-1.701 * ab[0] * (x + ab[1]))), label=f"a={ab[0]}, b={ab[1]}" ) plt.legend() plt.show()
aの値が増えると地力度が高くなる(青、オレンジの線)。
bの値が増えると難易度が高くなる(赤、オレンジの線)。
結果の確認
細かい分析は次の記事で行うが、大雑把な結果だけ確認しておこう。
EASY難度TOP10
tabulateを使って見やすく出力してみる。
from tabulate import tabulate headers = ["songName", "Lv", "難度", "地力度"] table = [(x["songName"], x["songLevel"], x["beta"], x["alpha"]) for x in iedats[0:10]] print(tabulate(table, headers, tablefmt="html"))
その結果は
songName | Lv | 難度 | 地力度 |
---|---|---|---|
GO OVER WITH GLARE -ROOTAGE 26- | 12 | 6.87197 | 0.684238 |
Carmina | 12 | 6.58538 | 0.692371 |
X-DEN | 12 | 6.32772 | 0.62095 |
IX | 12 | 6.10528 | 0.804026 |
焔極OVERKILL | 12 | 6.01395 | 0.80108 |
Mare Nectaris | 12 | 5.90423 | 0.50209 |
KAISER PHOENIX† | 12 | 5.86133 | 0.61742 |
EMERALDAS | 12 | 5.82693 | 0.708859 |
疾風迅雷†LEGGENDARIA | 12 | 5.76623 | 0.566272 |
ICARUS† | 12 | 5.6974 | 0.588566 |
GO OVER WITH GLAREが最も難度が高いという結果である。
意外な結果と思われるかもしれないが、この曲はRootageのEXTRA専用ということで、最初にFAILEDしたきりという人が多いのだろう。2位のCarmina、3位のX-DEN共々今作解禁の曲なので、プレイしにくいことも相まっていると思われる。
ラス殺しが(量産型には)非常にエグいIXが4位。新曲以外ではTOPとなる。
個人的にもかなりキツい印象だったので納得いくところではある。
ところで、難易度が高いことで有名な卑弥呼、灼熱 pt2などが含まれていないことに違和感を感じるかもしれない。
このデータにはNOPLAYが含まれてないため、露骨に高難度なことがわかっている曲は実際より低く評価されがちである(できないことがわかりきっている曲はそもそもプレイされない)。
その点新曲は一回はプレイされるため、より実情に即しているとも言えるだろう。
HARD難度TOP10
songName | Lv | 難度 | 地力度 |
---|---|---|---|
ICARUS† | 12 | 6.19613 | 0.478636 |
X-DEN | 12 | 5.8746 | 0.556091 |
冥 | 12 | 5.51613 | 0.599784 |
卑弥呼 | 12 | 5.34633 | 0.624138 |
Mare Nectaris | 12 | 5.19616 | 0.532996 |
Go Ahead!! | 12 | 5.05182 | 0.620802 |
焔極OVERKILL | 12 | 5.04602 | 0.844977 |
Level One | 12 | 5.04042 | 0.635427 |
疾風迅雷†LEGGENDARIA | 12 | 5.01203 | 0.588161 |
灼熱 Pt.2 Long Train Running | 12 | 4.98316 | 0.506118 |
いつもの、という面々である。
HARDにおいても前述の問題は存在するが、こちらの場合はeasy/clearなどがデータに含まれているのでEASYの難度順よりはより正確である。
この中だと煉獄OVERKILLが若干浮いているように感じる。
地力指数が相当高い(0.8)ため、上級者から見れば簡単と思われるのだろう(その分下からはキツい)
ちなみに、
Lv11HARD難度TOP10
songName | Lv | 難度 | 個人差 |
---|---|---|---|
花冠 feat.Aikapin | 11 | 1.32057 | 0.389596 |
naughty girl@Queen's Palace† | 11 | 1.22396 | 0.448964 |
SABER WING | 11 | 1.02403 | 0.534569 |
FUTURE is Dead | 11 | 0.964118 | 0.289237 |
HYPE THE CORE | 11 | 0.6656 | 0.517921 |
Golden Palms† | 11 | 0.654368 | 0.356392 |
SAMURAI-Scramble | 11 | 0.525223 | 0.350382 |
UMMU | 11 | 0.51142 | 0.556542 |
Fascination MAXX | 11 | 0.465101 | 0.34004 |
V2 | 11 | 0.444756 | 0.83085 |
花冠、強い。大体地力C~Bぐらいである。