注目キーワード

秘書問題をpythonで解いてみた(2)/採用した人の評価期待値が一番高いのはどこ?

秘書問題をもうちょっとだけ掘り下げる

 前回のエントリでは、python で秘書問題を解くシミュレーションを実施し、応募者の中でベストな候補者を採用できる確率が約35.5%になったことを書きました。(シミュレーション回数が1000回と少ないので、理論値から見てばらついています。後で10万回シミュレーションしてみたら、約37%前後で安定しました)

関連記事

まじめにコーディング練習  最近、お菓子のレシピや自然薯栽培の記録がメインなエントリでした。  本職のフルスタックデータサイエンティストの片鱗が、欠片すら感じられないブログでしたが、ぼちぼちテクノロジーな香りのするエントリをアップしていこう[…]

 ベストの候補者を探し出すことが最優先ならば、37%前後の確率(=\frac{1}{e})で採用プロセス自体が失敗しようが構わないかもしれませんが、実際の採用プロセスではそうもいきません。採用プロセス自体が失敗して、「今年の採用は諦めて来年にかけましょう!秘書問題で!」なんてセリフを吐くわけにはいかないのです。

 ということで今回は、秘書問題をもう少しだけ掘り下げて、採用した人の評価の期待値が一番高くなるアプローチを、pythonを使ったシミュレーションを通して探してみたいと思います!

おさらい:シミュレーションの定義

 前回のエントリでも触れましたが、秘書問題を扱うシミュレーションにおける前提は次の通りです。

  • 応募者の定義
    • 採用プロセスにおける応募者の人数は100人とする。
    • 各採用候補者との面談において、0~100の間で評価値を決定するものとする。
    • 生物界で良く見られる正規分布を使って、候補者の評価値を正規ランダムに割り当てる。
  • 採用プロセスの定義
    • 一度採用を見送った候補者を、後から採用することはできないものとする。
    • 採用する候補者が決まった時点で、採用プロセスは終了する。
      • 採用した候補者の評価値を、この採用プロセスの評価とする。
    • 一人も採用できなかった場合、採用プロセスは失敗とみなし、便宜上採用プロセスの評価値を0とみなす。

今回の評価基準:評価期待値

 今回は、平均でどれくらい優秀な人を採用できたのかを数値化すべく、評価期待値(μ_{eval})という指標を使います。
 採用プロセスが失敗する場合も成功する場合も含めて、評価値の平均を求めます。

  μ_{eval}=\frac{1}{N_{simu}}\sum_{n=1}^{N_{simu}}{evaluation_{n}}

N_{simu} : シミュレーション回数
evaluation_{n} : n 回目のシミュレーションにおける評価値(成功した場合は、採用した候補者の評価値、失敗した場合は0)

実験

  • 計算から得られる評価期待値の特性曲線を滑らかにするため、シミュレーション回数を10,000回に増やす。
  • ベンチマークとして使う比率を変えながら、評価期待値を計算してゆく。
    • 得られる特性曲線は、μ_{eval}(benchmark_{ratio}) のグラフとなる。
  • 得られる評価期待値が最も良いベンチマーク比率を探す。

Python コード実装!

序盤定義部分

 シミュレーションで使うモジュール、重要定数の定義、実験結果格納変数の準備などをします。
 秘書問題は、numpy だけで計算処理を十分実装できます。

import numpy as np

# 応募者の人数
N = 100

# シミュレーション回数
n_exam = 10000

# 評価値の最大値
max_eval = 100

# 採用結果
# 採用した人間の評価値の履歴
# 採用できなかった場合 0
history = []

np.random.seed(1)

応募者サンプルデータを作る関数

  • 前回と同じ。
  • 応募者数Nと評価値の最大値max_evalを受けとり、応募者のリスト(各候補者の評価値のリスト)を返す関数。
def make_application_sample(N, max_eval):
    """
    応募者数Nと評価値の最大値max_evalを受けとり、応募者のリスト(各候補者の評価値のリスト)を返す
    :param N: 応募者数
    :param max_eval: 評価値の最大値
    """
    sample = np.random.normal(10, 10, N)
    min_sample = np.min(sample)
    max_sample = np.max(sample)
    normalized = (sample - min_sample)/(max_sample - min_sample) * max_eval
    return normalized

秘書問題を解く関数を実装する

 前回と一部仕様が異なっています。ベンチマーク用に割り当てる人数の比率(ベンチマーク比率)の導入です。

  • 応募者数Nと評価値の最大値max_eval、ベンチマーク用に割り当てる人数の比率 ratio_bench,を受けとり、秘書問題を1回解き、結果を返す。
    • 結果は、採用した候補者の評価値である。
      • 採用に失敗した場合は、評価値は 0 となる。
    • ratio_bench の導入が前回との差異である。
def parse_secretary_problem(N, max_eval, ratio_bench=1/np.e, verbose=False):
    """

    :param N: 応募者数
    :param max_eval: 評価値の最大値
    :param ratio_bench: ベンチマーク比率(0.0~1.0)
    :param verbose: デバッグ情報を表示するか?
    :return : 採用結果(候補者の評価値/失敗した場合は 0)
    """
    sample = make_application_sample(N, max_eval)

    # 先頭から N x ratio_bench 人を無条件で採用を見送り、ベンチマーク評価値を作る
    pos_bench = np.int(N*ratio_bench)
    score_bench = np.max(sample[0:pos_bench])
    if verbose:
        print("採用見送り人数: {}".format(pos_bench))
        print("ベンチマークスコア:{}".format(score_bench))
    result = 0
    for _score in sample[pos_bench:]:

        # 面談者の評価値が、ベンチマークを上回ったか?
        if _score >= score_bench:
            # ベンチマークを上回ったら、そこで採用して、選考プロセスを終了する
            result = _score
            break
    return result

Jupyter Notebook上で動作試験をしてみます。
特に問題ありません。

for i in range(10):
    print(parse_secretary_problem(N, max_eval,0.10 , verbose=True))
    print("=" * 70)
# 以下、実行結果

採用見送り人数: 10
ベンチマークスコア:90.17712426832686
98.0985491323
======================================================================
採用見送り人数: 10
ベンチマークスコア:73.7301015845663
74.7223764843
======================================================================
採用見送り人数: 10
ベンチマークスコア:90.59047960429206
100.0
======================================================================
採用見送り人数: 10
ベンチマークスコア:83.39912632981358
100.0
======================================================================
採用見送り人数: 10
ベンチマークスコア:81.57757507688156
100.0
======================================================================
採用見送り人数: 10
ベンチマークスコア:47.450528284825886
65.0219880702
======================================================================
採用見送り人数: 10
ベンチマークスコア:80.45601314069636
82.433141944
======================================================================
採用見送り人数: 10
ベンチマークスコア:68.8992797140362
69.7152821222
======================================================================
採用見送り人数: 10
ベンチマークスコア:89.46786707186655
99.2935702936
======================================================================
採用見送り人数: 10
ベンチマークスコア:100.0
0
======================================================================

実験

  • bench_ratio を変えながら、秘書問題をn_exam回解いて、採用結果を蓄積し、採用結果の統計値を求めます。
  • 1%~99%まで変えながら実験します。
  • 計算処理の進捗を見える化するため、tqdm モジュールを使います。
    • 繰り返し可能なオブジェクトを引数にして、for 制御などで使うのが多く見られる利用方法です。
%matplotlib inline
import seaborn as sns
import pandas as pd
from tqdm import tqdm

# プロット用バッファ
plot_buff = []

for _r in tqdm(range(1,100)):
    bench_ratio = 0.01 * _r
    history = []
    for i in range(n_exam):
        # 秘書問題を解いた結果を格納してゆく
        history.append(parse_secretary_problem(N, max_eval, bench_ratio, verbose=False))
    history = np.array(history)
    #print("bench_ratio={}, mean_eval={}".format(bench_ratio, np.mean(history)))
    plot_buff.append([bench_ratio, # ベンチマーク比率 
                      np.mean(history),  # 評価期待値
                      np.mean(history[history > 0]), # 採用成功時の評価期待値 
                      np.sum(history > 0)/n_exam # 採用の成功率
                    ])
df_plot = pd.DataFrame(plot_buff, columns=['bench_ratio', 
                                           'mean evaluation(total)', 
                                           'mean evaluation(in success)',
                                           'success ratio'])

Jupyter Notebook上で動かすと、以下のように進捗ゲージが埋まっていきます。

100%|██████████████████████████████████████████| 99/99 [01:24<00:00,  1.20it/s]

実験結果を描画する

 今回は、実験結果をグラフ上に描画してレビューします。
 まず、採用失敗時も含めた評価期待値(mean evaluation(total))と、採用に成功した場合の評価期待値(mean evaluation(in success))をプロットしましょう。

import matplotlib.pyplot as plt
sns.set()
plt.figure(figsize=(16,8))
plt.title(r"mean evaluation in recruiting")
plt.xticks([_i * 0.10 for _i in range(11)])
plt.legend()
plot_cols = ['mean evaluation(total)', 'mean evaluation(in success)']
for _c in plot_cols:
    ax = sns.lineplot(x='bench_ratio', y=_c, data=df_plot, label=_c)

# 1/e の所に垂直破線を引く
ax.vlines([1/np.e], 0, 100, color="orange", linestyles='dashed')
評価期待値の傾向
評価期待値の傾向

 採用失敗も考慮した期待評価値の傾向を見ると、ベンチマーク比率を10%ぐらいに抑えた方が賢明であるようです。
 ベンチマーク比率を約37%(=\frac{1}{e})まで引き上げると、評価期待値は60ぐらいなのに対して、10%付近だと、評価期待値が80近辺とハッキリと差が出ていますね。
 
 一方、採用に成功した場合という条件を付けると、ベンチマーク比率を上げるほど評価期待値は高くなる傾向が分かります。ベンチマーク比率を上げるほど、限りなく100に近づきますが、その分だけ容赦なく候補者を切り捨てることになります。
 それは、採用成功率の悪化という形に現れ、結果としてトータルな期待評価値も限りなくゼロに近づくことになります。

 採用成功時の評価期待値と、採用プロセスの成功率の関係をプロットしてみましょう!

sns.set()
plt.figure(figsize=(16,8))
plt.title(r"mean evaluation in recruiting")
plt.xticks([_i * 0.10 for _i in range(11)])
plt.legend()
plot_cols = ['mean evaluation(in success)']
for _c in plot_cols:
    ax = sns.lineplot(x='bench_ratio', y=_c, data=df_plot, label=_c)
ax.set_ylim([0,110])
ax.legend(loc="center left")

# 1/e の所に垂直破線を引く
ax.vlines([1/np.e], 0, 100, color="orange", linestyles='dashed')

ax2 = ax.twinx()
ax2.set_ylim([0,1.1])
_c = 'success ratio'
sns.lineplot(x='bench_ratio', y=_c, data=df_plot, label=_c, ax=ax2, color='orange')
ax2.legend(loc="upper left")
成功時の評価期待値と成功率の関係
成功時の評価期待値と成功率の関係

 採用成功率も一緒にプロットすると、ベンチマーク比率を上げるほどリニアに減少し、採用成功率がゼロに近づくことが分かります。
 ベンチマーク比率を上げるほど採用出来たら抜群に優秀な人を取れるけど、何年採用を続ければ良いのか分からない荊の道を歩むことになりそうです。
 ベンチマーク比率の選び方は、業種・業態・経営方針に沿って決めるべきでしょうね。
 優秀な人をコンスタントに採用したいのか、100年に一人の逸材が一人いれば良いのか、採用戦略の描き方は様々です。

 

レビューまとめ

 ということで、そろそろまとめに入りたいと思います。

  • 全体の10%をベンチマークにして採用基準に使うと、採用した人材の期待評価値が最大になる模様。
    • ただし、採用失敗のペナルティを考慮しての期待値である。
  • 採用に成功した場合という条件付きの評価値ならば、ベンチマークに使う候補者の割合を上げれば上げるほど良くなるが、すぐに飽和している。
    • しかし、ベンチマークに使う候補者の割合を上げるほど採用失敗率はリニアに減少してゆく。
    • 採用失敗のリスクをどれくらい考慮するかによって、戦略が変わる。
  • 採用失敗ペナルティを考慮すると、全体の10%~20%をベンチマークにして、採用候補者を選考するのがベストエフォート戦略になりそうである。
    • 10%~20%のどこを狙うかは、採用戦略の方針に依存する。許容範囲と願望のせめぎ合い。
    • 優秀な人をコンスタントに取りたい、採用プロセスの失敗は避けたい場合を前提としている。

数学問題にPythonでチャレンジするエントリ

 こちらもご覧いただけます!

目次 1 まじめにコーディング練習1.1 秘書問題とは1.1.1 代表的な例1.2 数学的に証明されている最適解1.3 まずは数学的な最適解を確かめる2 実験2.1 応募者サンプルデータを作る関数2.2 自然対数 e を取得する2.3 秘書問題を解く関数を実装する3 実験を実施4 採用結果の期待値( […]

目次 1 秘書問題をもうちょっとだけ掘り下げる2 おさらい:シミュレーションの定義3 今回の評価基準:評価期待値4 実験5 Python コード実装!5.1 序盤定義部分5.2 応募者サンプルデータを作る関数5.3 秘書問題を解く関数を実装する6 実験6.1 実験結果を描画する7 レビューまとめ8 […]

こちらのエントリも併せてどうぞ!

目次 1 まじめにコーディング練習1.1 秘書問題とは1.1.1 代表的な例1.2 数学的に証明されている最適解1.3 まずは数学的な最適解を確かめる2 実験2.1 応募者サンプルデータを作る関数2.2 自然対数 e を取得する2.3 秘書問題を解く関数を実装する3 実験を実施4 採用結果の期待値( […]

目次 1 概要2 命名規則(Naming)2.1 避けるべき名前2.2 命名規則(Naming Conventions)2.2.1 Protected/private メソッド・プロパティ2.2.2 ダンダー(__)を使ったprivate化の注意点2.2.3 モジュール化:クラスや関数のまとめ方2. […]

目次 1 Python でExcelファイルをいじってみたらzip圧縮ファイルだった!2 このエントリを読むと得られるであろうもの3 実験環境についての説明4 今回使用するPythonモジュールを読み込む5 xlsxファイルをzipファイルとして読み込んでみる6 zipファイルのアーカイブからxml […]

目次 1 Pythonでtar.gzアーカイブを扱う方法2 Python の tarfile モジュール3 tarfile モジュールのインポート4 tar.gz アーカイブを読み込む5 tar.gz アーカイブを出力するコード6 まとめ等 Pythonでtar.gzアーカイブを扱う方法  備忘録& […]

目次 1 Apache Beam チュートリアル2 Apache Beam 概要2.0.1 (補足) Apache Beam のリリース状況3 概念3.1 Runner(実行環境)3.2 パイプライン処理を構成する概念4 基本的なパイプラインの開発の流れ4.1 パイプライン処理内の各工程の詳細5 動 […]

目次 1 長期保有シミュレーション1.1 背景2 シミュレーションの準備2.1 データファイルの設置場所3 シミュレーション処理3.1 使用するモジュール群の読み込み3.2 視覚化処理の設定3.3 価格データの読み込みと前処理3.3.1 前処理3.3.1.1 カンマ区切りのOHLCの処理3.3.1. […]

目次 1 概要2 Anacondaで開発環境を構築2.1 自然言語処理用の開発環境を conda で構築3 mecab 本体のインストール4 neologd のインストール&設定4.1 ビルド環境の整備4.2 neologd のインストール4.3 mecab の辞書を neologd に変更する4. […]

目次 1 概要2 感染フェーズの数値化とは2.1 Phase Position の分布から分かること2.2 Phase Position 上位国の顔ぶれ3 序盤から感染爆発に襲われた欧米先進国4 感染者の重症化率から分かること4.1 重症化率の高い国は医療崩壊のリスクに晒されている5 医療崩壊リスク […]

目次 1 秘書問題をもうちょっとだけ掘り下げる2 おさらい:シミュレーションの定義3 今回の評価基準:評価期待値4 実験5 Python コード実装!5.1 序盤定義部分5.2 応募者サンプルデータを作る関数5.3 秘書問題を解く関数を実装する6 実験6.1 実験結果を描画する7 レビューまとめ8 […]

目次 1 新型コロナ・致死率の時系列推移を分析した結果分かったこと1.1 新型コロナのニュースで頻繁に見かける国々と日本の比較:1.2 ヨーロッパにおける致死率の二極化:1.3 新型コロナ対策に成功している国?での比較 新型コロナ・致死率の時系列推移を分析した結果分かったこと  先日から使い始めたジ […]

最新情報をチェックしよう!