JavaScript Math.random 正态分布(高斯钟曲线)?

108

我想知道 JavaScript 函数 Math.random 是否使用正态分布而非均匀分布。

如果不是,我该如何获得使用正态分布的数字?在互联网上没有找到一个清晰的算法来生成随机正态分布的数字。

我想重建一台 Schmidt 机器(德国物理学家)。该机器会产生0或1的随机数,并且它们必须服从正态分布,以便我可以将它们作为高斯钟形曲线绘制出来。

例如,随机函数生成120个数(0或1),这些数值的平均值(均值)必须接近60。


1
不是答案(因为这不是你的问题:P),但这应该会有所帮助:http://www.meredithdodge.com/2012/05/30/a-great-little-javascript-function-for-generating-random-gaussiannormalbell-curve-numbers/ - Jongware
3
对 Math.random() 进行几次平均,即使只调用 3 次,也能得到类似正态分布的分布情况。请参见此处 https://jsbin.com/tetizotugu/1/edit?js,output,修改第一个参数即可。 - GameAlchemist
1
这是一个非常普遍的困惑,因为在人们理解正态曲线之前,统计学通常被教授为最重要的事情。如果您对每个样本取30次硬币的平均值(sum xs / length xs),那么这30个平均值将近似地服从于正态分布,并且随着您将30增加到更大的数字,它将收敛于以0.5为中心的完美正态曲线。随机本身是均匀整数分布(具有参数n、n + 1,通常为0、1),这就是在C++中使用的mersanne twister随机引擎mt19937中所知道的。 - Dmytro
2
这个问题本身就很有价值,因为它要求生成正态分布的随机数。关于 OP 的使用情况,他没有像 Dmirty 一样解释得很清楚。 - Andre Figueiredo
21个回答

194

由于在我的经验中,这是“js gaussian random”的第一个谷歌搜索结果,我感觉有义务给出对该查询的实际答案。

Box-Muller transform将(0, 1)上的两个独立均匀变量转换为两个标准高斯变量(平均值为0,方差为1)。这可能不太高效,因为涉及到sqrtlogcos调用,但这种方法比中心极限定理(对N个均匀变量求和)更优越,因为它不会将输出限制在有界范围(-N/2,N/2)内。而且它非常简单:

// Standard Normal variate using Box-Muller transform.
function gaussianRandom(mean=0, stdev=1) {
    const u = 1 - Math.random(); // Converting [0,1) to (0,1]
    const v = Math.random();
    const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
    // Transform to the desired mean and standard deviation:
    return z * stdev + mean;
}

3
在1000个样本中,范围为[-3.10, 3.24] - Dorian
4
@Dorian 这并不完全出乎意料,因为正态变量在该范围之外的概率约为千分之一:$ python3; >>> import scipy.stats as stats; >>> 1 - ( stats.norm.cdf( 3.24 ) - stats.norm.cdf( -3.10 ) ); 0.0015652517111527375 - Maxwell Collard
2
@Dorian:谢谢,最终我使用了一个近似高斯函数,其范围为[0, 1],正如我的程序所需的那样:https://dev59.com/El8e5IYBdhLWcg3wlbTx#39187274。 - Dorian
10
为什么不直接将u = 1 - Math.random()赋值,而不需要使用while循环的条件? - Vortico
7
@Indra 是的,这不是问题。实际上,我们可以忘记循环 v 并用稍微更有效率的一行代码编写函数。return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()) - Vortico
显示剩余12条评论

168

0到1之间的正态分布

在Maxwell的回答基础上,此代码使用Box-Muller变换给出了一个包括0和1在内的正态分布。如果距离平均值超过3.6个标准差(小于0.02%的概率),它将重新采样数值。

function randn_bm() {
  let u = 0, v = 0;
  while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
  while(v === 0) v = Math.random();
  let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
  num = num / 10.0 + 0.5; // Translate to 0 -> 1
  if (num > 1 || num < 0) return randn_bm() // resample between 0 and 1
  return num
}

可视化

enter image description here

n = 100

enter image description here

n = 10,000

enter image description here

n = 10,000,000

带有最小值、最大值和偏度的正态分布

此版本允许您设置最小值、最大值和偏度系数。请参见我在底部的用法示例。

function randn_bm(min, max, skew) {
  let u = 0, v = 0;
  while(u === 0) u = Math.random() //Converting [0,1) to (0,1)
  while(v === 0) v = Math.random()
  let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v )
  
  num = num / 10.0 + 0.5 // Translate to 0 -> 1
  if (num > 1 || num < 0) 
    num = randn_bm(min, max, skew) // resample between 0 and 1 if out of range
  
  else{
    num = Math.pow(num, skew) // Skew
    num *= max - min // Stretch to fill range
    num += min // offset to min
  }
  return num
}

在这里输入图片描述

randn_bm(-500, 1000, 1);

输入图像描述

randn_bm(10, 20, 0.25);

这里输入图片描述

randn_bm(10, 20, 3);

以下是这些屏幕截图的JSFiddle链接: https://jsfiddle.net/2uc346hp/


2
偏斜因子到底代表什么?它似乎不代表标准差或方差。我如何在数学上理解它? - user5834627
2
@André 我承认,在我这里的答案中,偏斜因子背后没有太多的数学支持。这只是一种方便的方法,用于拉伸或缩小我的用例数据。如果您想要实现一个合法的偏斜正态变换,具有适当的均值、标准差和形状变量,您可以查看此链接。https://spin.atomicobject.com/2019/09/30/skew-normal-prng-javascript/ - joshuakcockrell
此外,这是我回答的原始 JS Fiddle https://jsfiddle.net/ktq9jaoe/4/ 也许我会修改它以包含更多的数学均值/标准偏差/形状变量。 - joshuakcockrell
2
非常好的答案,但是我发现你代码中有一个小错误: if (num > 1 || num < 0) num = randn_bm(min, max, skew); // resample between 0 and 1 if out of range...应该改为: if (num > 1 || num < 0) { num = randn_bm(min, max, skew); //在超出范围时重新取样在0和1之间 } else { ... } 返回 num;也就是说,只有在不重新采样的情况下才可以缩放最终数字。 - MushyMiddle
1
为什么当最小值是 0,最大值是 1 时无法使其工作?我想模仿本地的 random 方法,只输出在 0 到 1 之间。 - vsync
显示剩余5条评论

69
我想知道JavaScript函数Math.random是否符合正态分布。
Javascript Math.random不是正态分布(高斯钟形曲线)。根据ES 2015, 20.2.2.27“返回一个数字值,带有正号,大于或等于0但小于1,在该范围内随机或拟随机地选择,使用实现相关的算法或策略。此函数不需要参数。”因此,当n足够大时,提供的集合将获得近似均匀分布。间隔内的所有值出现的概率相等(平行于x轴的直线,表示介于0.0和1.0之间的数字)。
如何获得正态分布的数字?

有几种方法可以获得符合正态分布的数字集合。正如 Maxwell Collard 所回答的那样,Box-Muller transform 可以将均匀分布转换为正态分布(代码可以在Maxwell Collard answer中找到)。

另一个stackoverflow问题的回答中有一个回复提供了其他均匀分布转换为正态分布的算法,例如: Ziggurat、 Ratio-of-uniforms、 反转CDF。 此外,其中一位回答者说:

Ziggurat算法对此非常有效,尽管从头开始实现Box-Muller transform更容易(而且不会太慢)。

最后,

我希望重建一台Schmidt机器(德国物理学家),该机器产生0或1的随机数,并且它们必须是正态分布的,以便我可以在高斯钟形曲线中绘制它们。
当我们只有两个值(0或1)时,高斯曲线看起来与具有2个可能值的均匀分布相同。这就是为什么简单的
function randomZero_One(){
    return Math.round(Math.random());
}

这将足以满足需求。它会以大约相等的概率返回伪随机值0和1。


24
如果你好奇点赞来自哪里,我提醒一下。这个回答被提及在 Reddit 的一个帖子中,激发了“元效应”。https://www.reddit.com/r/ProgrammerHumor/comments/a3ngy7/stackoverflow_is_a_weird_place/ - LukeG
5
我在Reddit上发布了那条评论。现在这个答案有了积极的投票,我为自己的所作所为感到骄傲。 - SCLeo
哇,这真是让我的一天,因为这个问题真的很老了,我无法删除它。可惜Stack每天只能获得200个点,否则我本来会获得超过1000个点的。但仍然感觉棒极了:D,感谢Reddit的支持(还有SCLeo)。:) - kodvin
2
除了少数几个答案外,所有这些答案都没有意识到随机分布不会在0和1之间分布,但在某些情况下可以达到任意高的值。这是一个非常混乱的概念。 - Brethlosze

59

我想要在0到1之间获得大致符合高斯分布的随机数,通过多次测试后,我发现以下代码最适合:

function gaussianRand() {
  var rand = 0;

  for (var i = 0; i < 6; i += 1) {
    rand += Math.random();
  }

  return rand / 6;
}

而且还有一个额外的奖励:

function gaussianRandom(start, end) {
  return Math.floor(start + gaussianRand() * (end - start + 1));
}

4
这实际上是一个非常有效的简单解决方案。增加因子(6),可以使分布更加紧密。 - tadman
11
值得一提的是,这是中心极限定理的实现,样本大小为6。与高斯曲线相比,它更偏向于中心数值,而忽略了尾部数值。以下是我的一个实现:http://plnkr.co/edit/jaky1FHCGpt81vs5Lohz?p=preview。 - Andre Figueiredo
这不是一般的正态分布,而是一个经过缩放的正态分布,因为其标准差不是1。 - GA1
var rand = 0; for (var i = 0; i < 6; i += 1) { rand += Math.random(); } return rand / 6; } 还有一个额外的函数: function gaussianRandom(start, end) { return Math.floor(start + gaussianRand() * (end - start + 1)); } 这适用于start和end > 0且< 1``` - fedeb

19

Javascript的Math.random()伪随机函数返回0到1之间均匀分布的变量。为了获得高斯分布,我使用以下代码:

// returns a gaussian random function with the given mean and stdev.
function gaussian(mean, stdev) {
  var y2;
  var use_last = false;
  return function() {
    var y1;
    if (use_last) {
      y1 = y2;
      use_last = false;
    } else {
      var x1, x2, w;
      do {
        x1 = 2.0 * Math.random() - 1.0;
        x2 = 2.0 * Math.random() - 1.0;
        w = x1 * x1 + x2 * x2;
      } while (w >= 1.0);
      w = Math.sqrt((-2.0 * Math.log(w)) / w);
      y1 = x1 * w;
      y2 = x2 * w;
      use_last = true;
    }

    var retval = mean + stdev * y1;
    if (retval > 0)
      return retval;
    return -retval;
  }
}

// make a standard gaussian variable.     
var standard = gaussian(100, 15);

// make a bunch of standard variates
for (i = 0; i < 1000; i++) {
  console.log( standard() )
}

我想这个来自于Knuth

这里可以看到图表


8
这是Marsaglia极坐标法 - Rafi
9
可以,请陈述需要翻译的内容。请问您需要翻译成哪个语言? - Chris K
@ChrisK。在符号翻转之前,retvar将围绕所提供的均值的负值分布。如果您将均值设为0,则无论是否翻转符号都没有关系。在这两种情况下,您将拥有相等的正数和负数分布。 - John Pankowicz
2
@ChrisK。你是对的:由于代码的最后三行,gaussian函数返回的值总是正数,因此它不能是高斯分布的随机值。 - Alessandro Jacopson
2
我认为Knuth没有 if(retval > 0) return retval; return -retval; - Alessandro Jacopson
显示剩余2条评论

14

利用中心极限定理的函数。

function normal(mu, sigma, nsamples){
    if(!nsamples) nsamples = 6
    if(!sigma) sigma = 1
    if(!mu) mu=0

    var run_total = 0
    for(var i=0 ; i<nsamples ; i++){
       run_total += Math.random()
    }

    return sigma*(run_total - nsamples/2)/(nsamples/2) + mu
}

我的统计知识有限,但我可以提出@AndreFigueiredo的警告可能非常重要。我发现使用Marsaglia极坐标法替换此函数可以得到更加高斯正态分布的结果。 - Ross Rogers
2
当样本量nsamples趋近于无穷大时,极限定理意味着分布更好地逼近。我不会使用此函数进行强健的统计分析。但是,如果您需要一个用于测试目的的生成器,则上述方法可以正常工作。可访问的最大中心距离munsamples控制。 - Joe
我发现,为了生成更好的高斯分布,方程式应该写成:return sigma*(run_total - nsamples/2)/Math.sqrt(nsamples/12) + mu - ZephDavies
@ZephDavies,你的代码在nsamples != 6时似乎有问题。 - cowlinator

10

来自规范:

15.8.2.14 随机数 ( )

返回一个正号的数字值,大于或等于0但小于1,并以近似均匀分布的方式随机或伪随机选择,使用实现相关的算法或策略。该函数不需要参数。

因此,这是一种均匀分布,而不是正态分布或高斯分布。这就是你会在任何基本语言运行时中的标准随机数工具中找到的内容,除了专门的统计库之外。


9
你混淆了函数的输出(它是0到1之间的均匀分布)和通过重复绘制随机数(要么是0要么是1)生成高斯分布的需求 - 经过大量试验后,它们的总和将近似于正态分布。
你可以使用Math.random()函数,然后将结果四舍五入为整数:如果小于0.5,则返回0;如果大于等于0.5,则返回1。现在你有了等概率的零和一,你可以继续使用你在问题中描述的方法。
只是为了澄清:我认为不可能有一个算法以正态分布的方式产生0或1 - 正态分布需要连续变量。
例如,当你对120个数字执行上述操作时,你平均会得到60个1和60个0。你所获得的实际分布将是具有均值为60和标准差的二项分布。
stdev = sqrt(p(1-p)N) = 5.48

当你有 n 个样本和概率为 p(我们设定为0.5)时,特定数字k的概率为

p = n! / ((n-k)! k!) p^k (1-p)^(n-k)

当p = 0.5时,你只会得到二项式系数 - 当n > 30时,这些系数通常趋近于正态分布。


2
在二进制(或一元)中不存在高斯分布,你说得对。但是可以是“数字的”;例如,可以从三进制整数值(0、1、2)构建钟形曲线,显示高斯分布的效果(其中1出现最多,作为平均值),尽管为了获得符合68-95-99.7规则的适当曲线形状,粒度需要更高。顺便说一句,你关于舍入整数的注释是获取随机布尔值的绝佳方法! - Beejor

8

以下是一个单行示例:

Math.sqrt(-2 * Math.log(Math.random()))*Math.cos((2*Math.PI) * Math.random())

一个 JSFiddle 实例: https://jsfiddle.net/rszgjqf8/


请解释以下内容,并在可能的情况下添加图表。 - vsync
@vsync 大约已经过去五年了,但如果我今晚有时间,我可以分解这个一行代码,并且如果我感到慷慨的话,可能会画出它的图表。 - unsalted
2
这应该是 Math.sqrt(-2*Math.log(1-Math.random()))*Math.cos(2*Math.PI*Math.random()),这样你就永远不会在 log 中得到零值。 - Brethlosze

5

对于那些想要生成正态分布值的人,我建议查看这个JavaScript中Ziggurat算法的实现:https://www.npmjs.com/package/node-ziggurat

作者页面上找到的代码如下:

function Ziggurat(){

var jsr = 123456789;

var wn = Array(128);
var fn = Array(128);
var kn = Array(128);

function RNOR(){
  var hz = SHR3();
  var iz = hz & 127;
  return (Math.abs(hz) < kn[iz]) ? hz * wn[iz] : nfix(hz, iz);
}

this.nextGaussian = function(){
  return RNOR();
}

function nfix(hz, iz){
  var r = 3.442619855899;
  var r1 = 1.0 / r;
  var x;
  var y;
  while(true){
    x = hz * wn[iz];
    if( iz == 0 ){
      x = (-Math.log(UNI()) * r1); 
      y = -Math.log(UNI());
      while( y + y < x * x){
        x = (-Math.log(UNI()) * r1); 
        y = -Math.log(UNI());
      }
      return ( hz > 0 ) ? r+x : -r-x;
    }

    if( fn[iz] + UNI() * (fn[iz-1] - fn[iz]) < Math.exp(-0.5 * x * x) ){
      return x;
    }
    hz = SHR3();
    iz = hz & 127;

    if( Math.abs(hz) < kn[iz]){
      return (hz * wn[iz]);
    }
  }
}

function SHR3(){
  var jz = jsr;
  var jzr = jsr;
  jzr ^= (jzr << 13);
  jzr ^= (jzr >>> 17);
  jzr ^= (jzr << 5);
  jsr = jzr;
  return (jz+jzr) | 0;
}

function UNI(){
  return 0.5 * (1 + SHR3() / -Math.pow(2,31));
}

function zigset(){
  // seed generator based on current time
  jsr ^= new Date().getTime();

  var m1 = 2147483648.0;
  var dn = 3.442619855899;
  var tn = dn;
  var vn = 9.91256303526217e-3;

  var q = vn / Math.exp(-0.5 * dn * dn);
  kn[0] = Math.floor((dn/q)*m1);
  kn[1] = 0;

  wn[0] = q / m1;
  wn[127] = dn / m1;

  fn[0] = 1.0;
  fn[127] = Math.exp(-0.5 * dn * dn);

  for(var i = 126; i >= 1; i--){
    dn = Math.sqrt(-2.0 * Math.log( vn / dn + Math.exp( -0.5 * dn * dn)));
    kn[i+1] = Math.floor((dn/tn)*m1);
    tn = dn;
    fn[i] = Math.exp(-0.5 * dn * dn);
    wn[i] = dn / m1;
  }
}
zigset();
}

创建一个Ziggurat.js文件,然后:
var z = new Ziggurat();
z.nextGaussian();

对我来说,它完美地运作,正如我在维基百科上所读到的那样,这是比Box-Muller更高效的算法。

输入链接描述在此处


1
也许最好添加一些相关的代码,这样才能成为一个答案。 - Razvan Dumitru

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