つばきファクトリーで本当ににらめっこが強いのは誰か

行くぜ!つばきファクトリー#76でにらめっこ対決をしていました。個人の対戦成績を列挙すると、

村田x o土居
石井o x豫風
小野x o秋山
石井x o小野田
村田o x八木
小野x o小野田
谷本o x福田
豫風o x河西
秋山o x村田
土居x o小野
谷本o x石井
八木x o石井
小野田o x小野
福田o x村田
福田o x土居
小野田x o豫風
河西o x秋山

です。これを個人の戦績に直すと、

1位 谷本 2勝0敗
2位 小野田 3勝1敗
3位 豫風 2勝1敗
3位 秋山 2勝1敗
3位 福田 2勝1敗
6位 石井 2勝2敗
6位 河西 1勝1敗
8位 土居 1勝2敗
9位 村田 1勝3敗
9位 小野 1勝3敗
11位 八木 0勝2敗

勝率で順位付けしています。
しかし、強い人と当たるとどうしても成績が悪くなりますよね。こういうときよくElo Ratingという指標が用いられますが、あまりに誤差が大きそうなのでMCMCを使ってレーティングを計算してみました。

1位 谷本 1764.4±111.3
2位 豫風 1619.3±76.6
3位 小野田 1619.3±76.5
4位 石井 1619.2±76.6
5位 河西 1556.0±77.0
6位 福田 1546.6±119.4
7位 秋山 1494.5±77.4
8位 小野 1433.9±80.3
9位 土居 1368.3±82.9
10位 村田 1297.5±90.3
11位 八木 1189.2±116.5

mean±σです。勝率とは違う順位になることが分かるでしょう。2~4位は同じ分布です。それから、分布は広くてこんな少ない対戦では本当の強さは分からないことが分かります。


# coding: utf-8
from __future__ import annotations
from itertools import groupby
from collections import defaultdict
from fractions import Fraction
import sys

import pandas as pd
import numpy as np

def calculate_frequencies(trace, players, num_bins=20):
    frequencies = {}
    for player in players:
        samples = trace[player]
        freq, edges = np.histogram(samples, bins=num_bins)
        bin_centers = (edges[:-1] + edges[1:]) / 2
        frequencies[f"{player}_freq"] = freq
        frequencies[f"{player}_bins"] = bin_centers
    return frequencies

def export_frequencies_to_excel(frequencies, filename='player_skill_frequencies.xlsx'):
    # 各プレイヤーのビンセンターを同じにするため、最小および最大範囲を計算
    min_bin = min(min(freq) for freq in frequencies.values() if '_bins' in freq)
    max_bin = max(max(freq) for freq in frequencies.values() if '_bins' in freq)
    bin_centers = np.linspace(min_bin, max_bin, len(frequencies[next(iter(frequencies))]))

    data = {}
    for player in players:
        freq = frequencies[f"{player}_freq"]
        bins = frequencies[f"{player}_bins"]
        data[f"{player}_freq"] = np.interp(bin_centers, bins, freq)
        data[f"{player}_bins"] = bin_centers
    
    df = pd.DataFrame(data)
    df.to_excel(filename, index=False)
    print(f'Data exported to {filename}')

def export_frequencies_to_excel(frequencies, filename='player_skill_frequencies.xlsx'):
    df = pd.DataFrame(frequencies)
    df.to_excel(filename, index=False)
    print(f'Data exported to {filename}')

def export_trace_to_excel(trace, players, filename='player_skills.xlsx'):
    data = {player: trace[player] for player in players}
    df = pd.DataFrame(data)
    df.to_excel(filename, index=False)
    print(f'Data exported to {filename}')

colors = { '谷本': '#e6e6fa', '小野': '#50c878', '小野田': '#ffdab9',
           '秋山': '#ff6347', '河西': '#800080', '八木':   '#ffa500',
           '福田': '#4169e1', '豫風': '#ffdb58', '石井':   '#ffffff',
           '村田': '#ffb6c1', '土居': '#00ff7f' }

# -> (win, lose)
def parse_result(line: str) -> tuple[str, str]:
    for i, c in enumerate(line):
        if c not in 'ox':
            continue
        player1 = line[:i]
        player2 = line[i+3:]
        if c == 'o':
            return (player1, player2)
        else:
            return (player2, player1)
    return ('', '')

def read_results(path: str) -> list[tuple[str, str]]:
    with open(path, 'r') as f:
        return [ parse_result(line.rstrip()) for line in f ]

def add(v: tuple[int, int], w: tuple[int, int]) -> tuple[int, int]:
    return (v[0] + w[0], v[1] + w[1])

def determin_ranking(resutls: list[tuple[str, str]]
                                ) -> list[tuple[int, str, int, int]]:
    dic: dict[str, tuple[int, int]] = defaultdict(lambda : (0, 0))
    for player1, player2 in results:
        dic[player1] = add(dic[player1], (1, 0))
        dic[player2] = add(dic[player2], (0, 1))
    
    players = [ (p, w, l) for p, (w, l) in dic.items() ]
    players.sort(key=lambda v: Fraction(v[1], v[1]+v[2]), reverse=True)
    ranks: list[tuple[int, str, int, int]] = []
    rank = 1
    for r, v in groupby(players, key=lambda v: Fraction(v[1], v[1]+v[2])):
        ps = list(v)
        for p, w, l in ps:
            ranks.append((rank, p, w, l))
        rank += len(ps)
    return ranks

def print_ranks(ranks: list[tuple[int, str, int, int]]):
    for rank, player, win, lose in ranks:
        args = (rank, player, win, lose)
        print('|%d位|%s|%d勝%d敗|' % args)

import pymc3 as pm
import numpy as np

# モデルの構築
import pymc3 as pm

def build_model(matches, players, initial_rating, sigma):
    model = pm.Model()
    with model:
        # 各プレイヤーの実力を正規分布でモデル化
        skills = { player: pm.Normal(player, mu=initial_rating, sigma=sigma)
                                                        for player in players }
        
        # 対戦結果の尤度関数を定義
        for i, (player1, player2) in enumerate(matches):
            skill_diff = skills[player1] - skills[player2]
            prob_win = pm.math.sigmoid(skill_diff)
            pm.Bernoulli(f'{i}', p=prob_win, observed=1)
    
    return model

def sample_model(model, num_samples, num_tune):
    trace = pm.sample(num_samples, tune=num_tune, cores=2, model=model)
    return trace

def print_rate(trace, players):
    rates = [ (np.mean(trace.get_values(player)), player)
                                                for player in players ]
    rates.sort(reverse=True)
    for i, (rate, player) in enumerate(rates, 1):
        print("%d位 %6s %.2f" % (i, player, rate))

import matplotlib.pyplot as plt
import arviz as az
from matplotlib import font_manager

def plot_results(trace, players, path_img):
    # 日本語フォントを指定
    font_path = '/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf'
    font_prop = font_manager.FontProperties(fname=font_path)
    plt.rcParams['font.family'] = font_prop.get_name()
    
    plt.figure(figsize=(10, 6))
    
    for player in players:
        az.plot_posterior(trace, var_names=[player],
                                ref_val=1500, hdi_prob=0.94, ax=plt.gca())
    
    plt.title('Skill Distributions of Players')
    plt.xlabel('Skill Level')
    plt.ylabel('Density')
    plt.legend(players)
    plt.savefig(path_img)
    plt.close()
    print(f'Saved plot to {path_img}')

path = sys.argv[1]
results = read_results(path)
players = list(set(p for v in results for p in v))
ranks = determin_ranking(results)
print_ranks(ranks)

model = build_model(results, players, 1500, 200)
trace = sample_model(model, 2000, 1000)
print_rate(trace, players)
plot_results(trace, players, "N-1.png")
# 頻度の計算
frequencies = calculate_frequencies(trace, players)

# データのエクスポート
export_frequencies_to_excel(frequencies)

年の差ラジオのWOC

https://radio.rcc.jp/toshinosa/

ザ・ギース尾関高文とOCHA NORMA広本瑠璃の年の差ラジオをでゲームをやっていましたが、全く勝てそうになかったのに勝ちました。勝つ確率はどれくらいなのか算出します。

ルールとしては明確ではなかったですが、プレーヤー1は1イニングに1~20のどれかの点数が同確率で入る、プレーヤー2はアウトが1/2、単打が1/3、ホームランが1/6で起こるとします。イニングが終わって10点以上差がついていたらコールドです。

まず、ランダムに1億回プレイしてみましょう。

from __future__ import annotations
from itertools import *
from collections import Counter
import random
import sys
from enum import Enum
from typing import List, Tuple

State = Tuple[int, int, int]

class Action(Enum):
    OUT = 1
    HIT = 2
    HOMERUN = 3
    
    @staticmethod
    def select_randomly():
        r = random.randrange(1, 7)
        if r <= 3:
            return Action.OUT
        elif r <= 5:
            return Action.HIT
        else:
            return Action.HOMERUN

def transit(s: State, a: Action) -> State:
    (score, out, base) = s
    if a == Action.OUT:
        return (score, out + 1, base)
    elif a == Action.HIT:
        if base == 3:
            return (score + 1, out, base)
        else:
            return (score, out, base + 1)
    else:
        return (score + base + 1, out, 0)

def play1_randamly() -> int:
    return random.randrange(1, 21)

def play2_randomly() -> int:
    s = (0, 0, 0)
    while True:
        s = transit(s, Action.select_randomly())
        if s[1] == 3:
            return s[0]

def play_randomly() -> str:
    score1 = 0
    score2 = 0
    for _ in range(9):
        score1 += play1_randamly()
        score2 += play2_randomly()
        if score1 >= score2 + 10:
            return 'Lose'
        elif score2 >= score1 + 10:
            return 'Win'
    
    if score1 > score2:
        return 'Lose'
    elif score1 < score2:
        return 'Win'
    else:
        return 'Draw'

N = int(sys.argv[1])
c = Counter(play_randomly() for _ in range(N))
print(c)
Counter({'Lose': 99860253, 'Win': 139604, 'Draw': 143})

勝つ確率は0.14%程度のようです。
次に、真面目に確率計算をします。最初にプレーヤー2が1イニングに何点入るかの確率を求めます。ただし、181点以上入ったら必ず勝つので、181点以上はまとめて181点とします。

from __future__ import annotations
from functools import reduce
from itertools import *
from collections import Counter
import random
import sys
from enum import Enum
from typing import List, Tuple

State = Tuple[int, int, int]

class Action(Enum):
    OUT = 1
    HIT = 2
    HOMERUN = 3
    
    @staticmethod
    def probability(a: Action):
        if a == Action.OUT:
            return 3 / 6
        elif a == Action.HIT:
            return 2 / 6
        else:
            return 1 / 6

def transit(s: State, a: Action) -> State:
    (score, out, base) = s
    if a == Action.OUT:
        return (score, out + 1, base)
    elif a == Action.HIT:
        if base == 3:
            return (score + 1, out, base)
        else:
            return (score, out, base + 1)
    else:
        return (score + base + 1, out, 0)

def probs_inning(N: int) -> list[float]:
    probs: list[float] = [0.0] * (N + 2)
    c: Dict[State, float] = Counter()
    c[(0, 0, 0)] = 1.0
    while c:
        new_c: Dict[State, float] = Counter()
        for s0, p0 in c.items():
            for action in Action:
                s1 = transit(s0, action)
                score, out, base = s1
                p1 = p0 * Action.probability(action)
                if out == 3:
                    probs[score] += p1
                elif score > N:
                    pass
                else:
                    new_c[s1] += p1
        c = new_c
    print(1.0 - sum(probs[:11]))    # 11点以上取る確率
    probs[N+1] = probs[N]   # 半分ずつ減っていくのでたぶんこれくらい
    return probs

def decide_probs():
    probs1 = [0.0] + [1/20] * 20
    probs2 = probs_inning(180)  # 181点上なら必ず勝つ
    
    probs = [0.0] * 3   # [Lose, Win, Draw]
    scores = { (0, 0): 1.0 }
    for i in range(9):
        new_scores: Dict[tuple[int, int], float] = Counter()
        for (score1, score2), p0 in scores.items():
            for s1, s2 in product(range(1, 21), range(182)):
                prob1 = probs1[s1]
                prob2 = probs2[s2]
                new_score1 = score1 + s1
                new_score2 = score2 + s2
                p = p0 * prob1 * prob2
                if new_score1 >= new_score2 + 10:
                    probs[0] += p   # Lose
                elif new_score2 >= new_score1 + 10:
                    probs[1] += p   # Win
                else:
                    new_scores[(new_score1, new_score2)] += p
        scores = new_scores
    
    for (score1, score2), p in scores.items():
        if score1 > score2:
            probs[0] += p   # Lose
        elif score1 < score2:
            probs[1] += p   # Win
        else:
            probs[2] += p   # Draw
    
    print("Lose: %.6f, Win: %.6f, Draw: %.2e" % tuple(probs))

decide_probs()

まず、プレーヤー2が1イニングに11点以上取る確率は、0.6%程度です。
そして、

Lose: 0.998606, Win: 0.001392, Draw: 1.39e-06

ランダムにプレイしたときとほとんど変わらないですね。

ハロプロ楽曲大賞2019集計(3)

第18回ハロプロ楽曲大賞'19

投票者がどのユニットの曲に入れたかを、自分の推しのユニットごとに集計した。
兼任は当分配で。
グラフは、累積の割合。

f:id:aya_momo:20200105154524p:plain

どのユニットも、自ユニットに最も入れているのは変わらないが、その次に入れているユニットかを見てみると、BEYOOOOONDSヲタ以外はBEYOOOOONDSであることがわかる。

順位を表にすると分かりやすい。

f:id:aya_momo:20200105183431p:plain

以下に、全てのデータを示す。

,モーニング娘。,アンジュルム,Juice=Juice,カントリー・ガールズ,こぶしファクトリー,つばきファクトリー,BEYOOOOONDS,others
モーニング娘。,6635.5,1807.75,2677.25,546.5,1317.0,706.25,3858.5,811.25
アンジュルム,220.75,2258.0,543.5,173.5,332.0,122.0,820.25,280.0
Juice=Juice,262.5,409.0,1990.0,118.0,335.5,153.5,757.5,264.0
カントリー・ガールズ,138.25,231.5,177.25,508.0,96.75,29.75,278.0,95.5
こぶしファクトリー,55.5,110.5,182.5,53.0,1262.0,39.5,307.5,59.5
つばきファクトリー,92.5,149.5,254.0,32.0,171.0,443.5,365.5,92.0
BEYOOOOONDS,260.5,319.5,345.75,92.75,298.75,119.25,2861.75,146.75
others,1068.0,1382.5,1384.75,715.5,695.75,247.5,1696.0,2185.0

ハロプロ楽曲大賞2019集計(2)

第18回ハロプロ楽曲大賞'19


前に記事にしたものだが、グラフにしてみた。

推し部門に入れた投票者を投票先の推しのユニットの推しだとみなして、上位楽曲はどのユニットの推しが投票したのか調べた。兼任メンバーは0.5ずつとした。グラフはポイントの累積割合。

f:id:aya_momo:20200104110432p:plain

1位はJuice=Juiceヲタよりモーニング娘。ヲタの方がポイントを入れている。
モーニング娘。ヲタは全体の41.5%という巨大勢力なので、こういうことも起こる。

ハロプロ楽曲大賞の投票者数の減少は本当にカントリー・ガールズヲタのせいなのか?

第18回ハロプロ楽曲大賞'19

ハロプロ楽曲大賞の投票人数が去年は一昨年に比べて約400人減ったという。推しメン部門では、3905人から3511人になった。
これについて、カントリー・ガールズのヲタが投票しなくなったから、という言説をどこかで見た。これを検証してみる。

まず、一昨年の得票数は、

森戸知沙希 85票
船木結 62票
梁川奈々美 57票
山木梨沙 54票
小関舞 31票
嗣永桃子 29票

と、全体で318票で、400票に満たない。

では、カントリー・ガールズヲタは、他のユニットのヲタより減少しているのだろうか。
一昨年と去年で同じIDの投票者を追いかければ分かるかと思ったが、実は同じ投票者でもIDは変わるらしい。そこで、ニックネームで追いかけることにした。
では、ニックネームはどれくらい重複するだろうか。

一昨年は、ニックネームは3610あり、ユニークな(一度しか現れない)ニックネームは3405。去年は、ニックネームは3287あり、ユニークなニックネームは3129。

そして、一昨年のユニークなニックネームの投票者3405人が去年どうしたかを調べた。

全体では、1729人が投票、1676人が不明(ニックネームを変えているかもしれないので)。

では、カントリー・ガールズヲタの投票者はどうしたのか調べた。

名前 投票 不明
梁川奈々美 30 21
小関舞 8 18
嗣永桃子 13 12
森戸知沙希 36 34
山木梨沙 31 22
船木結 23 31
合計 141 138

全体の投票・不明の割合と変わらない。カントリー・ガールズヲタが投票しなかったという証拠は見つからなかった。

ついでに、各ヲタが推し変したかを調べた。

梁川奈々美 -> 梁川奈々美 9 小林萌花 3 山木梨沙 3 西田汐里 2 その他 13
小関舞 -> 小関舞 6 その他 2
嗣永桃子 -> 嗣永桃子 8 その他 5
船木結 -> 船木結 16 江口紗耶 2 その他 5
森戸知沙希 -> 森戸知沙希 29 野村みな美 2 その他 5
山木梨沙 -> 山木梨沙 26 その他 5

ちなみに、一昨年上位は、

佐藤優樹 -> 佐藤優樹 134 その他 26
小田さくら -> 小田さくら 57 その他 9
宮本佳林 -> 宮本佳林 65 その他 14
加賀楓 -> 加賀楓 74 その他 13
和田彩花 -> 和田彩花 19 中西香菜 4 上國料萌衣 4 その他 27

ニックネームで追うのもそんなに悪くないことが分かる。

ハロプロ楽曲大賞2019集計(1)

第18回ハロプロ楽曲大賞'19


上位の曲に、どのユニットのヲタが投票したかをポイントで集計してみた。
兼任の3人は、等分。

song モーニング娘。'19 アンジュルム Juice=Juice カントリー・ガールズ こぶしファクトリー つばきファクトリー BEYOOOOONDS others
「ひとりで生きられそう」って それってねえ、褒めているの? 1266.0 343.5 1066.5 84.0 126.0 136.0 158.0 401.5
「ひとりで生きられそう」って それってねえ、褒めているの? (New Vocal Ver.) 66.75 26.75 39.5 5.0 11.5 18.0 17.0 15.0
ニッポンノD・N・A! 805.75 260.0 231.0 87.25 82.0 108.5 391.0 268.0
眼鏡の男の子 673.0 211.5 158.0 45.5 60.0 58.0 481.0 187.0
青春Night 1296.5 89.5 89.0 60.0 16.0 35.0 73.0 147.0
赤いイヤホン 378.75 641.25 133.0 64.0 45.0 46.0 62.5 208.0
都営大江戸線六本木駅で抱きしめて 480.0 147.25 120.0 75.75 75.5 94.0 312.0 200.0
人生Blues 1033.0 56.5 85.0 34.0 18.0 24.5 51.5 127.5
I surrender 愛されど愛 873.0 37.75 32.0 12.25 10.0 14.0 34.5 87.5
恋してみたくて 840.25 28.0 34.5 28.75 8.0 12.0 20.5 97.0
微炭酸 240.5 80.5 342.5 35.0 17.0 59.5 29.0 117.0
消せやしないキモチ 178.75 66.25 83.0 31.0 279.5 31.5 54.5 124.5
人生、すなわちパンタ・レイ 153.25 419.0 49.5 39.75 10.0 21.5 33.5 107.0
全然起き上がれないSUNDAY 225.5 291.5 64.5 40.5 11.5 19.0 55.5 78.5
ふわり、恋時計 259.0 68.75 71.5 14.75 15.0 203.5 52.0 79.0
One Summer Night ~真夏の決心~ 161.75 82.75 58.5 190.0 22.5 16.0 31.5 82.0
ハルウララ 179.25 69.0 56.0 16.25 164.0 47.5 41.5 64.0
三回目のデート神話 195.0 51.75 73.0 13.75 19.5 194.0 31.5 39.0
元年バンジージャンプ 117.25 27.75 68.0 21.5 16.5 14.5 212.0 66.0
弱気女子退部届 126.25 54.5 34.5 152.25 16.5 15.0 27.5 103.5