範囲と分散・標準偏差の比較で二峰性を検知する
データから「二峰性」の兆候を数値で察知するために、ここでは「範囲(レンジ)」と「分散」、「標準偏差」という、性質の異なる指標を用います。
- 範囲(Range):データ内の最大値(Max)から最小値(Min)を引いたもの。これは、データの「端から端までの単純な広がり」を示します。ただし、たった1人でも極端な点数を取る人がいるだけで数値が大きく変動してしまう弱点があるため、実務ではあくまで「大体の目安」として使われます。
- 分散(Variance):各データの値から平均値を引いた「偏差」を2乗し、それらを合計してサンプル数で割ったもの。データの「平均的な散らばり具合」を表します。
- 標準偏差(Standard Deviation):分散の平方根(ルート)をとったもの。2乗によって変化した単位を元のデータの単位(点数など)に戻し、「平均から±何点離れているか」という、より直感的に理解しやすい距離として扱えるようにした指標です。
学校の授業では、これらは単に「データの散らばり具合を表す2つのアプローチ」として並列に習うことが大半でしょう。しかし、データサイエンスの視点に立つと、この2つの指標を比較することが、二峰性を見抜くための鍵となります。
分布の形状変化が標準偏差に与える影響
100点満点のテスト(範囲:0点~100点=100点)を想定し、分布の形状によって散らばりの指標、特に標準偏差がどのように変化するかを比較してみましょう。
綺麗な左右対称の単峰分布(正規分布など)では、おおよそデータの大部分(約95%)が平均値を中心とした標準偏差の4倍程度の枠内に収まる傾向があります。そのため、100点満点の試験であれば、標準偏差は15点~20点くらい(範囲の4分の1~5分の1程度)に落ち着くケースが多く見られます。
一方、同じ「範囲=100点」という条件のまま、データが30点(初心者層)と80点(経験者層)の2つの山に分裂し、平均値である50点付近が谷になった場合はどうなるでしょうか。 最大値と最小値しか見ない「範囲」は100点のまま変わりませんが、標準偏差の数値は大きく跳ね上がります。
これは、分散や標準偏差の計算式が、平均値(50点)から離れれば離れるほど、そのズレ(偏差)を「2乗」したペナルティとして蓄積するためです。 30点の山も80点の山も、平均値から20点~30点近く離れているため、その2乗(400~900)のインパクトが計算にダイレクトに反映されます。その結果、中央が空洞化したデータでは、標準偏差が「20点~30点以上」へと上昇します。
本稿のサンプルシミュレーターでも、標準偏差の数値が20を超えるあたりから山が分かれていく様子が確認できるはずです(図4参照)。
D3による二峰性データのシミュレーション
実際に2つの異なる山を掛け合わせた「混合分布」を生成するシミュレーターの実装を見てみましょう。
ここでは、前回作成したベータ分布の乱数生成器(betaSamples)を再利用し、指定した混合比率(ウェイト)に基づいて2つの集団を1つの配列にマージするプログラム(リスト1)を作成します。
export default class BasicContainer extends GrindController {
// (省略)
/**
2つの異なる分布を混ぜ合わせたサンプルデータを作成する
@param {Object} config1 集団1の設定(最頻値、集中度、比率)
@param {Object} config2 集団2の設定(最頻値、集中度、比率)
@param {number} totalLength 総サンプル数
*/
createMixtureSamples(config1, config2, totalLength = 1000) {
// (1) 混合比率(ウエイト)から、それぞれの集団の正確なサンプル数を逆算
// 例:config1.ratio が 0.6 なら、1000人中 600人が集団1になる
const len1 = Math.round(totalLength * config1.ratio);
const len2 = totalLength - len1;
// (2) 前回、作成したベータ分布ロジックを呼び出し、それぞれの実データ(配列)を生成
// config.modeScore: 目標とする最頻値(山の頂点)
// config.concentration: 山の鋭さ(集中度)
const samples1 = this.betaSamples(config1.modeScore, config1.concentration, len1);
const samples2 = this.betaSamples(config2.modeScore, config2.concentration, len2);
// (3) 1つの大きな配列にマージし、配列の要素をランダムに並び替える
const mixtureSamples = [...samples1, ...samples2];
return d3.shuffle(mixtureSamples);
}
/**
* 異なる2つの母集団のデータを要求する
* @param mode1 1つめの母集団の最頻値(山の頂点)
* @param mode2 2つめの母集団の最頻値(山の頂点)
* @param sampleLength 観測データのサンプル数
* @returns {*}
*/
mixtureSamples(mode1 = 30,mode2 = 80,sampleLength) {
// (4) 集団1(初心者層):30点付近に鋭い山がある(全体の60%)
const config1 = {modeScore: mode1, concentration: 15, ratio: 0.6};
// (5) 集団2(経験者層):80点付近に広い山がある(全体の40%)
const config2 = {modeScore: mode2, concentration: 8, ratio: 0.4};
return this.createMixtureSamples(config1, config2, sampleLength);
}
// (省略)
}
このプログラムでは、現実の業務で向き合うことになる「特徴が混ざったデータ」を再現するための処理を行っています。
まずメソッドの起点となる(4)と(5)では、2つの母集団の具体的な条件を定義しています。ここでは30点付近に全体の60%が集中する鋭い山(初心者層)と、80点付近に全体の40%がなだらかに集まる山(経験者層)という2つのパラメータを設定しています。
そのデータの詳細を作るのが、 createMixtureSamplesメソッドで、(1)と(2)のステップにおいて、指定された総サンプル数からそれぞれの構成比率に応じた正確な人数を逆算し、前回作成したベータ分布の関数を用いて初心者層と経験者層の点数データを個別に生成します。
最後に(3)で、個別に作られた2つの集団を1つの配列へと結合し、そのままでは前半と後半でデータが綺麗に分かれてしまっているため、D3のシャッフル関数を用いて要素の順序をランダムに並び替えます。これにより、「どちらの集団に属していたか」という元の特徴がわからないようなデータが完成します。
