/**
* 本函数计算第一个样本相对于第二个样本的[威尔科克森秩和统计量](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;