SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

データサイエンス基礎を高校数学から復習

分布の「山」が割れるとき。混在するクラスタを解き明かす「混合分布」のモデリング

データサイエンス基礎を高校数学から復習しよう 第8回

分散の最小化による母集団の分離アルゴリズム

 混ざり合ってしまった2つの集団を、統計学的に最も妥当な境界線(しきい値)で切り分けるにはどうすればよいでしょうか。

 まず、データ全体の散らばりを表す「グループ内分散」という指標に着目し、コンピュータのループ処理を用いて最適な閾値を自動的に探索するアルゴリズムの論理と、実際の実装コード、およびその計算結果について順を追って解説します。

分離アルゴリズムの3つのステップ

 2つの集団が混ざっているときは全体の分散が大きく膨らんでいますが、もし最適な境界線で2つの集団に分離することができれば、分割後の各集団はそれぞれ本来の単峰分布に戻り、内部の分散(散らばり)は最小化されるはずです。

 この「分割後における各グループ内の分散の小ささ」を指標にすることで、データを適切に分離するアルゴリズムを構築できます。具体的には、境界線の位置を1点ずつ動かしながら、以下の3つのステップをループ処理で計算していきます。

  • ステップ1(データの分割):仮の境界線(例:50点など)を設定し、データを「境界線未満のグループ」と「境界線以上のグループ」の2つに分割します。
  • ステップ2(グループ別分散の算出):分割された2つのグループそれぞれにおいて、個別に分散(平均からのズレの2乗の平均)を計算します。
  • ステップ3(人数比による加重平均):それぞれのグループの人数(サンプルの重み)を考慮して、2つの分散を足し合わせます。これを統計学では「グループ内分散」と呼びます。

境界線探索プログラムの実装

 この一連の探索手順を実装したのが、以下のコードです。最小点から最高点まで1点刻みで境界線を動かし、グループ内分散が最も小さくなったポイントを最適解として決定します。

[リスト2]グループ内分散が最小となる最適な境界線を探索する(js/sample/MixtureContainer.jsの抜粋)
export default class BasicContainer extends GrindController {
    // (省略)
    /**
     * 混ざったデータから、グループ内分散が最小となる最適な境界線(カットライン)を探索する
     * @param {number[]} samples 2つの山が混ざったスコアデータの配列
     * @param {number} minScore 探索を開始する最低点(例: 0)
     * @param {number} maxScore 探索を終了する最高点(例: 100)
     * @returns {Object} 最適な境界線と、それによって分割された2つのグループのデータ
     */
    findOptimalThreshold(samples, minScore = 0, maxScore = 100) {
        let minWithinVariance = Infinity; // 最小のグループ内分散を記録する変数(初期値は無限大)
        let bestThreshold = -1;           // 最適な境界線を記録する変数

        // (1) 最小点から最高点まで、1点刻みで境界線の位置を動かして総当たり戦を行う
        // ※端すぎると分割できないため、実質的な探索範囲(例: 1〜99)でループ
        for (let t = minScore + 1; t < maxScore; t++) {

            // (2) 仮の境界線「t」でデータを2つのグループにぶった切る
            const group1 = samples.filter(score => score < t);
            const group2 = samples.filter(score => score >= t);

            // どちらかのグループが空になってしまった場合はスキップ
            if (group1.length === 0 || group2.length === 0) continue;

            // (3) 分割された2つのグループそれぞれで、教科書通りの「分散」を個別に計算する
            const var1 = d3.variance(group1) || 0;
            const var2 = d3.variance(group2) || 0;

            // (4) 2つのグループの人数比(ウエイト)を考慮して、分散をブレンド(加重平均)する
            // これが統計学でいう「グループ内分散(内分散)」
            const n1 = group1.length;
            const n2 = group2.length;
            const total = n1 + n2;
            const withinVariance = (n1 * var1 + n2 * var2) / total;

            // (5) もし計算したグループ内分散が、これまでの記録(最小値)を塗り替えたら
            // その時の「境界線」と「分散の低さ」を最新レコードとして記憶する
            if (withinVariance < minWithinVariance) {
                minWithinVariance = withinVariance;
                bestThreshold = t;
            }
        }

        // (6) 最終的に勝ち残った「最も身内のバラつきをギュッと引き締めた境界線」で本分割する
        const finalGroup1 = samples.filter(score => score < bestThreshold);
        const finalGroup2 = samples.filter(score => score >= bestThreshold);

        return {
            threshold: bestThreshold,      // 割り出された境界線
            withinVariance: minWithinVariance, // その時の最小グループ内分散
            group1: finalGroup1,          // 分割された集団1(初心者層)
            group2: finalGroup2           // 分割された集団2(経験者層)
        };
    }
    //  (省略)
}

 コード内では、先ほど解説した3つのステップが以下のように実行されています。

 ステップ1(データの分割)は、ループ処理内の(2)で実行されています。変数tを仮の境界線として、filterメソッドにより境界線未満(group1)と境界線以上(group2)の2つの配列へと機械的に分割しています。

 ステップ2(グループ別分散の算出)は(3)に対応しています。D3のd3.variance関数を呼び出すことで、分割された2つのグループそれぞれの分散(var1とvar2)を個別に算出しています。

 ステップ3(人数比による加重平均)は(4)で計算されています。各グループのデータ数(n1とn2)を重みとして掛け合わせ、全体のサンプル数で割ることで、目的指標である「グループ内分散(withinVariance)」を導き出しています。

 この手順を(1)でひたすら繰り返す中で(5)の条件分岐によりグループ内分散が「最も小さくなった瞬間」の境界線の位置を記憶し、最終的に(6)でその最適な閾値を用いた本分割を行っています。

分離アルゴリズムの計算結果

 この探索プログラムを実行し、グループ内分散が最も小さくなる閾値を求めた結果が図5です。

図5:2つの集団を分割した計算結果
図5:2つの集団を分割した計算結果

 実際の計算結果は乱数の影響により理論値から多少の前後が生じますが、ヒストグラムの谷底(今回のサンプルでは50点付近)のデータが最も薄い位置に、自動的に境界線が設定される様子が確認できます。統計学的には、グループ内のバラつきを最小にすることは、グループ同士の離れ具合である「グループ間分散」を最大にすることと同じ意味になるので、この表現の方が分かりやすい方もいるでしょう。

 実は、この「グループ内分散を最小化する位置を総当たりで探す」というアプローチは「大津の二値化(判別分析法)」として有名なアルゴリズムそのものです。画像認識に携わったことがある方であれば、聞いたことがあると思います。

この検知手法の前提と実務における限界

 ここまで、範囲と標準偏差の関係から分布の二峰性を察知するアプローチを解説してきましたが、一つ重要な注意点があります。

 現実のデータ分析において「標準偏差が大きければ、必ず二峰性分布である」と言い切ることはできません。

 実際の実務データはさらに複雑であり、例えば「山が3つ以上ある(多峰性)」ケースや、山が割れていなくても全体が均等にスカスカな「一様分布」のケースでも、標準偏差は同じように大きな値を示します。そのため、標準偏差の数値単体から機械的に「二峰性である」と100%断定することは不可能です。

 しかし、このアプローチを知っておくことには実務上大きな意味があります。

 例えば、毎日計測している定常データにおいて、「範囲(データの端から端)」が変わらないにもかかわらず、ある日突然、標準偏差の数値だけが不自然に跳ね上がったとします。このとき、この手法を理解していれば、「母集団の中で何かしらの異変が起き、データが複数のクラスタに分裂し始めているのではないか」というアタリ(仮説)を素早く立てることができます。

 このように、数値の背後で起きている「山の分裂(混合分布)」という特徴を捉え、その構造を予測するための強力なアプローチとして、この散らばりの指標の比較は極めて有効なのです。

次のページ
AI時代に私たちが「高校数学」を復習する意義

この記事は参考になりましたか?

データサイエンス基礎を高校数学から復習連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

< WINGSプロジェクト について> 有限会社 WINGSプロジェクト が運営する、テクニカル執筆コミュニティ(代表 ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/29429 2026/09/10 08:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー