D3.jsを使って「理想の母集団」を生成する
ここからは、設計した意図を実際にデータへと落とし込んでいきましょう。
本稿のサンプルプログラムでは、画面上のパラメータを操作しながら、実際の分布がどう変化するかをリアルタイムに試行錯誤できるシミュレーター機能を用意しています。数学的な理屈を追うだけでなく、ぜひ手元のツールを動かしながら、「データ」の挙動を体感してみてください。
ベータ分布によるサンプル値の作成
正規分布を基本としつつ、山の頂点(最頻値)を自由にスライドできる統計データを作成するには「ベータ分布」が最適です。ベータ分布の数学的な詳細は本稿の範疇を超えますが、テスト結果をシミュレートする上では「最頻値」と「集中度」(山の鋭さ)を直感的に操れる非常に便利なツールです。
D3.jsには、このベータ分布に基づいた乱数生成器が標準で用意されています。リスト1のように実装することで、複雑な計算を意識することなく、意図通りの分布を持つサンプルデータを作成できます。
/**
* @param modeScore 目標とする最頻値
* @param concentration 集中度(山のとがり具合)
* @param length 作成するサンプル数
*/
betaSamples(modeScore, concentration = 10,length = 1000) {
// (1) 0〜100点のスケールを、ベータ分布が扱う0〜1の範囲に変換
const mode = modeScore / 100;
// (2) 最頻値(mode)から、ベータ分布のパラメータ(alpha, beta)を逆算
// この数式はベータ分布の特性に基づく固定のルールです
const alpha = mode * (concentration - 2) + 1;
const beta = (1 - mode) * (concentration - 2) + 1;
// (3) D3.jsの乱数生成器を作成
const gen = d3.randomBeta(alpha, beta);
// (4) 0〜1の値を出力し、再び100倍して「点数」に戻す
const generator = () => Math.round(gen() * 100);
// (5) 指定されたサンプル数だけ配列を作成して返す
return Array.from({ length: length }, generator);
}
renderChart(ele,mode = 50,sampleLength = 1000,concentration = 10){
const samples = this.betaSamples(mode,concentration,sampleLength);
// (6) 0〜100点それぞれの人数をカウントして、グラフ用データに変換
const counts = d3.rollup(samples, v => v.length, d => d);
const histDataset = d3.range(0, 101).map(score => {
return {
x: score,
y: counts.get(score) || 0
};
});
// (以下省略)
}
(1)~(5)は、ベータ分布を用いたデータを作成する際の手順になります。特に(3)がベータ分布用のデータを作成するための肝となる部分です。また(6)で使用しているd3.rollupは、配列内の重複する要素をグループ化し、一気に集計できる非常に強力な関数です。SQLのGROUP BYでcountをしているイメージになります。
「理論」と「現実」の違い:サンプリングの解像度
このシミュレーションの面白い点は、サンプル数によってグラフの表情が劇的に変わることです。図3は、同じ設定値でサンプル数を1000とした場合と100万とした場合の比較です。
1000サンプルのヒストグラムは、あちこちがデコボコしており、最頻値を求めても「たまたまその点数が多かっただけ」というノイズの影響を強く受けます。学校での例題の規模では、最頻値などが統計的な効果を実感しにくいのはこのためです。
しかし、サンプル数を100万まで引き上げると、グラフは驚くほど滑らかな曲線を描き、設計した「理論上の最頻値」へと完璧に収束していきます。「個人のゆらぎ」が消え、設計思想が非常に正確に姿を現します。
サンプルから各統計値を求める
生成された任意のデータ数から実際に、平均値や中央値、そして偏差値を算出します。D3.jsにはこれらの統計処理を1行で実行できるメソッドが用意されていて、リスト2のように求めることができます。
/**
* 基本統計値を求める
* @param samples データサンプル
* @param targetMode 理論上の最頻値
*/
calcStats(samples,targetMode) {
// (1) 平均値 (Mean): 全データの合計を個数で割る
const mean = d3.mean(samples);
// (2) 中央値 (Median): データを並べた時の真ん中の値
const median = d3.median(samples);
// (3) 分散 (Variance) & 標準偏差 (StdDev)
// 偏差値を計算するための「データのばらつき」を特定する
const variance = d3.variance(samples);
const stdDev = Math.sqrt(variance);
// (4) 実際の最頻値 (Actual Mode)
// 集計データからカウントが最大の点数を特定する
const counts = d3.rollup(samples, v => v.length, d => d);
const modeScoreActual = d3.greatest(counts, d => d[1])[0];
return {
total : samples.length,
mean: mean,
median: median,
mode: modeScoreActual,
// (省略)
};
}
(1)~(3)は、D3.jsが配列を走査して自動的に計算してくれます。(4)では、先ほど作成した「点数ごとの集計データ」の中から、最も人数(v.length)が多い点数をd3.greatestで抽出しています。
