クソ診断紹介1「ちんぽ揃えゲーム」

この記事は、クソ診断 Advent Calendar 2019 の2日目の記事です。

1. はじめに

ごあいさつ

皆様はじめまして。赤きちと申します。twitterで主に女子中学生をしています。 昨年に引き続き、「クソ診断 Advent Calendar 2019」が作成されたとの事なので、 4回にわたり過去に作成したクソ診断(診断メーカー)を紹介していきたいと思います。

ちんぽ揃えゲームの概要

「ち」「ん」「ぽ」の3種類の文字から等確率に1文字を選び、 左から右に順番に並べる操作を、末尾3文字が「ちんぽ」となるまで繰り返します。 5文字以内に「ちんぽ」が揃うと、何かが起こるそうです(?) shindanmaker.com

本記事の主な内容

  • 2章「確率について考える」 では、「ちんぽ」の文字列が揃うまでの過程から確率漸化式を立て、文字数の確率分布を調べる方法について解説しています。

  • 3章「実装してみる」 では、診断メーカーの制約条件を乗り越えて実装する工夫の過程を綴っています。末尾の「(参考)診断メーカーの仕様」を適宜参照しながら読むと理解が深まるかもしれません。

  • 4章「Twitterの診断結果を分析する」 では、ツイートされた全診断結果(約3000件)を取得し、ツイートが特定の診断結果に偏っていないか統計検定を行った結果が書かれています。

先行プログラムの紹介

2013年には、たろいも氏により「おちんぽ」の文字列が出るまで「お」「ち」「ん」「ぽ」の4文字を表示し続ける「おちんぽ表示プログラム」が作成されています。 musicstd.nobody.jp 正直なところ、「ちんぽ揃えゲーム」は完全に二番煎じなのです…

2. 確率について考える

確率漸化式をつくる

「ちんぽ」が揃うまでの過程を分かりやすくするため、文字列の状態にADという名前を付けておきます。

状態名 意味
A 末尾3文字が「ちんぽ」
B 末尾2文字が「ちん」
C 末尾1文字が「ち」
D ACのいずれにも当てはまらない

空の文字列(状態 D に相当)に対し、下に示す操作を n 回行います。操作終了後に文字列の状態が A, B, C, D となる確率を、それぞれ a_{n}, b_{n}, c_{n}, d_{n} と表すものとします。

「ち」「ん」「ぽ」の3種類の文字から等確率で1文字を選び、文字列の末尾に加える。
ただし、末尾3文字が「ちんぽ」(状態 A )の場合は、何も行わない

このとき、ある状態から操作を1回行ったときに各状態に遷移する確率は、 図1 のように表せます。

f:id:yryrrrrryryr:20191201001452p:plain:w400
図1 状態遷移図

また、a_{n} ~ d_{n} (n\geq0)は、以下の漸化式で表すことができます。


\begin{pmatrix} a_{0} \\ b_{0} \\ c_{0} \\ d_{0} \end{pmatrix}
= \begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \end{pmatrix},
\begin{pmatrix} a_{n+1} \\ b_{n+1} \\ c_{n+1} \\ d_{n+1} \end{pmatrix}
=\dfrac {1}{3}
\begin{pmatrix}
3 \quad 1 \quad 0 \quad 0 \\
0 \quad 0 \quad 1 \quad 0 \\
0 \quad 1 \quad 1 \quad 1 \\
0 \quad 1 \quad 1 \quad 2
\end{pmatrix}
\begin{pmatrix} a_{n} \\ b_{n} \\ c_{n} \\ d_{n} \end{pmatrix}

この漸化式を用いて、n 回以内の操作で「ちんぽ」が揃う確率は a_{n}、ちょうどn 回の操作で「ちんぽ」が揃う確率は (a_{n}-a_{n-1})と表すことができます。

文字数の分布をみる

「ちんぽ」が揃ったときの文字数の頻度および累積確率分布を 図2 に示します。 中央値は20文字で、20文字以内に半分以上の確率(51.96%)で「ちんぽ」が揃うことになります。

f:id:yryrrrrryryr:20191201020028p:plain:w400
図2 文字数の頻度および累積確率分布

また、期待値の27文字はちょうど3の3乗と綺麗な数字になりますが、「ちんぽ」以外の並びでは、(直観に反して)揃うまでの文字数の期待値が異なることがあります。3文字の全ての組み合わせ27通りで期待値を平均すると、29文字となります。

文字列パターン 組み合わせ数 期待値
ちんぽ 6 27
ちちん 6 27
んちち 6 27
ちんち 6 30
ちちち 3 39

3. 実装してみる

状態遷移を再現する

文字列の状態ごとに抽選に使うリストを作成し、次回の抽選に使うリストを再帰的に参照することを考えます。

  • 状態 D のリスト
抽選結果 次回の抽選に使うリスト
状態 C
状態 D
状態 D


  • 状態 C のリスト (末尾1文字が「ち」)
抽選結果 次回の抽選に使うリスト
状態 C
状態 B
状態 D


  • 状態 B のリスト (末尾2文字が「ちん」)
抽選結果 次回の抽選に使うリスト
状態 C
状態 D
なし(完成!)

上の表を見ると、抽選結果次第では、次回の抽選に使うリストが今回と同一になる事がわかります。
しかし、診断メーカーの仕様上、リストからは「異なるリスト」の抽選結果しか参照する事ができません。 ここで、以下のように抽選回数の奇偶によりリストを分ける工夫を行います。こうすることにより、同一の状態に遷移する場合でも、次回必ず異なるリストを使用するため、診断メーカー上でも正しくリスト間の移動を行うことが可能になります。

リスト名 リストを使用する条件
[LIST1] 状態 D かつ 奇数回目の抽選
[LIST2] 状態 D かつ 偶数回目の抽選
[LIST3] 状態 C かつ 奇数回目の抽選
[LIST4] 状態 C かつ 偶数回目の抽選
[LIST5] 状態 B かつ 奇数回目の抽選
[LIST6] 状態 B かつ 偶数回目の抽選

f:id:yryrrrrryryr:20191201040344p:plain:w400
図3 抽選回数の奇偶によるリストの分割

抽選確率の精度を高める

抽選されたリストが再び使われた場合、無限ループに陥り、診断結果が出力されなくなります。そのため、一度抽選されたリストは、次回以降の抽選で使われないように設定する必要があります。
一方で、1度使用したリストの値を削除するとその後の抽選確率が変化してしまいます。この影響を最小限に抑えるためには、

  • リストの値の個数を可能な限り増やす
  • リストが再帰的に参照される回数を可能な限り減らす

ことが有効です。
今回は、後者の「リストが再帰的に参照される回数を可能な限り減らす」方針で、1回のリスト参照で抽選する文字数を増やしてみましょう。

「ち」「ん」「ぽ」の3種類の文字を一列に n 個並べたときの場合の数は、 3^n 通りです。 ここで、各リストの値の数は999個以下である必要があり、この制約条件を満たす最大の n は6となります。

1回のリスト参照で6文字抽選する場合、抽選前の状態に応じた、抽選後の状態の場合の数は以下のとおりとなります。

抽選後 状態D 状態C 状態B 状態A (完成時の文字数)
6 5 4 3 2 1
抽選前 状態 D 331 216 75 26 27 27 27 0 0
状態 C 291 190 66 23 24 27 27 81 0
状態 B 216 141 49 17 18 18 27 0 243

文字数をカウントする

診断メーカーには、文字数をカウントする関数こそ用意されていないものの、リストから参照された数値の合計を計算する SUMLIST 関数が実装されています。この関数では、実際に結果に表示されなくとも、関数内で使用されたリストの数値は合計対象に含まれます。
そこで、[LIST1][LIST6]の文字列値の後ろに、条件に関わらず何も表示しないIF関数を挿入し、条件文中で文字数と等しい数値が格納されたリストを参照しています。
リスト数には限りがあるため、[LIST8] および [LIST9] に 6 を、[LIST10] に 1 をそれぞれ格納し、2文字~5文字の抽選結果については、枝番を変えて[LIST10]を複数回参照することで文字数をカウントさせています。

(例1) 6文字の抽選結果の場合

ちちちちんぽ=IF([LIST8_1]=0,"","")

(例2) 3文字の抽選結果の場合

ちんぽ=IF(=CALC([LIST10_1]+[LIST10_2]+[LIST10_3])=0,"","")

特殊演出を表示させる

少ない文字数で「ちんぽ」を出せた場合に、評価およびアスキーアートを表示させています。
なお、複数行にわたるアスキーアートは診断結果基本テキストに直接書き込むことはできないので、 [LIST7] 内にアスキーアートを格納しています。

  • 3文字~5文字→ EXCELLENT評価 (出現確率:11.11%)
\( ^ω^)/ EXCELLENT!!
  \  \
   \   γ∩ミ
    ⊂:: ::⊃)
      /乂∪彡\
  • 6文字~10文字→ GREAT評価 (出現確率:16.48%)
(/^ω^)/ 〇U〇 GREAT!

4. Twitterの診断結果を分析する

診断結果を取得する

作成した診断メーカーが診断結果ツイートを通して広く拡散し、沢山の人に遊ばれると嬉しいものです。診断メーカーのアドレスを検索窓に入力して、個々のツイートや診断結果に続く一言コメントを見るのが趣味の一つになっているといっても良いでしょう。
さて、診断結果ツイートが数百、数千と蓄積していくと、個々の診断結果だけでなく、「気に入った結果のときだけ診断結果をツイートしていないだろうか?」といった疑問に対する統計的な答えを知りたくなることがあります。
このような欲望に応えてくれるAPIが、twitter開発者アカウントを登録すると使用可能になる"Search API Full-Archive"です。これを用いると、Twitterサービス開始以来の全ツイートを対象としたツイートの検索および取得ができます。
なお、このサービスには無料版(Sandbox) と 有料版(Premium) があります。有料版の料金は最低でも月額$99 からと、趣味用途に用いるには高額ですが、無料版でも月間50回まで、1回あたり最大100ツイートの検索および取得が可能です。

サービスの詳細は以下のリンクを参考にしてください。 https://developer.twitter.com/en/account/subscriptions/search-fullarchive

さて今回は、無料版のAPIを用いて、診断作成日(2019/01/02)以降で、「ちんぽ揃えゲーム」のアドレス(https://shindanmaker.com/855159)を含む診断結果を取得してみました。取得したツイート数は下記のとおりで、無料版の範囲内に収まりながらも統計的な考察をするには十分なサンプルが集まったかと思います。

  • 取得に要した検索回数: 33回 / 上限50回
  • 取得できたツイート数(RTを除く): 3,031ツイート

また、診断メーカーサイト上での診断人数カウントは 10,922人 (2019/11/30 16:00現在) ですので、診断ページにアクセスした人のうち3割弱(27.8%)が診断結果をツイートしていたことも分かりました。

診断結果ツイートの偏りを調べる

この節では、EXCELLENT評価(3文字~5文字) および GREAT評価(6文字~10文字) が出現する確率が、理論的に期待される確率と有意差があるか調べることを目標に、データ処理や検定を行っていきます。

正規表現による検索

次に、正規表現を用いて、ちんぽ揃えに成功したときの文字列を抽出しました。

  • 文頭が「ち」「ん」「ぽ」のみで構成された文字列で、その後に左括弧が続くものを検索します。
 ^[ちんぽ]+\(
  • また、最後の1文字でちんぽ揃えに成功した場合として、文頭が「ち」「ん」「ぽ」のみで構成された文字列で、その後に"ちんぽ…"が続くものを検索しました。
^[ちんぽ]+ちんぽ

事後確率の計算

「特殊演出を表示させる」の節で、EXCELLENT評価 と GREAT評価 が出現する確率はそれぞれ 11.11% と 16.48% となる事を紹介しました。しかし、正規表現で抽出したツイート中での各評価の出現確率は、先述した確率よりも高くなります。
これは、「ちんぽ」を揃えるまでにツイートが省略されるほど文字数の多い診断結果は、正規表現にマッチしなくなり、サンプルに含まれなくなるからです。

2019年11月6日以降の診断メーカーのシステムでは、診断結果本文に最大120文字が使用可能です。本診断では、120文字から以下の10文字を除いた110文字が、「ちんぽ」を揃えるためのボーダー文字数になります。

  • ハッシュタグ「 #ちんぽ揃えゲーム」(全角9文字分、半角スペース含む)
  • 途中省略が発生した場合、省略記号の「…」(全角1文字分)

さて、正規表現で抽出したツイート中での各評価の出現確率は、「110文字以内にちんぽ揃えに成功した」という情報が加わったときの事後確率と考えることができます。110文字以内に「ちんぽ」が揃う確率が98.80%である事を考慮すると、求める確率は以下の通りとなります。

評価 事前確率 事後確率
EXCELLENT 11.11% 11.25%
GREAT 16.48% 16.68%

ツイートの取捨選択

上記で求めた出現確率は、「110文字以内にちんぽ揃えに成功した」ツイートが母集団であるときに成り立つものです。何らかの理由でちんぽ揃えの成功条件となるボーダー文字数が異なるツイートが含まれる場合、以下のように取捨選択の処理を行いました。

  • 成功条件のボーダー文字数が110文字を超えるツイート → 111文字以上でちんぽ揃えに成功したツイートのみを除外

これは、本診断の診断作成日(2019年1月2日)から現在までの間に、診断メーカーの仕様変更(2019年11月6日)が行われたことに起因します。
仕様変更以前は、診断結果に「#shindanmaker」タグが付与されなかったぶん診断結果に使用可能な文字数が現在よりも多く、成功条件のボーダーもその分緩くなっていました。現在のボーダー文字数に基準を合わせるため、111文字以上でちんぽ揃えに成功したツイートは集計の対象外としました。


  • 成功条件のボーダー文字数が110文字未満のツイート → 全て除外

失われた分布(実際には110文字以内でちんぽ揃えに成功していたにも関わらず抽出不能なもの)は、どんなに頑張っても復元することはできません。よって、診断結果本文に使用可能な文字数を減少させうる条件が含まれるツイートは、文字数の結果によらず全て集計の対象外としました。
具体的には、文頭に「ち」「ん」「ぽ」以外の文字が含まれるツイートを除外しました。対象となったツイートのほとんどは、他者への@リプライとして診断結果をツイートしたものでした。

以上の手順で、合計2,964件のサンプルを抽出し、ちんぽが揃うまでの文字数を集計しました。結果は図4のとおりです。

f:id:yryrrrrryryr:20191202032132p:plain:w400
図4 文字数の期待値および実測値の出現回数分布

二項検定の実施

最後に、「各評価のツイート中の出現率は、診断結果での理論的な出現率と等しい」と帰無仮説を設定し、有意水準 α=0.05 の両側二項検定を行いました。

評価 期待値 実測値 Z統計量 p値
EXCELLENT 333.32 561 13.24 <0.001
GREAT 494.42 526 1.56 0.060

結果として、EXCELLENT評価は、理論的な期待値よりも有意に多く出現している(1.68倍)が、GREAT評価は、理論的な期待値と有意差があるとはいえない事が分かりました。

診断結果に偏りが見られる原因については、

  • 良い結果が出た場合のみツイートをしている
  • 良い結果が出るまで名前を変えつつ何度も診断してからツイートしている
  • 良い結果に改ざんしてツイートをしている

などが考えられます。3番目のような不正行為はやめましょうね!

5. おわりに

本記事では、作成した「ちんぽ揃えゲーム」について、確率的な考察や、実装方法の解説、診断結果の統計分析などを雑多に解説してみました。 私がブログを開設して初めて作った記事という事もあり、想像以上に筆の進みが遅く、まとまった量の文章を書くには普段からの鍛錬が必要であることを痛感させられました。

最後に、診断メーカーを経由せずに「ちんぽ揃えゲーム」をプレイする方法を紹介したいと思います。

シェル芸bot(@minyoruminyon)にフォローバックされる必要がありますが、上記ツイートをコピーしてツイートすると、引用リプライで結果が返ってきます。
シェルスクリプトであれば1行で書けてしまうこのゲームを、診断メーカーで実装しようとすると何十倍もの労力がかかるのは事実です。しかし、何万人あるいは何十万人に診断を楽しんでもらえるのは、抜群に利用者の多い診断メーカーというプラットフォームがあってこその事でしょう。
皆様の診断結果ツイートを励みにして、今後も時間を見つけながら新しい診断メーカーを作っていきたいと思います。

(参考1) 診断メーカーの仕様

診断メーカーの基本データは、「診断結果基本テキスト」と「リスト」で構成されます。

  • 診断結果基本テキスト

    • 診断をした際に「診断結果」として表示される文章のベースとなる部分
    • 全てのリストから抽選結果を呼び出し可能
    • 改行記号[BR]を直接的に使用できない
  • リスト

    • 抽選結果の候補となる「値」を格納する部分
    • 異なるリストから抽選結果を呼び出し可能
    • 改行記号[BR]の使用が可能

リストの呼び出し方法

[LISTx\_y, z] (y, zは省略可能)

記号 意味
x リストの番号
y 枝番 (枝番が異なると、異なる抽選結果が得られる)
z 値の要素番号 (値がカンマで区切られている場合のみ有効)

制約条件

リスト上の制約

作成可能なリストの上限数は10個です。また、リストごとに以下の制約があります。

対象 上限値
値の数 999
各値の文字数※1 300
値の合計バイト数※2 65535

※1 全角文字、半角文字を問わず1文字として計算
※2 全角文字は3バイト、半角文字は1バイトとして計算

診断結果本文の自動カット

ツイートボタン経由のツイート、および「コピペ用(140文字)」の診断結果では、文字数が長い診断結果の全角換算文字数が139文字以下になるように末尾が省略されます。

  • 診断結果本文に使える全角換算文字数は120文字以下 (ツイートの際、#shindanmaker タグおよび診断メーカーへのリンクが自動で付与されるため)
  • 120文字を超過した場合、119文字目までが有効、以降は「…」として省略される
  • 自分でハッシュタグを設定した場合、その分だけ本文に使える文字数が減る (診断結果の後にタグが付与されるため)

関数の仕様

SUMLIST(x_{1},x_{2},...x_{n}) 関数

  • リストから参照された数値の合計を計算する
  • x_{n} は、カウント対象とするリストの番号
  • [LISTx\_y]について、リスト番号 x と枝番 y が同一のものが複数回参照された場合、1回のみカウント

(参考2) 「ちんぽ」が揃う確率の一般項を求める

ゲームのルールのシンプルさとは裏腹に、「ちんぽ」が揃う確率の一般項はやや複雑なものとなります。

ここでは、n 回以内の操作で「ちんぽ」が揃う確率 a_{n} の一般項について、簡単な導出過程を含め紹介していきます。

まず、ちょうど (n+3) 回の操作で「ちんぽ」が揃う必要十分条件は以下の通りです。

  • n 回目の操作終了時に、まだ「ちんぽ」が揃っていない
  • (n+1)〜(n+3) 回目の操作で、「ちんぽ」の 3 文字が続く

このことは、式(1)のように漸化式で表すことができます。

a_{n+3}-a_{n+2}=\dfrac{1}{27} (1-a_{n}) \tag{1}


(1)を整理し、式(2)のように変形します。式(2)の形は一般に「定数係数線形漸化式」と呼ばれます。

27a_{n+3}-27a_{n+2}+a_{n} = 1 \tag{2}


漸化式を満たす具体的な解の 1 つを「特解」、(漸化式の右辺) =0 としたときの一般解を「斉次解」としたとき、一般解は「特解」と「斉次解」の和として求めることができます。


まず、a_{n} = 1 (定数数列) は、式(2)を満たす解の 1 つであるため、式(2)の特解となります。 次に、斉次解を求めるため、式(2)の右辺を 0 とした式(3)の一般解を求めていきましょう。

27a_{n+3}-27a_{n+2}+a_{n} = 0 \tag{3}


(3)中の a_{n+k}x^{k} に置き換えた特性方程式(4)を考えます。

27x^{3}-27x^{2}+1=0  \tag{4}


なお、式(4)は下表のとおり、異なる3つの実数解 p_{1},p_{2},p_{3} をもちます。

記号 近似値
p_{1} \dfrac{1+2cos(\dfrac{7\pi}{9})}{3} -0.177363
p_{2} \dfrac{1+2cos(\dfrac{5\pi}{9})}{3} 0.217568
p_{3} \dfrac{1+2cos(\dfrac{\pi}{9})}{3} 0.959800


(4)の解のうち 1 つを p とします。

a_{n} = C \cdot p^{n-1} とおき、式(3)に代入することで、式(5)を得ます。

 C \cdot p^{n-1}  (27 p^{3} - 27 p^{2} + 1) = 0 \tag{5}


p は 式(4)の解であることから (27 p^{3} - 27 p^{2} + 1) = 0 となり、式(5)C の値によらず恒等的に成立します。

p_{1},p_{2},p_{3} について同様のことがいえるため、C_{1}〜C_{3}を任意定数として、式(2)の斉次解は式(6)のように書き表すことができます。

a_{n} = C_{1} \cdot p_{1}^{n-1} + C_{2} \cdot p_{2}^{n-1} + C_{3} \cdot p_{3}^{n-1} \tag{6}


なお、C_{1}〜C_{3}は、式(6)a_{1}=0, a_{2}=0, a_{3}=\dfrac{1}{27}を代入し連立方程式を解くことで、下表のとおり求まります。

記号 近似値
C_{1} \dfrac {2\sqrt{93}}{27} cos(\theta+\dfrac{\pi}{3})-\dfrac{1}{3} 0.0124236
C_{2} \dfrac {2\sqrt{93}}{27} cos(\theta-\dfrac{\pi}{3})-\dfrac{1}{3} 0.0351339
C_{3} \dfrac {2\sqrt{93}}{27} cos(\theta-\pi)-\dfrac{1}{3} -1.04756

ここで、 \theta は定数で、 \theta = \dfrac{1}{3}tan^{-1}(\dfrac{19}{199\sqrt{3}}) です。


以上より、特解と斉次解を足し合わせた式(7)が求める一般項となります。

a_{n} = C_{1} \cdot p_{1}^{n-1} + C_{2} \cdot p_{2}^{n-1} + C_{3} \cdot p_{3}^{n-1} + 1 \tag{7}

(参考3) ソースコード

診断に使用した基本診断テキストおよび各リストをgithubに公開したので、興味のある人はご覧ください。 github.com