【IIDX】曲データの統計分析を試す【2: IRT編】

前回は曲データの収集を行った。
今回は早速得られたデータの難易度推定を行っていきたいと思う。
といっても、この記事はその準備が大半なのでせっかちな人は適当に読み飛ばしてどうぞ。

次: 【IIDX】曲データの統計分析を試す【3: 難易度表編】 - analytics-arkのブログ

introduction

IRT

今回使用する手法は項目応答理論(IRT)と呼ばれるものである。
よく知られた例だとTOEFLなどの試験に使われている手法である。

BMSの難易度推定(リコメンド)もこれが使われている。というかそれの真似です。
こちらの記載Wikipediaなどが詳しい*1

リンクだけ貼るのも味気ないので自分なりの理解をここに書いておく。

一般的な試験におけるIRT

一般的な試験で例えよう。

試験にはいろいろな難易度の問題がある。100問とか200問とかあるわけである。
この時、以下の2つを同時に考えることができる。

優秀な人間

大体の問題を正解できる人間。9割の得点ができる人というのは言うまでもなく5割得点の人より優秀である。

難易度の高い問題

上記の優秀な人間でも正解できない、あるいは正答率が低い問題というのは難しい問題ということになる。


この2つは相補的である。
つまり【難しい問題を正解できれば→優秀な人間】ともいえるし、
【優秀な人間でも正解できなければ→難しい問題】ともいえる。


さて、こういう状況のときに得点をどうやって決めるか、という問題が発生する。

一律同じ配点の場合、難しい問題を解けた恩恵というのは全くないことになる。
これでは「優秀な人間」には面白くないだろう。
やはり難しい問題を解けた場合はその人間の評価も高めてほしいものである。


こういう問題を解決するのがIRTである。

殆どの人が正解できる問題の配点は下げ、難しい問題の配点を上げることで個人個人の評価をより正確に行うことができる。
そして、その副産物として「問題の難易度」も導かれる(先述したように、この2つは相補的であるため)。

また、実際には更にもう一個「個人差の激しい問題」というカテゴリーも追加される。


こういった状況についてもパラメータを追加することでうまく記述できるのがIRTの利点である。

欠点としては、直感的ではないのと計算が面倒なことが挙げられる。

音ゲーにおけるIRT

さて、これを音ゲーに転用しよう。


問題を各楽曲、問題の正答率を楽曲のクリア状況と置き換える。

すると、各プレイヤーのクリアスキル各楽曲の難しさが計算できることになる。
また、各楽曲の個人差も同時に算出できる。


各楽曲の難しさ個人差
これこそ欲しかったものではなかろうか!


というわけで、うまいことハマっている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が出来上がる。
f:id:arikaneko:20191215145146p:plain


後はこのテーブルをCSV出力する。phpmyadmincsv出力を使用した。
この際、【1 行目にカラム名を追加する】にチェックを入れておくこと。

Google Driveにアップロード

先程出力したCSVGoogle Drive上にアップロードする。
後述するColaboratoryで使用するために必要なので、適当にディレクトリを作成して上げておく。

pythonでIRT計算

Google Colaboratoryを使う

解析処理にはそれなりにマシンパワーが必要だが、私はノートPCしか持ち合わせていない。
そこで、実マシン上で処理するのではなくGoogle Colaboratoryを使用する。
colab.research.google.com


GPUなどの充実した環境を無料で使うことができる。
スペック面についてはこちらが詳しい。

使用ライブラリ

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に対応している。
f:id:arikaneko:20191215165145p:plain

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()

f:id:arikaneko:20191215170524p:plain

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- 126.871970.684238
Carmina 126.585380.692371
X-DEN 126.327720.62095
IX 126.105280.804026
焔極OVERKILL 126.013950.80108
Mare Nectaris 125.904230.50209
KAISER PHOENIX 125.861330.61742
EMERALDAS 125.826930.708859
疾風迅雷†LEGGENDARIA 125.766230.566272
ICARUS† 125.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† 126.196130.478636
X-DEN 125.8746 0.556091
125.516130.599784
卑弥呼 125.346330.624138
Mare Nectaris 125.196160.532996
Go Ahead!! 125.051820.620802
焔極OVERKILL 125.046020.844977
Level One 125.040420.635427
疾風迅雷†LEGGENDARIA 125.012030.588161
灼熱 Pt.2 Long Train Running 124.983160.506118

いつもの、という面々である。
HARDにおいても前述の問題は存在するが、こちらの場合はeasy/clearなどがデータに含まれているのでEASYの難度順よりはより正確である。

この中だと煉獄OVERKILLが若干浮いているように感じる。
地力指数が相当高い(0.8)ため、上級者から見れば簡単と思われるのだろう(その分下からはキツい)


ちなみに、

Lv11HARD難度TOP10

songName Lv 難度 個人差
花冠 feat.Aikapin 111.32057 0.389596
naughty girl@Queen's Palace† 111.22396 0.448964
SABER WING 111.02403 0.534569
FUTURE is Dead 110.9641180.289237
HYPE THE CORE 110.6656 0.517921
Golden Palms† 110.6543680.356392
SAMURAI-Scramble 110.5252230.350382
UMMU 110.51142 0.556542
Fascination MAXX 110.4651010.34004
V2 110.4447560.83085


花冠、強い。大体地力C~Bぐらいである。

Next:
analytics-ark.hatenablog.com

*1:BMSのリコメンドではaは曲ごとに一定である。が、実際にはEASYは地力/HARDは個人差というケースも多々あるので今回はランプごとに算出する