如何在JavaScript(或PHP)中获取数组的中位数、四分位数/百分位数?

35
这个问题变成了一个问答,因为我曾经苦苦寻找答案,并认为它对其他人有用。
我有一个JavaScript值数组,需要在JavaScript中计算它的Q2(第50个百分位数,即中位数),Q1(第25个百分位数)和Q3(第75个百分位数)值。 enter image description here

4个回答

60

我更新了第一个答案中的JavaScript翻译,使用箭头函数和更简洁的符号。功能大体相同,除了 std,它现在计算样本标准差(通过将除以 arr.length - 1 而不是仅除以 arr.length

// sort array ascending
const asc = arr => arr.sort((a, b) => a - b);

const sum = arr => arr.reduce((a, b) => a + b, 0);

const mean = arr => sum(arr) / arr.length;

// sample standard deviation
const std = (arr) => {
    const mu = mean(arr);
    const diffArr = arr.map(a => (a - mu) ** 2);
    return Math.sqrt(sum(diffArr) / (arr.length - 1));
};

const quantile = (arr, q) => {
    const sorted = asc(arr);
    const pos = (sorted.length - 1) * q;
    const base = Math.floor(pos);
    const rest = pos - base;
    if (sorted[base + 1] !== undefined) {
        return sorted[base] + rest * (sorted[base + 1] - sorted[base]);
    } else {
        return sorted[base];
    }
};

const q25 = arr => quantile(arr, .25);

const q50 = arr => quantile(arr, .50);

const q75 = arr => quantile(arr, .75);

const median = arr => q50(arr);

1
我们为什么需要标准差? - Andre Miras
1
这不是计算中位数和分位数所必需的 - 它只是作为额外奖励包含在内 ;) - buboh
我已经使用这个一段时间了,但我注意到在较小的数组中有一些奇怪的事情。例如,数组[56, 571, 580, 887]返回442.25作为q25,在箱线图中应该是Q1,然而Q1是313.5。我认为return sorted[base] + rest * (sorted[base + 1] - sorted[base]);应该改成return (sorted[base] + sorted[base + 1]) / 2; - Jimmy Knoot
@JimmyKnoot 很不幸,实际上有很多计算分位数的方法(例如在Wikipedia中提到的)。我尝试了一些在线计算器,得到了几个不同的结果,但没有得到442.25。我还尝试了Python Pandas库,它显然使用了“我的”方法,因为它返回了442.25 - buboh
@JimmyKnoot 我转换的D3版本如下,也给出了442.25作为第一四分位数。var x = quantileSorted([56, 571, 580, 887], .25); console.log(x);输出442.25。我认为你犯了和我在那里描述的“常识”方法相同的错误--请参考特别是这个stats.stackexchange的答案。还请参考“_分数(0.25)表示除了值5之外,还添加了5和6之间距离的四分之一。因此,Q1为5 + 0.25 * 2 = 5.5._” - undefined

23

在长时间的搜索中,我找到了不同版本的代码,得出了不同的结果。最终,在Bastian Pöttner的博客上找到了这个非常好的PHP代码片段。使用这个代码片段,我们可以获取数据的平均值标准差(适用于正态分布)...

PHP版本

//from https://blog.poettner.de/2011/06/09/simple-statistics-with-php/

function Median($Array) {
  return Quartile_50($Array);
}

function Quartile_25($Array) {
  return Quartile($Array, 0.25);
}

function Quartile_50($Array) {
  return Quartile($Array, 0.5);
}

function Quartile_75($Array) {
  return Quartile($Array, 0.75);
}

function Quartile($Array, $Quartile) {
  sort($Array);
  $pos = (count($Array) - 1) * $Quartile;

  $base = floor($pos);
  $rest = $pos - $base;

  if( isset($Array[$base+1]) ) {
    return $Array[$base] + $rest * ($Array[$base+1] - $Array[$base]);
  } else {
    return $Array[$base];
  }
}

function Average($Array) {
  return array_sum($Array) / count($Array);
}

function StdDev($Array) {
  if( count($Array) < 2 ) {
    return;
  }

  $avg = Average($Array);

  $sum = 0;
  foreach($Array as $value) {
    $sum += pow($value - $avg, 2);
  }

  return sqrt((1 / (count($Array) - 1)) * $sum);
}

根据作者的评论,我简单地编写了一个JavaScript翻译,这肯定会很有用,因为令人惊讶的是,在网络上几乎不可能找到JavaScript等效物,否则需要额外的库,如Math.js。

JavaScript版本

//adapted from https://blog.poettner.de/2011/06/09/simple-statistics-with-php/
function Median(data) {
  return Quartile_50(data);
}

function Quartile_25(data) {
  return Quartile(data, 0.25);
}

function Quartile_50(data) {
  return Quartile(data, 0.5);
}

function Quartile_75(data) {
  return Quartile(data, 0.75);
}

function Quartile(data, q) {
  data=Array_Sort_Numbers(data);
  var pos = ((data.length) - 1) * q;
  var base = Math.floor(pos);
  var rest = pos - base;
  if( (data[base+1]!==undefined) ) {
    return data[base] + rest * (data[base+1] - data[base]);
  } else {
    return data[base];
  }
}

function Array_Sort_Numbers(inputarray){
  return inputarray.sort(function(a, b) {
    return a - b;
  });
}

function Array_Sum(t){
   return t.reduce(function(a, b) { return a + b; }, 0); 
}

function Array_Average(data) {
  return Array_Sum(data) / data.length;
}

function Array_Stdev(tab){
   var i,j,total = 0, mean = 0, diffSqredArr = [];
   for(i=0;i<tab.length;i+=1){
       total+=tab[i];
   }
   mean = total/tab.length;
   for(j=0;j<tab.length;j+=1){
       diffSqredArr.push(Math.pow((tab[j]-mean),2));
   }
   return (Math.sqrt(diffSqredArr.reduce(function(firstEl, nextEl){
            return firstEl + nextEl;
          })/tab.length));  
}

1
也许你可以遵循这样的约定,对于那些不是构造函数的函数,使用小写字母作为名称的首字母。 - Nina Scholz
如果将“rest”重命名为“sawtooth”,那么代码可能更容易理解,因为它代表的是正弦锯齿函数。 - onigame

9

简短概述

其他答案似乎已经给出了计算分位数的"R-7"版本的可靠实现。下面是一些背景知识和另一个JavaScript实现,借鉴自D3,使用相同的R-7方法,而且这个解决方案有额外的好处,它符合es5标准(不需要JavaScript转换),可能还涵盖了一些更多的边缘情况。


D3的现有解决方案(移植到es5 /“vanilla JS”)

下面的“一些背景”部分应该说服您抓取现有实现而不是编写自己的实现。

一个很好的候选者是D3d3.array包。它有一个分位数函数,基本上是BSD许可证

https://github.com/d3/d3-array/blob/master/src/quantile.js

我已经快速地将d3的quantileSorted函数(该文件中定义的第二个函数)从es6转换为了纯JavaScript,需要注意的是数组元素必须已经排序。这是代码。我已经对它进行了足够的测试,感觉它是一个有效的转换,但你的经验可能会有所不同(如果你发现有差异,请在评论中让我知道!):

再次提醒,排序必须在调用此函数之前进行,就像D3的quantileSorted一样。

  //Credit D3: https://github.com/d3/d3-array/blob/master/LICENSE
  function quantileSorted(values, p, fnValueFrom) {
    var n = values.length;
    if (!n) {
      return;
    }

    fnValueFrom =
      Object.prototype.toString.call(fnValueFrom) == "[object Function]"
        ? fnValueFrom
        : function (x) {
            return x;
          };

    p = +p;

    if (p <= 0 || n < 2) {
      return +fnValueFrom(values[0], 0, values);
    }

    if (p >= 1) {
      return +fnValueFrom(values[n - 1], n - 1, values);
    }

    var i = (n - 1) * p,
      i0 = Math.floor(i),
      value0 = +fnValueFrom(values[i0], i0, values),
      value1 = +fnValueFrom(values[i0 + 1], i0 + 1, values);

    return value0 + (value1 - value0) * (i - i0);
  }

请注意,fnValueFrom 是将复杂对象处理为值的一种方式。您可以在这里查看d3使用示例列表,搜索使用了.quantile的位置,以了解其工作原理。
简单来说,如果values是乌龟,而你在每种情况下都按tortoise.age排序,那么你的fnValueFrom可能是x => x.age。更复杂的版本,包括可能需要在值计算期间访问索引(参数2)和整个集合(参数3)的版本,留给读者自行决定。
我在这里添加了一个快速检查,以便如果没有为fnValueFrom给出任何内容,或者给出的不是函数,则逻辑会假定values中的元素是实际排序后的值本身。

对现有答案进行逻辑比较

我相当确定这与其他两个答案中的“R-7方法”版本相同,但如果您需要向产品经理等人证明为什么要使用此方法,下面的内容可能会有所帮助。

快速比较:

function Quartile(data, q) {
  data=Array_Sort_Numbers(data);        // we're assuming it's already sorted, above, vs. the function use here. same difference.
  var pos = ((data.length) - 1) * q;    // i = (n - 1) * p
  var base = Math.floor(pos);           // i0 = Math.floor(i)
  var rest = pos - base;                // (i - i0);
  if( (data[base+1]!==undefined) ) {
    //      value0    + (i - i0)   * (value1 which is values[i0+1] - value0 which is values[i0])
    return data[base] + rest       * (data[base+1]                 - data[base]);
  } else {
    // I think this is covered by if (p <= 0 || n < 2)
    return data[base];
  }
}

因此,逻辑上接近/看起来完全相同。我认为我移植的d3版本涵盖了一些更多的边缘/无效条件,并包括fnValueFrom集成,这两者都可能很有用。


R-7方法 vs.“常识”

如TL;DR中所述,根据d3.array的自述文件,这里的答案都使用“R-7方法”。

这个特定的实现[d3]使用R-7方法,这是R编程语言和Excel的默认方法。

由于d3.array代码与其他答案匹配,我们可以安全地说它们都在使用R-7。


背景

在一些数学和统计的StackExchange网站上进行了一些调查后(1, 2),我发现有“常识性”的方法可以计算每个分位数,但这些方法通常与通常认可的九种计算方法的结果不匹配。

来自stats.stackexchange的第二个链接中的答案表示...

你的教科书是错误的。很少有人或软件以这种方式定义四分位数。(这往往会使第一四分位数太小,第三四分位数太大。)

R中的quantile函数实现了九种不同的计算分位数的方法!

我认为最后一句话很有趣,以下是我找到的关于这九种方法的资料...

使用d3的“方法7”(R-7)来确定分位数与常识方法的差异在SO问题"d3.quantile seems to be calculating q1 incorrectly"中得到了很好的展示,其中this post详细描述了原因,可以在菲利普的php版本的原始来源中找到。
以下是Google翻译的一部分(原文为德语):
在我们的例子中,这个值位于(n+1)/4位=5.25,即第5个值(=5)和第6个值(=7)之间。分数(0.25)表示除了5的值之外,还要加上5和6之间距离的1/4。因此,Q1为5 + 0.25 * 2 = 5.5。
所有这些告诉我,我可能不应该尝试编写基于我的四分位理解的代码,而应该借用别人的解决方案。

1
我对Google翻译的“5 + 0.25 * 2 = 5.5”这一部分感到有些不满。我不明白为什么要乘以2。但是在阅读了页面之后,我发现2是两者之间的数字差异。因此,它是通过5-7=2得出的,2是完整长度,然后0.25是其25%。 因此,2的25%为0.5,这就是为什么要将其加到5中的原因。但是,您帖子中的其他内容真的很棒,非常有用。这节省了我大量的时间。谢谢@ruffin。 - Michael Kubler

0

基于buboh的答案,我已经使用了一年以上,当计算中间有2个数字时,我注意到了一些奇怪的事情,用于计算Q1和Q3。

我不知道为什么会有余数值以及它是如何使用的,但根据我的理解,如果你在中间有2个数字,那么你需要取它们的平均值来计算中位数。基于这个想法,我编辑了这个函数:

const asc = (arr) => arr.sort((a, b) => a - b);
const quantile = (arr, q) => {
    const sorted = asc(arr);
    
    let pos = (sorted.length - 1) * q;
    if (pos % 1 === 0) {
        return sorted[pos];
    }
    
    pos = Math.floor(pos);
    if (sorted[pos + 1] !== undefined) {
        return (sorted[pos] + sorted[pos + 1]) / 2;
    }
    
    return sorted[pos];
};


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接