UNPKG

8.04 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.randint = randint;
7exports.rand = rand;
8exports.sampleFisherYates = sampleFisherYates;
9exports.sample = sample;
10
11var _arrays = require('../arrays');
12
13var Arrays = _interopRequireWildcard(_arrays);
14
15var _search = require('../util/search');
16
17var Search = _interopRequireWildcard(_search);
18
19function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
20
21function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // Internal dependencies
22
23
24/**
25 * Generate a random integer between a lower bound (inclusive) and an upper bound (exclusive).
26 *
27 * @param {number} a - Lower bound (inclusive)
28 * @param {number} b - Upper bound (exclusive)
29 * @param {number|Array.<number>} [shape = null] - If null, a single element is returned. If
30 * integer, an array of {shape} elements is returned. If an Array, random numbers are returned in
31 * a shape specified by this array. n-th element corresponds to the number of elements in the
32 * n-th dimension.
33 * @return {Array.<mixed>} Array of the specified with zero in all entries
34 * @return {number|Array.<mixed>} Random integer in range [a,b), or a possibly nested array of
35 * random integers in range [a, b)
36 */
37function randint(a, b) {
38 var shape = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
39
40 if (Number.isInteger(shape)) {
41 // List of random integers
42 return [].concat(_toConsumableArray(Array(shape))).map(function (x) {
43 return randint(a, b);
44 });
45 } else if (Array.isArray(shape) && shape.length > 0) {
46 if (shape.length === 1) {
47 // Single shape item remaining; return list of integers
48 return randint(a, b, shape[0]);
49 }
50
51 // Nested list of random integers
52 return [].concat(_toConsumableArray(Array(shape[0]))).map(function () {
53 return randint(a, b, shape.slice(1));
54 });
55 }
56
57 // Single random integer
58 return a + Math.floor((b - a) * Math.random());
59}
60
61/**
62 * Generate a random number between a lower bound (inclusive) and an upper bound (exclusive).
63 *
64 * @param {number} a - Lower bound (inclusive)
65 * @param {number} b - Upper bound (exclusive)
66 * @return {number} Random number in range [a,b)
67 */
68function rand(a, b) {
69 return a + Math.random() * (b - a);
70}
71
72/**
73 * Take a random sample without replacement from an array. Uses the Fisher-Yates shuffling,
74 * algorithm, modified to accomodate sampling.
75 *
76 * @param {Array.<mixed>} input Input array
77 * @param {number} number Number of elements to sample from the input array
78 * @return {Array.<mixed>} Array of length {number} with values sampled from the input array
79 */
80function sampleFisherYates(input, number) {
81 // Copy input array
82 var shuffledArray = input.slice(0);
83
84 // Number of elements in the input array
85 var numElements = input.length;
86
87 for (var i = numElements - 1; i >= numElements - number; i -= 1) {
88 var index = randint(0, i + 1);
89 var tmp = shuffledArray[index];
90 shuffledArray[index] = shuffledArray[i];
91 shuffledArray[i] = tmp;
92 }
93
94 // Return the sampled values
95 return shuffledArray.slice(numElements - number);
96}
97
98/**
99 * Take a random sample with or without replacement from an array with uniform weights.
100 *
101 * @param {Array.<mixed>} input - Input array
102 * @param {number} number - Number of elements to sample from the input array
103 * @param {boolean} [withReplacement=true] - Whether to sample with (set to true) or without
104 * replacement (false)
105 * @return {Array.<mixed>} Array of length {number} with values sampled from the input array
106 */
107function sampleUniform(input, number) {
108 var withReplacement = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
109
110 // If sampling without replacement, use Fisher-Yates sampling
111 if (!withReplacement) {
112 return sampleFisherYates(input, number);
113 }
114
115 // If sampling with replacement, choose a random element each time
116 var indices = randint(0, input.length, number);
117 return indices.map(function (x) {
118 return input[x];
119 });
120}
121
122/**
123 * Take a random sample with or without replacement from an array. Supports using sampling weights,
124 * governing the probability of an item in the input array being selected.
125 *
126 * @param {Array.<mixed>} input - Input array
127 * @param {number} number - Number of elements to sample from the input array
128 * @param {boolean} [withReplacement=true] - Whether to sample with (set to true) or without
129 * replacement (false)
130 * @param {Array.<number>|string} [weights='uniform'] - Weights to use for sampling. Defaults to
131 * 'uniform', which means all samples have equal probability of being selected. Alternatively, you
132 * can pass an array of weights, containing a single weight for each element in the input array
133 * @return {Array.<mixed>} Array of length {number} with values sampled from the input array
134 */
135function sample(input, number) {
136 var withReplacement = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
137 var weights = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'uniform';
138
139 if (Array.isArray(weights)) {
140 if (weights.length !== input.length) {
141 throw new Error('Weights array length does not equal input array length.');
142 }
143
144 if (!withReplacement && number > weights.filter(function (x) {
145 return x > 0;
146 }).length) {
147 throw new Error('Invalid sampling quantity specified: sampling without replacement cannot sample more elements than the number of non-zero weights in the weights array.');
148 }
149 } else if (weights !== 'uniform') {
150 throw new Error('Invalid value specified for "weights".');
151 }
152
153 if (!withReplacement && number > input.length) {
154 throw new Error('Invalid sampling quantity specified: sampling without replacement cannot sample more elements than the number of input elements.');
155 }
156
157 // Use the uniform sampling method if the user has specified uniform weights
158 if (weights === 'uniform') {
159 return sampleUniform(input, number, withReplacement);
160 }
161
162 // Copy weights vector
163 var useWeights = weights.slice();
164
165 var calculateCumWeights = function calculateCumWeights(localWeights) {
166 return localWeights.reduce(function (r, a, i) {
167 return [].concat(_toConsumableArray(r), [i > 0 ? r[i - 1] + a : a]);
168 }, []);
169 };
170
171 var sampleSingle = function sampleSingle(useCumWeights) {
172 // Generate a random number, and find the interval in the array of cumulative weights to which
173 // it corresponds. We use this index as the sampled array index, which corresponds to weighted
174 // sampling
175 var randomNumber = rand(0, useCumWeights[useCumWeights.length - 1]);
176 return Search.binaryIntervalSearch([0].concat(_toConsumableArray(useCumWeights)), randomNumber);
177 };
178
179 if (withReplacement) {
180 // Sample with replacement, i.e., randomly sample one element n times in a row
181 var cumWeights = calculateCumWeights(useWeights);
182
183 return Arrays.zeros(number).map(function () {
184 return input[sampleSingle(cumWeights)];
185 });
186 }
187
188 // Sample without replacement: randomly sample one element, remove it from the list of elements
189 // that can still be sampled and the weights list, and sample from the remaining elements
190
191 // Copy input
192 var useInput = input.slice();
193
194 // List of elements sampled
195 var samples = [];
196
197 while (samples.length < number) {
198 // Sample from remaining inputs
199 var _cumWeights = calculateCumWeights(useWeights);
200 var useSample = sampleSingle(_cumWeights);
201 samples.push(useInput[useSample]);
202
203 // Remove sampled element from input elements and weights lists
204 useInput.splice(useSample, 1);
205 useWeights.splice(useSample, 1);
206 }
207
208 return samples;
209}
\No newline at end of file