UNPKG

22.1 kBJavaScriptView Raw
1/* ================================================================
2 * probability-distributions by Matt Asher (me[at]mattasher.com)
3 * Originally created for StatisticsBlog.com
4 *
5 * first created at : Sat Oct 10 2015
6 *
7 * ================================================================
8 * Copyright 2015 Matt Asher
9 *
10 * Licensed under the MIT License
11 * You may not use this file except in compliance with the License.
12 *
13 * ================================================================ */
14
15var crypto = require('crypto');
16
17// Shortcuts
18var exp = Math.exp;
19var ln = Math.log;
20var PI = Math.PI;
21
22module.exports = {
23
24 /**
25 * This is the core function for generating entropy
26 *
27 * @param len number of bytes of entropy to create
28 * @returns {number} A pseduo random number between 0 and 1
29 *
30 */
31 prng: function(len) {
32 if(len === undefined) len=16;
33
34 var entropy = crypto.randomBytes(len);
35 var result = 0;
36
37 for(var i=0; i<len; i++) {
38 result = result + Number(entropy[i])/Math.pow(256,(i+1))
39 }
40 return result
41 },
42
43
44
45
46 /**
47 *
48 * @param n The number of random variates to create. Must be a positive integer.
49 * @param alpha First shape parameter
50 * @param beta Second shape parameter
51 * @param loc Location or Non-centrality parameter
52 */
53 rbeta: function(n, alpha, beta, loc) {
54 // Uses relationship with gamma to calculate
55
56 // Validations
57 n = this._v(n, "n");
58 alpha = this._v(alpha, "nn", 1);
59 beta = this._v(beta, "nn", 1);
60 loc = this._v(loc, "r", 0);
61
62 var toReturn = [];
63
64 for(var i=0; i<n; i++) {
65 var g1 = this.rgamma(1, alpha, 1)[0];
66 var g2 = this.rgamma(1, beta, 1)[0];
67
68
69 toReturn[i] = loc + g1/(g1+g2);
70 }
71 return toReturn
72
73 },
74
75
76 /**
77 *
78 * @param n Number of variates to return.
79 * @param size Number of Bernoulli trials to be summed up. Defaults to 1
80 * @param p Probability of a "success". Defaults to 0.5
81 * @returns {Array} Random variates array
82 */
83 rbinom: function(n, size, p) {
84 n = this._v(n, "n");
85 size = this._v(size, "nni", 1);
86 p = this._v(p, "p", 0.5);
87
88 var toReturn = [];
89
90 for(var i=0; i<n; i++) {
91 var result = 0;
92 for(var j=0; j<size; j++) {
93 if(this.prng() < p) {
94 result++
95 }
96 }
97 toReturn[i] = result;
98 }
99 return toReturn
100 },
101
102
103 /**
104 *
105 * @param n The number of variates to create
106 * @param loc Location parameter
107 * @param scale Scale parameter
108 * @returns {Array} Random variates array
109 */
110 rcauchy: function(n, loc, scale) {
111 n = this._v(n, "n");
112 loc = this._v(loc, "r", 0);
113 scale = this._v(scale, "nn", 1);
114
115 var toReturn = [];
116 for(var i=0; i<n; i++) {
117 var x = scale * Math.tan(PI * (this.prng()-0.5))+loc;
118
119 toReturn[i] = x;
120 }
121
122 return toReturn
123 },
124
125 /**
126 *
127 * @param n The number of variates to create
128 * @param df Degrees of freedom for the distribution
129 * @param ncp Non-centrality parameter
130 * @returns {Array} Random variates array
131 */
132 rchisq: function(n, df, ncp) {
133 n = this._v(n, "n");
134 df = this._v(df, "nn");
135 ncp = this._v(ncp, "r", 0);
136
137 var toReturn = [];
138 for(var i=0; i<n; i++) {
139 // Start at ncp
140 var x = ncp;
141 for(var j=0; j<df; j++) {
142 x = x + Math.pow(this.rnorm(1)[0],2);
143 }
144 toReturn[i] = x;
145 }
146 return toReturn
147 },
148
149 /**
150 *
151 * @param n The number of random variates to create. Must be a positive integer.
152 * @param rate The rate parameter. Must be a positive number
153 */
154 rexp: function(n, rate) {
155 n = this._v(n, "n");
156 rate = this._v(rate, "pos", 1);
157
158 var toReturn = [];
159
160 for(var i=0; i<n; i++) {
161
162 toReturn[i] = -ln(this.prng())/rate;
163 }
164
165 return toReturn
166 },
167
168 /**
169 *
170 * @param n The number of variates to create
171 * @param df1 Degrees of freedom for the first parameter
172 * @param df2 Degrees of freedom for the first parameter
173 * @returns {Array} Random variates array
174 */
175 rf: function(n, df1, df2) {
176 n = this._v(n, "n");
177 df1 = this._v(df1, "nn");
178 df2 = this._v(df2, "nn");
179
180 var toReturn = [];
181 for(var i=0; i<n; i++) {
182 var num = this.rchisq(1, df1)[0]/df1;
183 var denom = this.rchisq(1, df2)[0]/df2;
184
185 toReturn[i] = num/denom;
186 }
187
188 return toReturn
189
190 },
191
192
193 /**
194 *
195 * @param n The number of random variates to create. Must be a positive integer
196 * @param alpha
197 * @param rate
198 * @returns {Array} Random variates array
199 */
200 rgamma: function(n, alpha, rate) {
201 // Adapted from https://github.com/mvarshney/simjs-source/ & scipy
202 n = this._v(n, "n");
203 alpha = this._v(alpha, "nn");
204 rate = this._v(rate, "pos", 1);
205
206 var LOG4 = ln(4.0);
207 var SG_MAGICCONST = 1.0 + ln(4.5);
208 var beta = 1/rate;
209
210 var toReturn = [];
211 for(var i = 0; i<n; i++) {
212
213 /* Based on Python 2.6 source code of random.py.
214 */
215
216 if (alpha > 1.0) {
217 var ainv = Math.sqrt(2.0 * alpha - 1.0);
218 var bbb = alpha - LOG4;
219 var ccc = alpha + ainv;
220
221 while (true) {
222 var u1 = this.prng();
223 if ((u1 < 1e-7) || (u > 0.9999999)) {
224 continue;
225 }
226 var u2 = 1.0 - this.prng();
227 var v = ln(u1 / (1.0 - u1)) / ainv;
228 var x = alpha * exp(v);
229 var z = u1 * u1 * u2;
230 var r = bbb + ccc * v - x;
231 if ((r + SG_MAGICCONST - 4.5 * z >= 0.0) || (r >= ln(z))) {
232 var result = x * beta;
233 break;
234 }
235 }
236 } else if (alpha == 1.0) {
237 var u = this.prng();
238 while (u <= 1e-7) {
239 u = this.prng();
240 }
241 var result = - ln(u) * beta;
242 } else {
243 while (true) {
244 var u = this.prng();
245 var b = (Math.E + alpha) / Math.E;
246 var p = b * u;
247 if (p <= 1.0) {
248 var x = Math.pow(p, 1.0 / alpha);
249 } else {
250 var x = - ln((b - p) / alpha);
251 }
252 var u1 = this.prng();
253 if (p > 1.0) {
254 if (u1 <= Math.pow(x, (alpha - 1.0))) {
255 break;
256 }
257 } else if (u1 <= exp(-x)) {
258 break;
259 }
260 }
261 var result = x * beta;
262 }
263
264 toReturn[i] = result;
265 }
266
267 return toReturn;
268
269 },
270
271 // Syntax as in R library VGAM
272 /**
273 *
274 * @param n The number of random variates to create. Must be a positive integer
275 * @param loc Mean
276 * @param scale Scale parameter
277 * @returns {Array} Random variates array
278 */
279 rlaplace: function(n, loc, scale) {
280 n = this._v(n, "n");
281 loc = this._v(loc, "r", 0);
282 scale = this._v(scale, "nn", 1);
283
284 var toReturn = [];
285
286 for(var i=0; i<n; i++) {
287 var core = this.sample([-1,1])[0] * ln(this.prng());
288
289 var x = loc - scale * core;
290
291 toReturn[i] = x;
292 }
293
294 return toReturn
295 },
296
297
298 /**
299 *
300 * @param n The number of random variates to create. Must be a positive integer.
301 * @param meanlog The mean log.
302 * @param sdlog Log SD. Must be greater than 0.
303 * @returns {Array} Random variates array
304 */
305 rlnorm: function(n, meanlog, sdlog) {
306 n = this._v(n, "n");
307 meanlog = this._v(meanlog, "r", 0);
308 sdlog = this._v(sdlog, "nn", 1);
309
310 var toReturn = [];
311
312 for(var i=0; i<n; i++) {
313 var x = this.rnorm(1, meanlog, sdlog)[0];
314
315 toReturn[i] = exp(x);
316 }
317
318 return toReturn
319 },
320
321 /**
322 *
323 * @param n The number of random variates to create. Must be a positive integer.
324 * @param size Number of hits required
325 * @param p Hit probability
326 * @param mu Optional way to specify hit probability
327 * @returns {Array} Random variates array
328 */
329 rnbinom: function(n, size, p, mu) {
330 n = this._v(n, "n");
331 if(size === undefined) size=1;
332 if(Math.round(size) != size) throw "Size must be a whole number";
333 if(size < 1) throw "Size must be one or greater";
334 if(p !== undefined && mu !== undefined) throw "You must specify probability or mean, not both";
335 if(mu !== undefined) p = size/(size+mu);
336 p = this._v(p, "p");
337
338
339 var toReturn = [];
340
341 for(var i=0; i<n; i++) {
342
343 // Core distribution
344 var result = 0;
345 var leftToFind = size;
346 while(leftToFind > 0) {
347 result++;
348 if(this.prng() < p) leftToFind--;
349 }
350
351 toReturn[i] = result - 1;
352 }
353
354 return toReturn
355
356 },
357
358 /**
359 *
360 * @param x Where sample the density
361 * @param mean Mean of the distribution
362 * @param sd Standard deviation for the distribution
363 * @returns {Number} The density given the parameter values
364 */
365 dnorm: function(x, mean, sd) {
366 x = this._v(x, "r");
367 mean = this._v(mean, "r", 0);
368 sd = this._v(sd, "nn", 1);
369
370 // Check for degeneracy
371 if(sd === 0) {
372 if(x === mean) return Infinity;
373 return 0
374 }
375
376 var a = sd*(Math.sqrt(2*PI));
377 var b = -(x-mean)*(x-mean);
378 var c = 2*sd*sd;
379
380 return (1/a)*exp(b/c)
381 },
382
383 /**
384 *
385 * @param n The number of random variates to create. Must be a positive integer.
386 * @param mean Mean of the distribution
387 * @param sd Standard Deviation of the distribution
388 * @returns {Array} Random variates array
389 */
390 rnorm: function(n, mean, sd) {
391 // Adapted from http://blog.yjl.im/2010/09/simulating-normal-random-variable-using.html
392
393 n = this._v(n, "n");
394 mean = this._v(mean, "r", 0);
395 sd = this._v(sd, "nn", 1);
396
397 var toReturn = [];
398
399 for(var i=0; i<n; i++) {
400 var V1, V2, S, X;
401
402 do {
403 var U1 = this.prng();
404 var U2 = this.prng();
405 V1 = (2 * U1) - 1;
406 V2 = (2 * U2) - 1;
407 S = (V1 * V1) + (V2 * V2);
408 } while (S > 1);
409
410 X = Math.sqrt(-2 * ln(S) / S) * V1;
411 X = mean + sd * X;
412 toReturn.push(X);
413 }
414
415 return toReturn
416 },
417
418 /**
419 *
420 * @param n The number of random variates to create. Must be a positive integer.
421 * @param lambda Mean/Variance of the distribution
422 * @returns {Array} Random variates array
423 */
424 rpois: function(n, lambda) {
425 n = this._v(n, "n");
426 lambda = this._v(lambda, "pos");
427
428 var toReturn = [];
429
430 for(var i=0; i<n; i++) {
431
432 // Adapted from http://wiki.q-researchsoftware.com/wiki/How_to_Generate_Random_Numbers:_Poisson_Distribution
433 if (lambda < 30) {
434
435 var L = exp(-lambda);
436 var p = 1;
437 var k = 0;
438 do {
439 k++;
440 p *= this.prng();
441 } while (p > L);
442 toReturn.push(k - 1);
443
444 } else {
445
446 // Roll our own
447 // Fix total number of samples
448 var samples = 10000;
449 var p = lambda/samples;
450 var k = 0;
451 for(var j=0; j<samples; j++) {
452 if(this.prng() < p) {
453 k++
454 }
455 }
456 toReturn[i] = k;
457 }
458 }
459
460 return toReturn
461 },
462
463 /**
464 *
465 * @param n Number of variates to return
466 * @param min Lower bound
467 * @param max Upper bound
468 * @returns {Array} Random variates array
469 */
470 runif: function(n, min, max) {
471 n = this._v(n, "n");
472 min = this._v(min, "r", 0);
473 max = this._v(max, "r", 1);
474 if(min > max) throw "Minimum value cannot be greater than maximum value";
475
476 var toReturn = [];
477
478 for(var i=0; i<n; i++) {
479 var raw = this.prng();
480 var scaled = min + raw*(max-min);
481 toReturn.push(scaled)
482 }
483 return toReturn
484 },
485
486
487 /**
488 *
489 * @param collection Array of items to sample from
490 * @param n Number of items to sample. If missing, n will be set to the length of the collection and it will shuffle
491 * @param replace Sample with replacement? False by default
492 * @param ratios Ratios to weight items. Can be any non-negative number. By default all items are given equal weight
493 * @returns {Array} Array of sampled items
494 */
495 sample: function(collection, n, replace, ratios) {
496
497 // Validations
498 collection = this._v(collection, "a");
499 n = this._v(n, "n", collection.length); // If n is undefined, sample the full array
500 if(replace === undefined) replace = false;
501 if(!replace && collection.length < n)
502 throw "You cannot select " + n + " items from an array of length " + collection.length + " without replacement";
503
504 if(ratios === undefined) {
505 ratios = [];
506 for(var m=0; m<collection.length; m++) { ratios[m] = 1 }
507 }
508
509 var cumulativeProbs = this._getCumulativeProbs(ratios, collection.length);
510
511 // Main loop
512 var toReturn = [];
513
514 for(var i=0; i<n; i++) {
515
516 var chosen = this._sampleOneIndex(cumulativeProbs);
517
518 if(replace) {
519 toReturn[i] = collection[chosen];
520 } else {
521
522 // Remove from collection and ratios
523 toReturn[i] = collection.splice(chosen, 1)[0];
524 ratios.splice(chosen, 1);
525
526 // Make sure we aren't at the end
527 if(ratios.length) {
528 cumulativeProbs = this._getCumulativeProbs(ratios, collection.length);
529 }
530 }
531 }
532
533 return toReturn;
534
535 },
536
537
538 // HELPERS
539
540 /**
541 *
542 * @param ratios Array of non-negative numbers to be turned into CDF
543 * @param len length of the collection
544 * @returns {Array}
545 * @private
546 */
547 _getCumulativeProbs: function(ratios, len) {
548 if(len === undefined) throw "An error occurred: len was not sent to _getCumulativeProbs";
549 if(ratios.length !== len) throw "Probabilities for sample must be same length as the array to sample from";
550
551 var toReturn = [];
552
553 if(ratios !== undefined) {
554 ratios = this._v(ratios, "a");
555 if(ratios.length !== len) throw "Probabilities array must be the same length as the array you are sampling from";
556
557 var sum = 0;
558 ratios.map(function(ratio) {
559 ratio = this._v(ratio, "nn"); // Note validating as ANY non-negative number
560 sum+= ratio;
561 toReturn.push(sum);
562 }.bind(this));
563
564 // Divide by total to normalize
565 for(var k=0; k<toReturn.length; k++) { toReturn[k] = toReturn[k]/sum }
566 return toReturn
567 }
568 },
569
570 _sampleOneIndex: function(cumulativeProbs) {
571
572 var toTake = this.prng();
573
574 // Find out where this lands in weights
575 var cur = 0;
576 while(toTake > cumulativeProbs[cur]) cur++;
577
578 return cur;
579 },
580
581
582 // Return default if undefined, otherwise validate
583 _v: function(param, type, defaultParam) {
584 if(param == null && defaultParam != null)
585 return defaultParam;
586
587 switch(type) {
588
589 // Array of 1 item or more
590 case "a":
591 if(!Array.isArray(param) || !param.length) throw "Expected an array of length 1 or greater";
592 return param;
593
594 // Natural number
595 case "n":
596 if(param === undefined) throw "You must specify how many values you want";
597 if(param !== Number(param)) throw "The number of values must be numeric";
598 if(param !== Math.round(param)) throw "The number of values must be a whole number";
599 if(param < 1) throw "The number of values must be a whole number of 1 or greater";
600 if(param === Infinity) throw "The number of values cannot be infinite ;-)";
601 return param;
602
603 // Valid probability
604 case "p":
605 if(Number(param) !== param) throw "Probability value is missing or not a number";
606 if(param > 1) throw "Probability values cannot be greater than 1";
607 if(param < 0) throw "Probability values cannot be less than 0";
608 return param;
609
610 // Positive numbers
611 case "pos":
612 if(Number(param) !== param) throw "A required parameter is missing or not a number";
613 if(param <= 0) throw "Parameter must be greater than 0";
614 if(param === Infinity) throw 'Sent "infinity" as a parameter';
615 return param;
616
617 // Look for numbers (reals)
618 case "r":
619 if(Number(param) !== param) throw "A required parameter is missing or not a number";
620 if(param === Infinity) throw 'Sent "infinity" as a parameter';
621 return param;
622
623 // Non negative real number
624 case "nn":
625 if(param !== Number(param)) throw "A required parameter is missing or not a number";
626 if(param < 0) throw "Parameter cannot be less than 0";
627 if(param === Infinity) throw 'Sent "infinity" as a parameter';
628 return param;
629
630 // Non negative whole number (integer)
631 case "nni":
632 if(param !== Number(param)) throw "A required parameter is missing or not a number";
633 if(param !== Math.round(param)) throw "Parameter must be a whole number";
634 if(param < 0) throw "Parameter cannot be less than zero";
635 if(param === Infinity) throw 'Sent "infinity" as a parameter';
636 return param;
637
638 }
639 },
640
641
642 // ________ _______ ______ _____ _____ __ __ ______ _ _ _______ _
643 // | ____\ \ / / __ \| ____| __ \|_ _| \/ | ____| \ | |__ __|/\ | |
644 // | |__ \ V /| |__) | |__ | |__) | | | | \ / | |__ | \| | | | / \ | |
645 // | __| > < | ___/| __| | _ / | | | |\/| | __| | . ` | | | / /\ \ | |
646 // | |____ / . \| | | |____| | \ \ _| |_| | | | |____| |\ | | |/ ____ \| |____
647 // |______/_/ \_\_| |______|_| \_\_____|_| |_|______|_| \_| |_/_/ \_\______|
648
649 /**
650 *
651 * @param n Number of variates to return
652 * @param loc Starting point. Must be a non-negative integer. 0 for degenerate distribution of 0.
653 * @param p Probability of moving towards finish
654 * @param cap Maximum steps before giving up
655 * @param trace Variable to track progress
656 * @returns {Array} Random variates array
657 *
658 * The FML distribution is a is based on the number of steps taken to return to the origin
659 * from a given position, with transition probabilities set at the beginning by picking a
660 * random variate from U(0,1).
661 */
662 rfml: function (n, loc, p, cap, trace) {
663 n = this._v(n, "n");
664 loc = this._v(loc, "nni", 1);
665 if(p === undefined) p=this.prng;
666 cap = this._v(cap, "n", 10000);
667 if(trace === undefined) trace={};
668
669 var toReturn = [];
670
671 for(var i=0; i<n; i++) {
672 var x = 0;
673 var s = loc;
674 var currP = p();
675 if(loc === 0) {
676
677 toReturn[i] = 0;
678 } else {
679
680 do {
681
682 var trial = this.prng();
683 if(trial < currP) {
684 s++;
685 trace[String(i) + "_" + String(x)] = { problems: s, p: currP, result: 1 }
686 } else {
687 s--;
688 trace[String(i) + "_" + String(x)] = { problems: s, p: currP, result: -1 }
689 }
690 x++
691 } while(s > 0 && x < cap);
692
693 if(x === cap) x = -1; // Indicate we failed to do it in time.
694 toReturn[i] = x;
695 }
696 }
697 return toReturn
698 },
699
700 // http://www.statisticsblog.com/2013/05/uncovering-the-unreliable-friend-distribution-a-case-study-in-the-limits-of-mc-methods/
701 /**
702 *
703 * The Unrelaible Friend distribution
704 * @param n
705 * @returns {Array} Random variates array
706 */
707 ruf: function(n) {
708 n = this._v(n, "n");
709
710 var toReturn = [];
711
712 for(var i=0; i<n; i++) {
713 toReturn[i] = this.rexp(1, this.prng())[0];
714 }
715
716 return toReturn
717 }
718};
719
720// TODO: Add "perfect fake" functions: http://www.statisticsblog.com/2010/06/the-perfect-fake/
721// NOTES
722// Potential config options:
723// default entropy amount
724// Need pathway to make ready for secure applications (NIST/diehard?)
725// Always return a vector unless number is 1? This could be config option or put "1" at end of fcn to get 1 only
726// Separate out core random variate creation from number to create loop
727// TODO: To test out quality of randomness, stub in specific values for this.prng and make sure correct stuff is returned.
\No newline at end of file