【Rubyで検証】普通な人はあまり存在しない件 〜 普通の人とマッチングする確率とは？な話

![kataomoi_woman.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605568/0a6db5d4-9fb3-1a10-ef55-1e9703a44a92.png)


> 「私、結婚する人の条件はふつうな人（年収700万）でいいの〜」
（なお、サラリーマンの平均年収は約400万）

みたいな話は有名かと思いますが、ほんとうに普通の人ってなかなかいないよねというお話です
（半分真面目に、半分ふざけて計算してみた）

## 今回のメインテーマ
ざっくり話をまとめると、次のようなことをお伝えする記事となっております

* **普通の人ってどのくらいの確率で存在するの？**
* **↑の計算するのにRubyでどうプログラムすれば良いの？**

この記事をきっかけに、`統計`や`Ruby`について親しみを持ってもらえればと思います

## 前提
まずは前提条件を整理しておかないと変な文句を言われかねないので、整理しておきます（というかこれをしないと計算できない）

**普通の人の定義**
以下の各項目の偏差値が50以上とします（これは普通ぐらいでいてほしいとみんなが思いそうなことをあげてみました）

* 身長
* 顔
* 頭脳（学歴とも言う）
* 性格
* 年収
* ファッション

**分布**
全てのステータスは**正規分布**に従うと仮定する

**その他補足**
それぞれのステータスは独立しており、他のステータスに影響を与えない
**（高身長だと高年収になるというような関係は無視する）**

## 計算してみる
各ステータスについて、偏差値50以下と50以上で数が半々になるので、偏差値50以上となる確率は1/2です

ステータスは６つを想定しているので、全て50以上となる確率は単純に

```math
\frac{1}{2}^6=\frac{1}{64}≒0.0156=1.56 [\%]
```

**つまりたったの2%程度です**😇

普通というぐらいだから最低でも40〜50%程度には落ち着いてもらいたいところ

ではどこまで妥協すれば良いのでしょうか？

やり方としては、次の２つがありそうです💡

**1. 普通であってほしいステータスを絞る**
**2. 普通という基準を下げる（基準の偏差値を50より小さくする）**


## 検証①
１つ目の方法は一瞬で終わります

ステータスが１つの場合は確率が50%
ステータスが２つの場合は`50% × 50%`で**25%**

25%（4人に1人）を普通というのは苦しいので、この方法を取る場合は、普通であって欲しいステータスは１つまでということになります（うーん厳しい）😕


## 検証②
こちらは期待する偏差値をどこまで下げたら良いの？ということですが、やや面倒です🌀

計算で出そうとすると、次のような手順になりましょう
（話が多少面倒なので、`（ここから）`〜`（ここまで）`間は読み飛ばしてもOKです）

`（ここから）`
ある偏差値`s`以上の確率を`p`としたときに、全てのステータスがある偏差値`s`以上になる確率が50%になれば良い

全てのステータスが`s`以上になる確率は次のように計算できる

```math
\begin{align}
\qquad &p^6 = 0.5\\
  \therefore &p = 0.89 = 89 [\%]
\end{align}
```
つまり存在確率が`89%`となる偏差値`s`を求めれば良いということになる

これを計算するには正規分布の確率密度関数を`s`から無限大の範囲で積分したときに`89%`となる`s`を求めれば良いということになる（ﾀﾌﾞﾝ）
`（ここまで）`

しかしそんな計算したくない（というか自分には多分ムリ）

なのでプログラミングで概算値をざっと算出してみることにしました

※ この手の計算は`Python`や`R`を使ったほうが楽に計算できるのかと思いますが、あいにく筆者はそのへん使い慣れてないので、今回はRubyで無理やりやることにしています（汗）

## プログラムの概要
次の手順で算出していこうと思います
（ゴールは**ここまでは妥協すべきという偏差値**を求めることです）

* 人物のモデルを作成
* インスタンス作成時に６つのステータスを付与
* ステータスは正規分布にしたがってランダムに与えられる
* 10000人分のインスタンスを作成し、全てのステータスが偏差値50以上となる人数をカウントし、その存在確率を算出する
* 存在確率が50%を下回ったら、期待する偏差値を1下げて再び存在確率を算出
* 50%以上となる偏差値を求める

※ 10000という試行回数については、このぐらいやれば問題ないだろうという勝手な数値としています。何度かこの条件で計算して、そこまで数値がぶれなかったのでまあ問題ないと判断しています

## プログラムの内容
まずはPersonモデルを作成します


```ruby:person.rb
require './rand'

class Person
  attr_accessor :height, :face, :brain, :personality, :income, :fashion

  MU_SIGMA = { mu: 50, sigma: 10 }

  def initialize(expectation)
    @height = Random.new.normal_rand(**MU_SIGMA)
    @face = Random.new.normal_rand(**MU_SIGMA)
    @brain = Random.new.normal_rand(**MU_SIGMA)
    @personality = Random.new.normal_rand(**MU_SIGMA)
    @income = Random.new.normal_rand(**MU_SIGMA)
    @fashion = Random.new.normal_rand(**MU_SIGMA)
    @expectation = expectation
  end

  def nomal?
    height >= @expectation &&
    face >= @expectation &&
    brain >= @expectation &&
    personality >= @expectation &&
    income >= @expectation &&
    fashion >= @expectation
  end
end

```
* initialize内ではRandomクラス（後述）によって、単純な乱数から正規分布に従う確率変数を発生させ、偏差値を付与しています。あと期待する偏差値をインスタンス変数として生成しています。

↑良い感じのライブラリがあればよかったんですが、見つからなかったのでこのような手法をとっています。

* normal?メソッドでは、全てのステータスが期待する偏差値（expectation）以上かどうかの判定をしています

↑ちなみに`mu`は母集団の平均値、`sigma`は標準偏差であり、偏差値の定義上平均が50、標準偏差を10にしております


続いて実際に計算するプログラムです（以下）

```ruby:calc_expectation.rb
equire './person'

SAMPLE_NUMBER = 10_000

def main
  expectation = 50
  hit_count = 0

  probability = calc_probability(SAMPLE_NUMBER, expectation)

  while probability < 50 do
    puts "確率が#{probability}%なので再計算します（偏差値: #{expectation}）"
    expectation -= 1
    probability = calc_probability(SAMPLE_NUMBER, expectation)
  end

  puts '計算が完了しました'
  puts "期待する偏差値が #{expectation}以上であれば、#{probability}%の確率で存在します"
end

def calc_probability(sample_number, expectation)
  hit_count = 0
  sample_number.times do
    hit_count += 1 if Person.new(expectation).nomal?
  end

  (hit_count.to_f / sample_number.to_f * 100).round(4)
end

main

```
概要にも書いたとおり、試行回数を1万回として、期待する偏差値の人が存在する確率を求め、50%以下であれば期待値を1下げて再度確率を求めています

いずれ存在確率が50%以上になったところで計算をやめるようにしています


補足的に、正規分布を発生させるクラスを載せておきます（以下）

```ruby:rand.rb
class Random
  include Math

  # ボックス―ミューラー法による正規分布乱数発生
  def normal_rand(mu: 0, sigma: 1.0)
    a, b = self.rand(), self.rand()
    (sqrt(-2 * log(a)) * sin(2 * PI * b) * sigma) + mu
  end
end
```
ボックス―ミューラー法なるもので、正規分布に従う乱数を作っています

以下Wikipedia参照
> ボックス＝ミュラー法（ボックス＝ミュラーほう、英: Box–Muller's method）とは、一様分布に従う確率変数から標準正規分布に従う確率変数を生成させる手法。計算機シミュレーションにおいて、正規分布に従う擬似乱数の発生に応用される。統計学者ジョージ・ボックス（英語版）とマーヴィン・マラー（ミュラー）によって考案された。


## 実行結果
スクリプトを実行すると以下の結果が得られました

```bash
$ ruby calc_expectation.rb
確率が1.65%なので再計算します（偏差値: 50）
確率が2.52%なので再計算します（偏差値: 49）
確率が3.88%なので再計算します（偏差値: 48）
確率が5.64%なので再計算します（偏差値: 47）
確率が7.52%なので再計算します（偏差値: 46）
確率が11.28%なので再計算します（偏差値: 45）
確率が14.9%なので再計算します（偏差値: 44）
確率が19.08%なので再計算します（偏差値: 43）
確率が23.47%なので再計算します（偏差値: 42）
確率が29.69%なので再計算します（偏差値: 41）
確率が35.24%なので再計算します（偏差値: 40）
確率が41.68%なので再計算します（偏差値: 39）
確率が48.16%なので再計算します（偏差値: 38）
計算が完了しました
期待する偏差値が 37以上であれば、53.84%の確率で存在します
```

**50%くらいとなると偏差値37~38**あたりという結果となりました
40%でも偏差値39ぐらいですね😂

**つまりステータスが６つの場合であれば、期待する偏差値は37〜40ぐらいになりましょう**


## まとめ

ふつうの人という存在がいかに稀有であるかお分かりいただけたでしょうか？

今回はステータスが６つを仮定しましたが、今回想定していないステータス（例えば体型や趣味・嗜好など）を追加すればするだけ、存在確率は天文学的な確率に近づいていきます🪐

とはいえ頭が良ければ年収が高くなるなど、お互い相関する部分もあるので、実際にはもう少しマシな確率にはなると思いますが・・・

普通の人はたくさんいるだろうという夢から覚めて、現実を見つめる一助となれば幸いです

（Rubyの学習にも一役買ってくれると幸いです）



## 参考
[国税庁の平均給与情報](https://www.nta.go.jp/publication/statistics/kokuzeicho/minkan1997/menu/04.htm)
[ボックス―ミューラー法](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9%EF%BC%9D%E3%83%9F%E3%83%A5%E3%83%A9%E3%83%BC%E6%B3%95)