ckmeans.js

import makeMatrix from "./make_matrix.js";
import numericSort from "./numeric_sort.js";
import uniqueCountSorted from "./unique_count_sorted.js";

/**
 * 基于数据数组的累积和及平方累积和,生成增量计算值
 *
 * @private
 * @param {number} j 起始索引
 * @param {number} i 结束索引
 * @param {Array<number>} sums 累积和数组
 * @param {Array<number>} sumsOfSquares 平方累积和数组
 * @return {number} 计算得到的平方偏差值
 * @example
 * ssq(0, 1, [-1, 0, 2], [1, 1, 5]);
 */
function ssq(j, i, sums, sumsOfSquares) {
    let sji; // s(j, i) 表示从j到i的平方偏差和
    if (j > 0) {
        const muji = (sums[i] - sums[j - 1]) / (i - j + 1); // μ(j, i) 表示j到i的均值
        sji =
            sumsOfSquares[i] - sumsOfSquares[j - 1] - (i - j + 1) * muji * muji;
    } else {
        sji = sumsOfSquares[i] - (sums[i] * sums[i]) / (i + 1);
    }
    if (sji < 0) {
        return 0;
    }
    return sji;
}

/**
 * 递归执行分治策略计算指定簇的矩阵列
 *
 * @private
 * @param {number} iMin 当前簇计算的最小索引
 * @param {number} iMax 当前簇计算的最大索引
 * @param {number} cluster 当前计算的簇索引
 * @param {Array<Array<number>>} matrix 动态规划矩阵
 * @param {Array<Array<number>>} backtrackMatrix 回溯矩阵
 * @param {Array<number>} sums 累积和数组
 * @param {Array<number>} sumsOfSquares 平方累积和数组
 */
function fillMatrixColumn(
    iMin,
    iMax,
    cluster,
    matrix,
    backtrackMatrix,
    sums,
    sumsOfSquares
) {
    if (iMin > iMax) {
        return;
    }

    // 选取iMin和iMax之间的中点作为当前计算位置
    const i = Math.floor((iMin + iMax) / 2);

    matrix[cluster][i] = matrix[cluster - 1][i - 1];
    backtrackMatrix[cluster][i] = i;

    let jlow = cluster; // j的下界初始值
    if (iMin > cluster) {
        jlow = Math.max(jlow, backtrackMatrix[cluster][iMin - 1] || 0);
    }
    jlow = Math.max(jlow, backtrackMatrix[cluster - 1][i] || 0);

    let jhigh = i - 1; // j的上界初始值
    if (iMax < matrix[0].length - 1) {
        /* c8 ignore start */
        jhigh = Math.min(jhigh, backtrackMatrix[cluster][iMax + 1] || 0);
        /* c8 ignore end */
    }

    let sji;
    let sjlowi;
    let ssqjlow;
    let ssqj;
    for (let j = jhigh; j >= jlow; --j) {
        sji = ssq(j, i, sums, sumsOfSquares);

        if (sji + matrix[cluster - 1][jlow - 1] >= matrix[cluster][i]) {
            break;
        }

        // 计算簇边界的下界
        sjlowi = ssq(jlow, i, sums, sumsOfSquares);
        ssqjlow = sjlowi + matrix[cluster - 1][jlow - 1];

        if (ssqjlow < matrix[cluster][i]) {
            matrix[cluster][i] = ssqjlow;
            backtrackMatrix[cluster][i] = jlow;
        }
        jlow++;

        ssqj = sji + matrix[cluster - 1][j - 1];
        if (ssqj < matrix[cluster][i]) {
            matrix[cluster][i] = ssqj;
            backtrackMatrix[cluster][i] = j;
        }
    }

    // 递归处理左右子区间
    fillMatrixColumn(
        iMin,
        i - 1,
        cluster,
        matrix,
        backtrackMatrix,
        sums,
        sumsOfSquares
    );
    fillMatrixColumn(
        i + 1,
        iMax,
        cluster,
        matrix,
        backtrackMatrix,
        sums,
        sumsOfSquares
    );
}

/**
 * 初始化Ckmeans算法的主矩阵并启动分治聚类计算策略
 *
 * @private
 * @param {Array<number>} data 已排序的数值数组
 * @param {Array<Array<number>>} matrix 动态规划矩阵
 * @param {Array<Array<number>>} backtrackMatrix 回溯矩阵
 */
function fillMatrices(data, matrix, backtrackMatrix) {
    const nValues = matrix[0].length;

    // 通过中值平移提升数值稳定性
    const shift = data[Math.floor(nValues / 2)];

    // 计算平移后的累积和与平方累积和
    const sums = [];
    const sumsOfSquares = [];

    // 初始化矩阵和回溯矩阵的第一列(簇数为1时)
    for (let i = 0, shiftedValue; i < nValues; ++i) {
        shiftedValue = data[i] - shift;
        if (i === 0) {
            sums.push(shiftedValue);
            sumsOfSquares.push(shiftedValue * shiftedValue);
        } else {
            sums.push(sums[i - 1] + shiftedValue);
            sumsOfSquares.push(
                sumsOfSquares[i - 1] + shiftedValue * shiftedValue
            );
        }

        // 初始化簇数为0时的状态
        matrix[0][i] = ssq(0, i, sums, sumsOfSquares);
        backtrackMatrix[0][i] = 0;
    }

    // 初始化其他簇数的矩阵列
    let iMin;
    for (let cluster = 1; cluster < matrix.length; ++cluster) {
        if (cluster < matrix.length - 1) {
            iMin = cluster;
        } else {
            // 最后一列只需计算最后一个元素
            iMin = nValues - 1;
        }

        fillMatrixColumn(
            iMin,
            nValues - 1,
            cluster,
            matrix,
            backtrackMatrix,
            sums,
            sumsOfSquares
        );
    }
}

/**
 * Ckmeans聚类算法是一种基于动态规划的改进聚类方法,相比启发式的Jenks算法更为优化。
 * 该算法由[Haizhou Wang和Mingzhou Song](http://journal.r-project.org/archive/2011-2/RJournal_2011-2_Wang+Song.pdf)
 * 提出,通过最小化组内平方偏差和来实现数值数据的最优分组。
 *
 * 该实现基于Ckmeans 3.4.6版本,采用分治法将时间复杂度从O(kn²)优化至O(kn log(n))。
 *
 * ### 核心特性
 * - 动态规划构建代价矩阵和回溯矩阵
 * - 数值稳定性优化:通过中值平移技术
 * - 分治策略加速计算过程
 *
 * ### 参考文献
 * 《Ckmeans.1d.dp: Optimal k-means Clustering in One Dimension by Dynamic Programming》
 * 作者:Haizhou Wang和Mingzhou Song,刊于The R Journal Vol. 3/2, December 2011
 *
 * @param {Array<number>} x 输入数据集(数值数组)
 * @param {number} nClusters 期望的聚类数目(需小于等于数据长度)
 * @returns {Array<Array<number>>} 分组后的聚类结果
 * @throws {Error} 当请求聚类数大于数据长度时抛出异常
 * @example
 * ckmeans([-1, 2, -1, 2, 4, 5, 6, -1, 2, -1], 3);
 * // 返回:[[-1, -1, -1, -1], [2, 2, 2], [4, 5, 6]]
 */
function ckmeans(x, nClusters) {
    if (nClusters > x.length) {
        throw new Error("聚类数目不能超过数据元素个数");
    }

    const sorted = numericSort(x);
    const uniqueCount = uniqueCountSorted(sorted);

    // 处理全同数据情况
    if (uniqueCount === 1) {
        return [sorted];
    }

    const matrix = makeMatrix(nClusters, sorted.length);
    const backtrackMatrix = makeMatrix(nClusters, sorted.length);

    // 核心计算过程:填充动态规划矩阵和回溯矩阵
    fillMatrices(sorted, matrix, backtrackMatrix);

    // 通过回溯矩阵重建最优聚类分组
    const clusters = [];
    let clusterRight = backtrackMatrix[0].length - 1;

    // 从矩阵右下角开始逆向回溯
    for (let cluster = backtrackMatrix.length - 1; cluster >= 0; cluster--) {
        const clusterLeft = backtrackMatrix[cluster][clusterRight];
        clusters[cluster] = sorted.slice(clusterLeft, clusterRight + 1);

        if (cluster > 0) {
            clusterRight = clusterLeft - 1;
        }
    }

    return clusters;
}

export default ckmeans;