あかり描像のブログ

思ったことや学習記録を適当に書いていきます。お気軽にコメントください

【5ch】「きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ」 のシミュレーションと集計をやってみた

ある日出会ったこちらの素敵なコンマスレのシミュレーションを突然やってみたくなったのでやりました。

きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)
https://fate.5ch.net/test/read.cgi/lovelive/1690078481/

ここにちらっと現れているもんじゃが私です。

※イメージ画像

環境

  • Windows 11 Pro
  • Python 3.7.6
    • Numpy 1.18.1
    • Pandas 1.0.1
    • Plotly 5.15.0
    • requests 2.22.0
    • beautifulsoup4 4.8.2
  • Jupyter Notebook 6.0.3

今回、途中の入浴者交代の様子が分かるように、領域別で色を変えて plot をしたかったのですが、Matplotlib では (できないことはないけど) 厳しそうだったので、Python の標準には入っていない Plotly を使うことにしました。
plotly.com


(自分用メモ)Matplotlib でも、LineCollection などを使えばできるっぽい。
pylab_examples example code: multicolored_line.py — Matplotlib 2.0.2 documentation

なんなら、2本の ndarray があるなら、該当しない方にも同時に NaN でも入れてやれさえすればよかったかもしれない。

(自分用メモ終わり)


Python3 の導入は、公式サイトからダウンロードしても良いですし、

www.python.org

Jupyter Notebook などを利用しても良いですし、

jupyter.org

Windows の方であれば Microsoft Store からダウンロードしても良いです。

Plotly は別途導入する必要があります。
(以下のコマンドは Windows PowerShell でも Mac のターミナルでも Linux の各種シェルでも動くと思います。)

python -m pip install --upgrade pip
pip install plotly

Numpy と Pandas は標準で既に入っていると思いますが、もしないと怒られた場合は上と同様に

pip install numpy
pip install pandas

してください。

シミュレーションパート

レギュレーション

  • 最初、きな子は 50 ℃ のお風呂に入っている
  • 1レスごとに 1 ℃下がる
  • 99, 00 が出たらオニナッツと交代する (オニナッツがお風呂に入っているときはきな子と交代する)
  • その他のゾロ目が出たら 10 ℃上がる

シミュレーションコード

オンライン上に載っけて誰でも遊べるようにしたかったのですが、Plotly を簡単に使えるツールが見つけられなかったので、ここにコードを貼り付けます。

雑にですが GitHub にも上げたので、ご興味ありましたら是非ご覧ください (.ipynb 形式です)。
github.com


タップで開く。(タップで閉じる)

# きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)
# https://fate.5ch.net/test/read.cgi/lovelive/1690078481/

import pandas as pd
import numpy as np
import random
import plotly.graph_objects as go   # Required plotly
from plotly.offline import iplot

Kinako_Color = "#fff442"  # メイズイエロー
Natsumi_Color = "#ff51c4" # オニナッツピンク

terminate = 1000  # 総レス数
temperature = 50  # 初期温度
isKinako = True   # 現在お風呂に入っているのはきな子であるか否か

# レス番号の ndarray
index = np.arange(terminate + 1)
# 温度の ndarray
temperature_array = np.array([temperature])
# お風呂に入っているのはきな子であるか否かの ndarray
isKinako_array = np.array([isKinako])

if __name__ == '__main__':
    
    for i in range(terminate):
        comma = random.randrange(100)
        
        # Update values
        if comma == 0 or comma == 99:
            isKinako = not isKinako
        elif comma % 11 == 0:
            temperature += 10
        else:
            temperature -= 1
                
        # push_back
        temperature_array = np.append(temperature_array, np.array([temperature]))
        isKinako_array = np.append(isKinako_array, np.array([isKinako]))

    # Plotly の setup
    fig = go.Figure()
    fig.update_layout(title = 'きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)')
    fig.update_xaxes(title = '# of response')
    fig.update_yaxes(title = 'Temperature [℃]')
    
    # Pandas の DataFrame を利用して plot
    df = pd.DataFrame({'index': index, 'temperature': temperature_array, 'isKinako': isKinako_array} )
    
    fig.add_scattergl(x = index, y = df.temperature.where(df.isKinako), line = {'color': Kinako_Color}, name = 'Kinako')
    fig.add_scattergl(x = index, y = df.temperature.where(df.isKinako == False), line = {'color': Natsumi_Color}, name = 'Natsumi')
    
    iplot(fig)


  • キャラクターのイメージカラーについては Wikipedia にあるカラーコードを参照しています。
  • 横軸に使うレス数 index と、縦軸の温度 temperature_array と、色分けをするために用意した現在お風呂に入っている女の子がきな子であるかを表す isKinako_array には Numpy の ndarray を使用しています。後に色分けプロットをする際に where 関数を使うことができます。 あとになって思ったのですが、この where は Pandas の DataFrame の where だったので、ndarray はまったく関係ない。
  • 各レスのコンマ以下の数字としては 0~99 の乱数を利用しています。そのため、「意図的にゾロ目を狙って書き込む」ような状況には対処し切れていません。
  • ndarray の append() は書き方がやや面倒です。Python 標準の list のように append が ndarray のメンバ関数 (メソッド) になっていないためです。
  • Pandas の DataFrame を予め作っておいて、逐次 append (若しくは concat) しても良いですが、書き方が面倒だったので (筆者にそれを書く力がなかったため)、とりあえず 2 つの ndarray を作ったのち、完成後にまとめて DataFrame を作る方針を採りました。
  • 最後のプロットの際に、Plotly の add_scattergl を利用しているのが肝です。ここに関しては私が日本語で説明するよりもコードとにらめっこして感じた方が良いと思うので、詳細は割愛します。以下のサイトが大変参考になりました。

実行例ギャラリー

  • 横軸が総レス数で、縦軸が温度[℃] です。普通に絶対零度よりも下がり得ます。
  • 黄色 (メイズイエロー) ではきな子、紫色 (オニナッツピンク) では夏美がお風呂に入っています。
  • 典型的なランダムウォークで、実行の度に出てくる線の振る舞いが大きく変わります。ここでは、6 回のシミュレーション結果を掲載してみます。

考察とか

確率で言うと、

  • 2% で交代
  • 8% で +10
  • 90% で -1

なので、1 レスごとの温度変化の期待値は -0.1℃、1000 レス目では期待値は -50 ℃になる。
よって、全体としては温度が下がるトレンドになるはずだが、上のギャラリーを見ただけでは正直よく分からない。上昇値と減少値がほどんど一緒のケースもあるし、なんか全体的に温度が上がってそうなのもある。
何回かこのシミュレーションを回して、N レス目の温度で histogram を作ってみると考察が捗りそうな気はする。
今回のレギュレーションでの温度は  \mathbb{Z} 上のマルコフ連鎖になっているため、再帰性について考えてみても面白いのかもしれない。

交代の回数の期待値は 1000 レスで 20 回だが、上のシミュレーションギャラリーを見る限り 15, 23, 21, 21, 29, 27 になっているので、何かそれっぽい感じはする。

各レス数における温度や交代回数の分散は、、、各シミュレーションギャラリーのトレンドラインは、、、などと考えていくと、確率過程を勉強してみたくなってくる。

後日また似たスレが立ったので、今回のと比較しても楽しいかもしれない。

1スレごとに歩夢ちゃんが太るスレ
https://fate.5ch.net/test/read.cgi/lovelive/1690284149/

。。。はい。考察になってないですね。すみません。

スクレイピングで集計する

【2023/08/29 追記】【2023/09/27 追記】
なんと次スレが!!

きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)
https://fate.5ch.net/test/read.cgi/lovelive/1693319159/

きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)★3
https://fate.5ch.net/test/read.cgi/lovelive/1695018529/


オニフッユも参戦!!

imgfilp.com


もうすでに先駆者が何人かいらっしゃいますが、せっかくなので集計もやっちゃいましょう!!*1

レギュレーション

オニフッユ参戦に伴い, Part 2 以降ではレギュレーションが次のようになりました。

  • 最初、きな子は 50 ℃ のお風呂に入っている
  • 1レスごとに 1 ℃下がる
  • 99 が出たらオニナッツと, 00 が出たらオニフッユと交代する (鬼塚姉妹がお風呂に入っているときはきな子と交代する)
  • その他のゾロ目が出たら 10 ℃上がる

方針

コンマ数をスクレイピングによって自動取得します。
スクレイピングには requests と BeautifulSoup4 を用います。

Python によるスクレイピングに関してはこちらの記事が分かりやすいです。
qiita.com

Plotly 同様、標準には入っていないので、新しく導入する必要があります。

pip install --upgrade pip
pip install requests
pip install bs4


さて、コンマ数の取得についてですが、2023年9月現在、5ch の HTML ではレスの投稿時間は

<span class="date">2023/09/18(月) 15:28:49.00</span>

のように格納されています。
そのため、HTML からこのタグをすべて取り出してきて、それらの末尾 2 文字を整数として取り出してゆけばよい、ということになります。詳細は次節のコードにて。

集計コード


タップで開く。(タップで閉じる)

# きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)★3
# https://fate.5ch.net/test/read.cgi/lovelive/1695018529/
import requests
from bs4 import BeautifulSoup
import pandas as pd
import plotly.graph_objects as go   # Required plotly
from plotly.offline import iplot

PART1_URL = 'https://fate.5ch.net/test/read.cgi/lovelive/1690078481/'
PART2_URL = 'https://fate.5ch.net/test/read.cgi/lovelive/1693319159/'
PART3_URL = 'https://fate.5ch.net/test/read.cgi/lovelive/1695018529/'

Kinako_Color = "#fff442"  # メイズイエロー
Natsumi_Color = "#ff51c4" # オニナッツピンク
Tomari_Color = "#4dd2e1"  # スモーキーブルー

temperature = 50   # 現在の温度
bather = 'Kinako'  # 現在の入浴者 'Kinako', 'Natsumi', 'Tomari'

comma_array = []
temperature_array = [temperature]
bather_array = [bather]

# Fetch all comma values from 5ch's html using requests and BS4
def fetchComma():
    global comma_array
    
    response = requests.get(PART3_URL)
    soup = BeautifulSoup(response.text, 'html.parser')

    # Find <span class="date">~</span> and push the comma value into commaArray.
    dates = soup.find_all('span', {'class': 'date'})
    for d in dates:
        comma_array.append( int(d.get_text()[-2:]) )

    
# Calculate temperature using comma_array
def calcTemperature():
    global temperature, bather
    global comma_array, temperature_array, bather_array
    
    for comma in comma_array:
        if comma == 99:
            if bather == 'Kinako':
                bather = 'Natsumi'
            else:
                bather = 'Kinako'
        elif comma == 0:
            if bather == 'Kinako':
                bather = 'Tomari'
            else:
                bather = 'Kinako'
        elif comma % 11 == 0:
            temperature += 10
        else:
            temperature -= 1
        
        temperature_array.append( temperature )
        bather_array.append( bather )
        

# Create a graph
def createGraph():
    fig = go.Figure()
    fig.update_layout(
        title = 'きな子の入ってるお風呂の温度が1レスごとに1℃下がるスレ(50℃からスタート)★3',
        font = {'size': 16},
        legend = {'font': {'size': 16 } },
        plot_bgcolor = 'white'
    )
    fig.update_xaxes(
        title = '# of response', 
        title_font = {'size': 26},
        mirror = True,
        ticks = 'outside',
        linecolor = 'black',
        gridcolor = 'lightgrey'
    )
    fig.update_yaxes(
        title = 'Temperature [℃]',
        title_font = {'size': 26},
        mirror = True,
        ticks = 'outside',
        linecolor = 'black',
        gridcolor = 'lightgrey'
    )
    
    df = pd.DataFrame( {'temperature': temperature_array, 'bather': bather_array} ) 
    
    fig.add_scattergl(x = df.index.tolist(), y = df.temperature.where(df.bather == 'Kinako'),
                          line = {'color': Kinako_Color}, name = 'Kinako')
    fig.add_scattergl(x = df.index.tolist(), y = df.temperature.where(df.bather == 'Natsumi'),
                          line = {'color': Natsumi_Color}, name = 'Natsumi')
    fig.add_scattergl(x = df.index.tolist(), y = df.temperature.where(df.bather == 'Tomari'),
                          line = {'color': Tomari_Color}, name = 'Tomari')
    
    iplot(fig)


if __name__ == '__main__':
    fetchComma()
    calcTemperature()
    createGraph()


基本はシミュレーションのコードを流用していますが、以下のような変更・追記をしています。

  • NumPy はメリットがないので使うのを止めました。
  • 横軸に入れる index は自分で作らなくてもよいことが分かったので (Pandas の DataFrame の index.tolist() で指定できる) 消しました。
  • 入浴者の管理方法を bool 値の isKinako ではなく、'Kinako', 'Natsumi', 'Tomari' の 3 値をとる bather に任せることにしました。
  • 少しだけ関数分けしました。
    • fetchComma()
      • ここで 5ch の HTML を取得し、コンマ値のみを抽出して comma_array へ格納していきます。
    • calcTemperature()
      • レギュレーションに従い、コンマ値からお風呂の温度及び入浴者の推移を求め、それぞれ temperature_array, bather_array へと格納していきます。
    • createGraph()
      • グラフを作ります。見た目長いですが Plotly の Setup が嵩張っているだけです。文字サイズを大きくしたり背景色を白にしたりそういうことをやっています。

実行例

Part3 の実行例。2023年9月27日2:50 頃実行。

上のコードを実行するとこのような感じのものが出力されます。
マウスポインターを折れ線に当てることで、その点での値がインタラクティブに分かるようになっています。便利!!

fetchComma()requests.get()PART2_URL を指定してあげるとそのまま Part2 の集計結果が得られます*2

Part2 の実行例。

感想

精神的に勉強するかって気分になり突発的に始めましたが、久しぶりに Python のいい勉強になりました。
今回は入浴者別に色分けするためだけに Plotly を使いましたが、このライブラリは他にも色々強そうなことができそうなので、また気が向いた時があったら遊んでみたいです。

スクレイピングも体験できて楽しかったです。requests と BeautifulSoup4、強すぎますね。ブラウザじゃないから Same Origin Policy とか気にしなくいいのもでかい。
コンマを取ってくるルーチン fetchComma() は毎回同じはずなので流用できるかも??

謝辞

このような記事を書くことができたのは、数々の恩方の支援によるものです。
第一に、このような素敵な機会を与えて下さったわたあめさん、および次スレを立てて下さった茸さん, 調整中さんに感謝いたします。また、いち早く自動集計スクリプトを作成し、私に火を付けて下さった茸さんともんじゃさん (国際宇宙ステーションさん) に感謝いたします。更に、1レスごとに矢澤にこちゃんのおっぱいが0.1cm膨らむスレ 71.0cmからスタート にて Python のフィードバックを賜ったもんじゃさんに感謝いたします。最後に、私を精神的に支えて下さったすべてのラ板の住民の皆さんに感謝いたします。

*1:ちなみに余談なのですが、1レスごとに矢澤にこちゃんのおっぱいが0.1cm膨らむスレ 71.0cmからスタート で Trinket.io 上に集計する Python を共有しているもんじゃも私です。ご興味があれば。

*2:スレタイは自動取得させていないので、グラフのタイトルはハードコーディングです。自動取得しても良いですが、面倒なだけで fetchComma() に追記すれば済む話なので、今回は勘弁してください。