ある日出会ったこちらの素敵なコンマスレのシミュレーションを突然やってみたくなったのでやりました。
きな子の入ってるお風呂の温度が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 の導入は、公式サイトからダウンロードしても良いですし、
Jupyter Notebook などを利用しても良いですし、
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 を作ってみると考察が捗りそうな気はする。
今回のレギュレーションでの温度は 上のマルコフ連鎖になっているため、再帰性について考えてみても面白いのかもしれない。
交代の回数の期待値は 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/
オニフッユも参戦!!
もうすでに先駆者が何人かいらっしゃいますが、せっかくなので集計もやっちゃいましょう!!*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
へ格納していきます。
- ここで 5ch の HTML を取得し、コンマ値のみを抽出して
calcTemperature()
- レギュレーションに従い、コンマ値からお風呂の温度及び入浴者の推移を求め、それぞれ
temperature_array
,bather_array
へと格納していきます。
- レギュレーションに従い、コンマ値からお風呂の温度及び入浴者の推移を求め、それぞれ
createGraph()
- グラフを作ります。見た目長いですが Plotly の Setup が嵩張っているだけです。文字サイズを大きくしたり背景色を白にしたりそういうことをやっています。
感想
精神的に勉強するかって気分になり突発的に始めましたが、久しぶりに Python のいい勉強になりました。
今回は入浴者別に色分けするためだけに Plotly を使いましたが、このライブラリは他にも色々強そうなことができそうなので、また気が向いた時があったら遊んでみたいです。
スクレイピングも体験できて楽しかったです。requests と BeautifulSoup4、強すぎますね。ブラウザじゃないから Same Origin Policy とか気にしなくいいのもでかい。
コンマを取ってくるルーチン fetchComma()
は毎回同じはずなので流用できるかも??
謝辞
このような記事を書くことができたのは、数々の恩方の支援によるものです。
第一に、このような素敵な機会を与えて下さったわたあめさん、および次スレを立てて下さった茸さん, 調整中さんに感謝いたします。また、いち早く自動集計スクリプトを作成し、私に火を付けて下さった茸さんともんじゃさん (国際宇宙ステーションさん) に感謝いたします。更に、1レスごとに矢澤にこちゃんのおっぱいが0.1cm膨らむスレ 71.0cmからスタート にて Python のフィードバックを賜ったもんじゃさんに感謝いたします。最後に、私を精神的に支えて下さったすべてのラ板の住民の皆さんに感謝いたします。
参考
以下のリンクは、上述のコードを作成する上で関係のないものも含みます。
- Plotly 公式
- Stack Overflow
- python - Is it possible to change line color in a plot if exceeds a specific range? - Stack Overflow
- python - Change color of lineplot depending on data - Stack Overflow
- pandas - Plot Multicolored line based on conditional in python - Stack Overflow
- python - Random walk pandas - Stack Overflow
- python 2.7 - Matplotlib - Changing line color above/below hline - Stack Overflow
- python - Setting Background color to transparent in Plotly plots - Stack Overflow
- numpy.where — NumPy v1.26 Manual
- Color change by range in line chart - 📊 Plotly Python - Plotly Community Forum
- Change the background colour and/or theme - Dash Python - Plotly Community Forum
- [matplotlib] 39. データの値に応じてプロットの色をかえる – サボテンパイソン
- matplotlib plotの色を、値によって変える。: 長続き目指しブログ
- 【plotly】グラフタイトルの設定[Python] | 3PySci
- pandas.DataFrameの構造とその作成方法 | note.nkmk.me
- [Python] Plotlyでぐりぐり動かせるグラフを作る #Python - Qiita
*1:ちなみに余談なのですが、1レスごとに矢澤にこちゃんのおっぱいが0.1cm膨らむスレ 71.0cmからスタート で Trinket.io 上に集計する Python を共有しているもんじゃも私です。ご興味があれば。
*2:スレタイは自動取得させていないので、グラフのタイトルはハードコーディングです。自動取得しても良いですが、面倒なだけで fetchComma() に追記すれば済む話なので、今回は勘弁してください。