wilcoxon_rank_sum.js

/**
 * 本函数计算第一个样本相对于第二个样本的[威尔科克森秩和统计量](https://en.wikipedia.org/wiki/Mann%E2%80%93Whitney_U_test)。
 * 该检验是t检验的非参数替代方法,等价于曼-惠特尼U检验。计算过程包含以下步骤:
 * 1. 合并两个样本并进行排序
 * 2. 处理相同观测值的并列秩次
 * 3. 计算指定样本的秩和
 *
 * @param {Array<number>} sampleX 第一个样本数值数组
 * @param {Array<number>} sampleY 第二个样本数值数组
 * @returns {number} 样本X的秩和统计量
 * @example
 * wilcoxonRankSum([1, 4, 8], [9, 12, 15]); // => 6
 */
function wilcoxonRankSum(sampleX, sampleY) {
    // 输入有效性验证:两个样本都不能为空
    if (!sampleX.length || !sampleY.length) {
        throw new Error("两个样本都不能为空");
    }

    // 合并样本并添加标签(用于后续区分来源)
    const pooledSamples = sampleX
        .map((x) => ({ label: "x", value: x }))
        .concat(sampleY.map((y) => ({ label: "y", value: y })))
        // 按数值升序排序
        .sort((a, b) => a.value - b.value);

    // 初始分配原始秩次(从0开始)
    for (let rank = 0; rank < pooledSamples.length; rank++) {
        pooledSamples[rank].rank = rank;
    }

    // 处理相同数值的并列秩次(使用平均秩法)
    let tiedRanks = [pooledSamples[0].rank];
    for (let i = 1; i < pooledSamples.length; i++) {
        if (pooledSamples[i].value === pooledSamples[i - 1].value) {
            tiedRanks.push(pooledSamples[i].rank);
            // 处理最后一个元素的边界情况
            if (i === pooledSamples.length - 1) {
                replaceRanksInPlace(pooledSamples, tiedRanks);
            }
        } else if (tiedRanks.length > 1) {
            // 当出现新数值时,处理累积的并列秩次
            replaceRanksInPlace(pooledSamples, tiedRanks);
            tiedRanks = [pooledSamples[i].rank];
        } else {
            tiedRanks = [pooledSamples[i].rank];
        }
    }

    /**
     * 将并列秩次替换为平均秩
     * @param {Array} pooledSamples 合并后的样本数组
     * @param {Array} tiedRanks 需要处理的并列秩索引数组
     */
    function replaceRanksInPlace(pooledSamples, tiedRanks) {
        // 计算最大和最小秩的平均值
        const average = (tiedRanks[0] + tiedRanks[tiedRanks.length - 1]) / 2;
        // 更新所有并列位置的秩值
        for (let i = 0; i < tiedRanks.length; i++) {
            pooledSamples[tiedRanks[i]].rank = average;
        }
    }

    // 计算样本X的秩和(注意JavaScript索引从0开始,需+1调整)
    let rankSum = 0;
    for (let i = 0; i < pooledSamples.length; i++) {
        const sample = pooledSamples[i];
        if (sample.label === "x") {
            rankSum += sample.rank + 1; // 将秩次转换为1-based计算
        }
    }

    return rankSum;
}

export default wilcoxonRankSum;