唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

東方キャラの関連性の強さをニコニコ動画の動画数で測ってみた

昨日の記事でニコニコ動画の動画検索の結果を取得できるようになりました

ニコニコ動画 『スナップショット検索API』 に触ってみた - 唯物是真 @Scaled_Wurm
このデータを使って東方キャラ同士の関連性の強さを測ってみます

PMI

PMI(Pointwise Mutual Information)という指標で関連性の強さを測ります

前に別の記事でもちょっと話題にしましたが、これは共起性(ある2つのことが一緒に発生しやすいか)を調べるための指標の一つです
共起性の指標には他にもいろいろあって下記の記事が参考になります

なぜ単純に2つのことが同時に発生した頻度の大小を比較しないかというと、例えばそれぞれ起こりやすい2つの事象が偶然同時に起こる頻度が大きくなることがあるからです

式としては次のようになっています(Wikipediaの式を参考)
$$\operatorname{pmi}(x;y) \equiv \log\frac{p(x,y)}{p(x)p(y)} = \log\frac{p(x|y)}{p(x)} = \log\frac{p(y|x)}{p(y)}$$ 2つの事象\(x\)と\(y\)についてのPMIは\(x\)と\(y\)がそれぞれ起こる確率\(p(x), p(y)\)と、\(x\)と\(y\)が同時に発生する確率\(p(x, y)\)で表されます

事象\(x\)と\(y\)が独立(同時に起こりやすい、起こりにくいなどの関係を持たない)場合\(p(x,y)=p(x)p(y)\)となるので、\(\log\frac{p(x,y)}{p(x)p(y)}\)は独立な場合と比較した時の起こりやすさの比の対数をとったものになっています

PMIは\(-\infty\)のような扱いづらい値になりうるので、-1から1に正規化したバージョンのNormalized PMI(NPMI)を今回は使います
$$\operatorname{npmi}(x;y) = \frac{\operatorname{pmi}(x;y)}{-\log \left[ p(x, y) \right] }$$

東方キャラの関連性の強さをNPMIで測る

昨日の記事でも使いましたが以下の記事から手作業で抽出したキャラのリストを使います

東方Projectの登場キャラクターとは (トウホウプロジェクトノトウジョウキャラクターとは) [単語記事] - ニコニコ大百科

東方を含む動画の中からある一人のキャラを含む確率と、ある二人のキャラを含む確率を使ってNPMIを計算します
PMIは頻度が少ないものに対しては大きくなる傾向があるので、100件の動画以上に含まれるキャラに限定して実行します

結果

上位30件を示します
そこそこ関係性の強いペアが取れていて、よい結果が得られていると思います

NPMI キャラ名1 キャラ名2 両方のキャラを含む動画数 キャラ1を含む動画数 キャラ2を含む動画数
0.931126459724 マエリベリー・ハーン 宇佐見蓮子 87 103 112
0.650891211299 八坂神奈子 洩矢諏訪子 43 130 154
0.631734271825 火焔猫燐 霊烏路空 40 137 148
0.602341571226 姫海棠はたて 射命丸文 49 102 337
0.59609987972 姫海棠はたて 犬走椛 32 102 178
0.590804045548 ナズーリン 寅丸星 44 210 149
0.588793784001 古明地こいし 古明地さとり 120 411 380
0.57607555259 茨木華扇 霍青娥 22 116 100
0.566549459188 射命丸文 犬走椛 60 337 178
0.546457926202 チルノ 大妖精 159 1090 283
0.541293449936 十六夜咲夜 紅美鈴 187 830 491
0.528101230505 八雲藍 71 292 341
0.522984039421 フランドール・スカーレット レミリア・スカーレット 163 561 650
0.502878552921 八意永琳 鈴仙・優曇華院・イナバ 21 117 160
0.495110667871 上白沢慧音 藤原妹紅 42 208 269
0.487466019296 聖白蓮 雲居一輪 24 249 103
0.484447101257 八意永琳 蓬莱山輝夜 22 117 197
0.482048739037 星熊勇儀 水橋パルスィ 29 228 155
0.477043462432 西行寺幽々子 魂魄妖夢 51 241 350
0.475368618885 博麗霊夢 霧雨魔理沙 249 1123 788
0.474838904611 星熊勇儀 茨木華扇 23 228 116
0.471282529366 物部布都 豊聡耳神子 35 230 219
0.470335114779 多々良小傘 東風谷早苗 55 191 516
0.463467329564 八坂神奈子 東風谷早苗 41 130 516
0.458571493885 東風谷早苗 洩矢諏訪子 45 516 154
0.454412940559 古明地さとり 火焔猫燐 33 380 137
0.451740069099 パチュリー・ノーレッジ 小悪魔 80 383 501
0.451734260935 八雲紫 八雲藍 67 508 292
0.446079627968 パチュリー・ノーレッジ 紅美鈴 77 383 491
0.439212242507 蓬莱山輝夜 藤原妹紅 31 197 269

ソースコード

実験に使ったキャラ数や期間が違うので、上の結果とはちょっと違う値が出るかもしれません
何件以上の動画に出てきたかのしきい値が低いとキャラ数の組み合わせが多くなってしまい、検索APIを叩くのに1秒ずつ間を開けているので、データを取るのに数時間かかる場合があるので注意

# -*- coding: utf-8 -*-
import math

from NiconicoSnapshotAPIWrapper import *

def npmi(px, py, pxy):
    if pxy == 0:
        return -1
    
    if px == py == pxy:
        return 1
    
    return -math.log(pxy / px / py) / math.log(pxy)

if __name__ == '__main__':
    api = NiconicoSnapshotAPIWrapper('NiconicoSnapshotAPIWrapper')#アプリ名を入れてください

    freq = {}

    freq[u'東方'] = api.query(u'東方', size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total

    data = u"""博麗霊夢
    霧雨魔理沙
    ルーミア
    大妖精
    チルノ
    紅美鈴
    小悪魔
    パチュリー・ノーレッジ
    十六夜咲夜
    レミリア・スカーレット
    フランドール・スカーレット
    レティ・ホワイトロック

    アリス・マーガトロイド
    リリーホワイト
    ルナサ・プリズムリバー
    メルラン・プリズムリバー
    リリカ・プリズムリバー
    魂魄妖夢
    西行寺幽々子
    八雲藍
    八雲紫
    伊吹萃香
    リグル・ナイトバグ
    ミスティア・ローレライ
    上白沢慧音
    因幡てゐ
    鈴仙・優曇華院・イナバ
    八意永琳
    蓬莱山輝夜
    藤原妹紅
    メディスン・メランコリー
    風見幽香
    小野塚小町
    四季映姫・ヤマザナドゥ
    射命丸文
    秋静葉
    秋穣子
    鍵山雛
    河城にとり
    犬走椛
    東風谷早苗
    八坂神奈子
    洩矢諏訪子
    永江衣玖
    比那名居天子
    キスメ
    黒谷ヤマメ
    水橋パルスィ
    星熊勇儀
    古明地さとり
    火焔猫燐
    霊烏路空
    古明地こいし
    ナズーリン
    多々良小傘
    雲居一輪
    雲山
    村紗水蜜
    寅丸星
    聖白蓮
    封獣ぬえ
    姫海棠はたて
    幽谷響子
    宮古芳香
    霍青娥
    蘇我屠自古
    物部布都
    豊聡耳神子
    二ッ岩マミゾウ
    秦こころ
    わかさぎ姫
    赤蛮奇
    今泉影狼
    九十九弁々
    九十九八橋
    鬼人正邪
    少名針妙丸
    堀川雷鼓
    綿月豊姫
    綿月依姫
    レイセン
    サニーミルク
    ルナチャイルド
    スターサファイア
    茨木華扇
    本居小鈴
    森近霖之助
    稗田阿求
    宇佐見蓮子
    マエリベリー・ハーン""".split()
    for word in data:
        time.sleep(1)
        freq[word] = api.query(u'東方 ' + word, retry = 3, size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total

    cofreq = {}
    for word1 in data:
        for word2 in data:
            if word2 <= word1:
                continue
            time.sleep(1)
            cofreq[word1 + u' ' + word2] = api.query(u'東方 ' + word1 + ' ' + word2, retry = 5, size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total

    TH = 100 #しきい値
    import numpy as np
    result = []
    for word1 in data:
        if freq[word1] < TH:
            continue
        
        px = float(freq[word1]) / freq[u'東方']
        for word2 in data:
            if word2 <= word1 or freq[word2] < TH:
                continue
            
            py = float(freq[word2]) / freq[u'東方']
            pxy = float(cofreq[word1 + u' ' + word2]) / freq[u'東方']
            result.append((npmi(px, py, pxy), word1, word2, cofreq[word1 + u' ' + word2], freq[word1], freq[word2]))

    result.sort(reverse=True)

    for r in result[:10]:
        print r[0], r[1], r[2], r[3], r[4], r[5]