permutation_test.js

import mean from "./mean.js";
import shuffleInPlace from "./shuffle_in_place.js";

/**
 * 执行[置换检验](https://en.wikipedia.org/wiki/Resampling_(statistics)#Permutation_tests)
 * 用于判断两个数据集是否具有*显著性差异*,使用组间均值差作为检验统计量。
 * 支持以下假设类型:
 * - two_side = 零假设:两个分布相同(双尾检验)
 * - greater = 零假设:样本X观测值通常小于样本Y(右尾检验)
 * - less = 零假设:样本X观测值通常大于样本Y(左尾检验)
 * [了解单尾与双尾检验的区别](https://en.wikipedia.org/wiki/One-_and_two-tailed_tests)
 *
 * @param {Array<number>} sampleX 第一数据集(如实验组)
 * @param {Array<number>} sampleY 第二数据集(如对照组)
 * @param {string} alternative 备择假设类型,可选'two_sided'(默认), 'greater', 或'less'
 * @param {number} k 置换分布生成值数量
 * @param {Function} [randomSource=Math.random] 可选随机源
 * @returns {number} p值 在零假设下观测到当前(或更极端)组间差异的概率
 *
 * @example
 * var control = [2, 5, 3, 6, 7, 2, 5];
 * var treatment = [20, 5, 13, 12, 7, 2, 2];
 * permutationTest(control, treatment); // ~0.1324
 */
function permutationTest(sampleX, sampleY, alternative, k, randomSource) {
    // 参数默认值设置
    if (k === undefined) {
        k = 10000;
    }
    if (alternative === undefined) {
        alternative = "two_side";
    }
    // 参数有效性验证
    if (
        alternative !== "two_side" &&
        alternative !== "greater" &&
        alternative !== "less"
    ) {
        throw new Error("`alternative`参数必须为'two_side', 'greater'或'less'");
    }

    // 计算各样本均值
    const meanX = mean(sampleX);
    const meanY = mean(sampleY);

    // 计算初始检验统计量(后续将与此值比较)
    const testStatistic = meanX - meanY;

    // 创建检验统计量分布存储
    const testStatDsn = new Array(k);

    // 合并数据集以便后续混洗
    const allData = sampleX.concat(sampleY);
    const midIndex = Math.floor(allData.length / 2);

    // 生成置换分布
    for (let i = 0; i < k; i++) {
        // 1. 混洗数据分配
        shuffleInPlace(allData, randomSource);
        const permLeft = allData.slice(0, midIndex);
        const permRight = allData.slice(midIndex, allData.length);

        // 2. 重新计算检验统计量
        const permTestStatistic = mean(permLeft) - mean(permRight);

        // 3. 存储统计量构建分布
        testStatDsn[i] = permTestStatistic;
    }

    // 根据备择假设计算极端统计量数量
    let numExtremeTStats = 0;
    if (alternative === "two_side") {
        // 双尾检验:统计绝对值大于等于观测值的案例
        for (let i = 0; i <= k; i++) {
            if (Math.abs(testStatDsn[i]) >= Math.abs(testStatistic)) {
                numExtremeTStats += 1;
            }
        }
    } else if (alternative === "greater") {
        // 右尾检验:统计大于等于观测值的案例
        for (let i = 0; i <= k; i++) {
            if (testStatDsn[i] >= testStatistic) {
                numExtremeTStats += 1;
            }
        }
    } else {
        // 左尾检验:统计小于等于观测值的案例
        for (let i = 0; i <= k; i++) {
            /* c8 ignore start */
            if (testStatDsn[i] <= testStatistic) {
                numExtremeTStats += 1;
            }
            /* c8 ignore end */
        }
    }

    // 计算p值(极端案例比例)
    return numExtremeTStats / k;
}

export default permutationTest;