Add time series segmentation algorithms as moving averages
[WebKit-https.git] / Websites / perf.webkit.org / public / v2 / js / statistics.js
1 var Statistics = new (function () {
2
3     this.min = function (values) {
4         return Math.min.apply(Math, values);
5     }
6
7     this.max = function (values) {
8         return Math.max.apply(Math, values);
9     }
10
11     this.sum = function (values) {
12         return values.length ? values.reduce(function (a, b) { return a + b; }) : 0;
13     }
14
15     this.squareSum = function (values) {
16         return values.length ? values.reduce(function (sum, value) { return sum + value * value;}, 0) : 0;
17     }
18
19     // With sum and sum of squares, we can compute the sample standard deviation in O(1).
20     // See https://rniwa.com/2012-11-10/sample-standard-deviation-in-terms-of-sum-and-square-sum-of-samples/
21     this.sampleStandardDeviation = function (numberOfSamples, sum, squareSum) {
22         if (numberOfSamples < 2)
23             return 0;
24         return Math.sqrt(squareSum / (numberOfSamples - 1) - sum * sum / (numberOfSamples - 1) / numberOfSamples);
25     }
26
27     this.supportedConfidenceIntervalProbabilities = function () {
28         var supportedProbabilities = [];
29         for (var quantile in tDistributionQuantiles)
30             supportedProbabilities.push((1 - (1 - quantile) * 2).toFixed(2));
31         return supportedProbabilities
32     }
33
34     // Computes the delta d s.t. (mean - d, mean + d) is the confidence interval with the specified probability in O(1).
35     this.confidenceIntervalDelta = function (probability, numberOfSamples, sum, squareSum) {
36         var quantile = (1 - (1 - probability) / 2);
37         if (!(quantile in tDistributionQuantiles)) {
38             throw 'We only support ' + this.supportedConfidenceIntervalProbabilities().map(function (probability)
39             { return probability * 100 + '%'; } ).join(', ') + ' confidence intervals.';
40         }
41         if (numberOfSamples - 2 < 0)
42             return NaN;
43         var deltas = tDistributionQuantiles[quantile];
44         var degreesOfFreedom = numberOfSamples - 1;
45         if (degreesOfFreedom > deltas.length)
46             throw 'We only support up to ' + deltas.length + ' degrees of freedom';
47
48         // d = c * S/sqrt(numberOfSamples) where c ~ t-distribution(degreesOfFreedom) and S is the sample standard deviation.
49         return deltas[degreesOfFreedom - 1] * this.sampleStandardDeviation(numberOfSamples, sum, squareSum) / Math.sqrt(numberOfSamples);
50     }
51
52     this.confidenceInterval = function (values, probability) {
53         var sum = this.sum(values);
54         var mean = sum / values.length;
55         var delta = this.confidenceIntervalDelta(probability || 0.95, values.length, sum, this.squareSum(values));
56         return [mean - delta, mean + delta];
57     }
58
59     // Welch's t-test (http://en.wikipedia.org/wiki/Welch%27s_t_test)
60     this.testWelchsT = function (values1, values2, probability) {
61         return this.computeWelchsT(values1, 0, values1.length, values2, 0, values2.length, probability).significantlyDifferent;
62     }
63
64     this.computeWelchsT = function (values1, startIndex1, length1, values2, startIndex2, length2, probability) {
65         var stat1 = sampleMeanAndVarianceForValues(values1, startIndex1, length1);
66         var stat2 = sampleMeanAndVarianceForValues(values2, startIndex2, length2);
67         var sumOfSampleVarianceOverSampleSize = stat1.variance / stat1.size + stat2.variance / stat2.size;
68         var t = Math.abs((stat1.mean - stat2.mean) / Math.sqrt(sumOfSampleVarianceOverSampleSize));
69
70         // http://en.wikipedia.org/wiki/Welch–Satterthwaite_equation
71         var degreesOfFreedom = sumOfSampleVarianceOverSampleSize * sumOfSampleVarianceOverSampleSize
72             / (stat1.variance * stat1.variance / stat1.size / stat1.size / stat1.degreesOfFreedom
73                 + stat2.variance * stat2.variance / stat2.size / stat2.size / stat2.degreesOfFreedom);
74         return {
75             t: t,
76             degreesOfFreedom: degreesOfFreedom,
77             significantlyDifferent: t > tDistributionQuantiles[probability || 0.9][Math.round(degreesOfFreedom - 1)],
78         };
79     }
80
81     function sampleMeanAndVarianceForValues(values, startIndex, length) {
82         var sum = 0;
83         for (var i = 0; i < length; i++)
84             sum += values[startIndex + i];
85         var squareSum = 0;
86         for (var i = 0; i < length; i++)
87             squareSum += values[startIndex + i] * values[startIndex + i];
88         var sampleMean = sum / length;
89         // FIXME: Maybe we should be using the biased sample variance.
90         var unbiasedSampleVariance = (squareSum - sum * sum / length) / (length - 1);
91         return {
92             mean: sampleMean,
93             variance: unbiasedSampleVariance,
94             size: length,
95             degreesOfFreedom: length - 1,
96         }
97     }
98
99     function recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, startIndex, length, minLength, segments) {
100         var tMax = 0;
101         var argTMax = null;
102         for (var i = 1; i < length - 1; i++) {
103             var firstLength = i;
104             var secondLength = length - i;
105             if (firstLength < minLength || secondLength < minLength)
106                 continue;
107             var result = Statistics.computeWelchsT(values, startIndex, firstLength, values, startIndex + i, secondLength, 0.9);
108             if (result.significantlyDifferent && result.t > tMax) {
109                 tMax = result.t;
110                 argTMax = i;
111             }
112         }
113         if (!tMax) {
114             segments.push(values.slice(startIndex, startIndex + length));
115             return;
116         }
117         recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, startIndex, argTMax, minLength, segments);
118         recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, startIndex + argTMax, length - argTMax, minLength, segments);
119     }
120
121     // One-sided t-distribution.
122     var tDistributionQuantiles = {
123         0.9: [
124             3.077684, 1.885618, 1.637744, 1.533206, 1.475884, 1.439756, 1.414924, 1.396815, 1.383029, 1.372184,
125             1.363430, 1.356217, 1.350171, 1.345030, 1.340606, 1.336757, 1.333379, 1.330391, 1.327728, 1.325341,
126             1.323188, 1.321237, 1.319460, 1.317836, 1.316345, 1.314972, 1.313703, 1.312527, 1.311434, 1.310415,
127             1.309464, 1.308573, 1.307737, 1.306952, 1.306212, 1.305514, 1.304854, 1.304230, 1.303639, 1.303077,
128             1.302543, 1.302035, 1.301552, 1.301090, 1.300649, 1.300228, 1.299825, 1.299439, 1.299069, 1.298714,
129
130             1.298373, 1.298045, 1.297730, 1.297426, 1.297134, 1.296853, 1.296581, 1.296319, 1.296066, 1.295821,
131             1.295585, 1.295356, 1.295134, 1.294920, 1.294712, 1.294511, 1.294315, 1.294126, 1.293942, 1.293763,
132             1.293589, 1.293421, 1.293256, 1.293097, 1.292941, 1.292790, 1.292643, 1.292500, 1.292360, 1.292224,
133             1.292091, 1.291961, 1.291835, 1.291711, 1.291591, 1.291473, 1.291358, 1.291246, 1.291136, 1.291029,
134             1.290924, 1.290821, 1.290721, 1.290623, 1.290527, 1.290432, 1.290340, 1.290250, 1.290161, 1.290075],
135         0.95: [
136             6.313752, 2.919986, 2.353363, 2.131847, 2.015048, 1.943180, 1.894579, 1.859548, 1.833113, 1.812461,
137             1.795885, 1.782288, 1.770933, 1.761310, 1.753050, 1.745884, 1.739607, 1.734064, 1.729133, 1.724718,
138             1.720743, 1.717144, 1.713872, 1.710882, 1.708141, 1.705618, 1.703288, 1.701131, 1.699127, 1.697261,
139             1.695519, 1.693889, 1.692360, 1.690924, 1.689572, 1.688298, 1.687094, 1.685954, 1.684875, 1.683851,
140             1.682878, 1.681952, 1.681071, 1.680230, 1.679427, 1.678660, 1.677927, 1.677224, 1.676551, 1.675905,
141
142             1.675285, 1.674689, 1.674116, 1.673565, 1.673034, 1.672522, 1.672029, 1.671553, 1.671093, 1.670649,
143             1.670219, 1.669804, 1.669402, 1.669013, 1.668636, 1.668271, 1.667916, 1.667572, 1.667239, 1.666914,
144             1.666600, 1.666294, 1.665996, 1.665707, 1.665425, 1.665151, 1.664885, 1.664625, 1.664371, 1.664125,
145             1.663884, 1.663649, 1.663420, 1.663197, 1.662978, 1.662765, 1.662557, 1.662354, 1.662155, 1.661961,
146             1.661771, 1.661585, 1.661404, 1.661226, 1.661052, 1.660881, 1.660715, 1.660551, 1.660391, 1.660234],
147         0.975: [
148             12.706205, 4.302653, 3.182446, 2.776445, 2.570582, 2.446912, 2.364624, 2.306004, 2.262157, 2.228139,
149             2.200985, 2.178813, 2.160369, 2.144787, 2.131450, 2.119905, 2.109816, 2.100922, 2.093024, 2.085963,
150             2.079614, 2.073873, 2.068658, 2.063899, 2.059539, 2.055529, 2.051831, 2.048407, 2.045230, 2.042272,
151             2.039513, 2.036933, 2.034515, 2.032245, 2.030108, 2.028094, 2.026192, 2.024394, 2.022691, 2.021075,
152             2.019541, 2.018082, 2.016692, 2.015368, 2.014103, 2.012896, 2.011741, 2.010635, 2.009575, 2.008559,
153
154             2.007584, 2.006647, 2.005746, 2.004879, 2.004045, 2.003241, 2.002465, 2.001717, 2.000995, 2.000298,
155             1.999624, 1.998972, 1.998341, 1.997730, 1.997138, 1.996564, 1.996008, 1.995469, 1.994945, 1.994437,
156             1.993943, 1.993464, 1.992997, 1.992543, 1.992102, 1.991673, 1.991254, 1.990847, 1.990450, 1.990063,
157             1.989686, 1.989319, 1.988960, 1.988610, 1.988268, 1.987934, 1.987608, 1.987290, 1.986979, 1.986675,
158             1.986377, 1.986086, 1.985802, 1.985523, 1.985251, 1.984984, 1.984723, 1.984467, 1.984217, 1.983972],
159         0.99: [
160             31.820516, 6.964557, 4.540703, 3.746947, 3.364930, 3.142668, 2.997952, 2.896459, 2.821438, 2.763769,
161             2.718079, 2.680998, 2.650309, 2.624494, 2.602480, 2.583487, 2.566934, 2.552380, 2.539483, 2.527977,
162             2.517648, 2.508325, 2.499867, 2.492159, 2.485107, 2.478630, 2.472660, 2.467140, 2.462021, 2.457262,
163             2.452824, 2.448678, 2.444794, 2.441150, 2.437723, 2.434494, 2.431447, 2.428568, 2.425841, 2.423257,
164             2.420803, 2.418470, 2.416250, 2.414134, 2.412116, 2.410188, 2.408345, 2.406581, 2.404892, 2.403272,
165
166             2.401718, 2.400225, 2.398790, 2.397410, 2.396081, 2.394801, 2.393568, 2.392377, 2.391229, 2.390119,
167             2.389047, 2.388011, 2.387008, 2.386037, 2.385097, 2.384186, 2.383302, 2.382446, 2.381615, 2.380807,
168             2.380024, 2.379262, 2.378522, 2.377802, 2.377102, 2.376420, 2.375757, 2.375111, 2.374482, 2.373868,
169             2.373270, 2.372687, 2.372119, 2.371564, 2.371022, 2.370493, 2.369977, 2.369472, 2.368979, 2.368497,
170             2.368026, 2.367566, 2.367115, 2.366674, 2.366243, 2.365821, 2.365407, 2.365002, 2.364606, 2.364217]
171     };
172
173     this.MovingAverageStrategies = [
174         {
175             id: 1,
176             label: 'Simple Moving Average',
177             parameterList: [
178                 {label: "Backward window size", value: 8, min: 2, step: 1},
179                 {label: "Forward window size", value: 4, min: 0, step: 1}
180             ],
181             execute: function (backwardWindowSize, forwardWindowSize, values) {
182                 var averages = new Array(values.length);
183                 // We use naive O(n^2) algorithm for simplicy as well as to avoid accumulating round-off errors.
184                 for (var i = 0; i < values.length; i++) {
185                     var sum = 0;
186                     var count = 0;
187                     for (var j = i - backwardWindowSize; j < i + backwardWindowSize; j++) {
188                         if (j >= 0 && j < values.length) {
189                             sum += values[j];
190                             count++;
191                         }
192                     }
193                     averages[i] = sum / count;
194                 }
195                 return averages;
196             },
197
198         },
199         {
200             id: 2,
201             label: 'Cumulative Moving Average',
202             execute: function (values) {
203                 var averages = new Array(values.length);
204                 var sum = 0;
205                 for (var i = 0; i < values.length; i++) {
206                     sum += values[i];
207                     averages[i] = sum / (i + 1);
208                 }
209                 return averages;
210             }
211         },
212         {
213             id: 3,
214             label: 'Exponential Moving Average',
215             parameterList: [{label: "Smoothing factor", value: 0.1, min: 0.001, max: 0.9}],
216             execute: function (smoothingFactor, values) {
217                 if (!values.length || typeof(smoothingFactor) !== "number")
218                     return null;
219
220                 var averages = new Array(values.length);
221                 var movingAverage = 0;
222                 averages[0] = values[0];
223                 for (var i = 1; i < values.length; i++)
224                     averages[i] = smoothingFactor * values[i] + (1 - smoothingFactor) * averages[i - 1];
225                 return averages;
226             }
227         },
228         {
229             id: 4,
230             label: 'Segmentation: Recursive t-test',
231             description: "Recursively split values into two segments if Welch's t-test detects a statistically significant difference.",
232             parameterList: [{label: "Minimum segment length", value: 20, min: 5}],
233             execute: function (minLength, values) {
234                 if (values.length < 2)
235                     return null;
236
237                 var averages = new Array(values.length);
238                 var segments = new Array;
239                 recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, 0, values.length, minLength, segments);
240                 var averageIndex = 0;
241                 for (var j = 0; j < segments.length; j++) {
242                     var values = segments[j];
243                     var mean = Statistics.sum(values) / values.length;
244                     for (var i = 0; i < values.length; i++)
245                         averages[averageIndex++] = mean;
246                 }
247
248                 return averages;
249             }
250         },
251         {
252             id: 5,
253             label: 'Segmentation: Schwarz criterion',
254             description: 'Adaptive algorithm that maximizes the Schwarz criterion (BIC).',
255             // Based on Detection of Multiple Change–Points in Multivariate Time Series by Marc Lavielle (July 2006).
256             execute: function (values) {
257                 if (values.length < 2)
258                     return null;
259
260                 var averages = new Array(values.length);
261                 var averageIndex = 0;
262
263                 // Split the time series into grids since splitIntoSegmentsUntilGoodEnough is O(n^2).
264                 var gridLength = 500;
265                 var totalSegmentation = [0];
266                 for (var gridCount = 0; gridCount < Math.ceil(values.length / gridLength); gridCount++) {
267                     var gridValues = values.slice(gridCount * gridLength, (gridCount + 1) * gridLength);
268                     var segmentation = splitIntoSegmentsUntilGoodEnough(gridValues);
269
270                     if (Statistics.debuggingSegmentation)
271                         console.log('grid=' + gridCount, segmentation);
272
273                     for (var i = 1; i < segmentation.length - 1; i++)
274                         totalSegmentation.push(gridCount * gridLength + segmentation[i]);
275                 }
276
277                 if (Statistics.debuggingSegmentation)
278                     console.log('Final Segmentation', totalSegmentation);
279
280                 totalSegmentation.push(values.length);
281
282                 for (var i = 1; i < totalSegmentation.length; i++) {
283                     var segment = values.slice(totalSegmentation[i - 1], totalSegmentation[i]);
284                     var average = Statistics.sum(segment) / segment.length;
285                     for (var j = 0; j < segment.length; j++)
286                         averages[averageIndex++] = average;
287                 }
288
289                 return averages;
290             }
291         },
292     ];
293
294     this.debuggingSegmentation = false;
295
296     function splitIntoSegmentsUntilGoodEnough(values) {
297         if (values.length < 2)
298             return [0, values.length];
299
300         var matrix = new SampleVarianceUpperTriangularMatrix(values);
301
302         var SchwarzCriterionBeta = Math.log1p(values.length - 1) / values.length;
303
304         var BirgeAndMassartC = 2.5; // Suggested by the authors.
305         var BirgeAndMassartPenalization = function (segmentCount) {
306             return segmentCount * (1 + BirgeAndMassartC * Math.log1p(values.length / segmentCount - 1));
307         }
308
309         var segmentation;
310         var minTotalCost = Infinity;
311         var maxK = 50;
312
313         for (var k = 1; k < maxK; k++) {
314             var start = Date.now();
315             var result = findOptimalSegmentation(values, matrix, k);
316             var cost = result.cost / values.length;
317             var penalty = SchwarzCriterionBeta * BirgeAndMassartPenalization(k);
318             if (cost + penalty < minTotalCost) {
319                 minTotalCost = cost + penalty;
320                 segmentation = result.segmentation;
321             } else
322                 maxK = Math.min(maxK, k + 3);
323             if (Statistics.debuggingSegmentation)
324                 console.log('splitIntoSegmentsUntilGoodEnough', k, Date.now() - start, cost + penalty);
325         }
326
327         return segmentation;
328     }
329
330     function findOptimalSegmentation(values, costMatrix, segmentCount) {
331         // Dynamic programming. cost[i][k] = The cost to segmenting values up to i into k segments.
332         var cost = new Array(values.length);
333         for (var i = 0; i < values.length; i++) {
334             cost[i] = new Float32Array(segmentCount + 1);
335         }
336
337         var previousNode = new Array(values.length);
338         for (var i = 0; i < values.length; i++)
339             previousNode[i] = new Array(segmentCount + 1);
340
341         cost[0] = [0]; // The cost of segmenting single value is always 0.
342         previousNode[0] = [-1];
343         for (var segmentStart = 0; segmentStart < values.length; segmentStart++) {
344             var costBySegment = cost[segmentStart];
345             for (var count = 0; count < segmentCount; count++) {
346                 if (previousNode[segmentStart][count] === undefined)
347                     continue;
348                 for (var segmentEnd = segmentStart + 1; segmentEnd < values.length; segmentEnd++) {
349                     var newCost = costBySegment[count] + costMatrix.costBetween(segmentStart, segmentEnd);
350                     if (previousNode[segmentEnd][count + 1] === undefined || newCost < cost[segmentEnd][count + 1]) {
351                         cost[segmentEnd][count + 1] = newCost;
352                         previousNode[segmentEnd][count + 1] = segmentStart;
353                     }
354                 }
355             }
356         }
357
358         if (Statistics.debuggingSegmentation) {
359             console.log('findOptimalSegmentation with k=', segmentCount);
360             for (var i = 0; i < cost.length; i++) {
361                 var t = cost[i];
362                 var s = '';
363                 for (var j = 0; j < t.length; j++) {
364                     var p = previousNode[i][j];
365                     s += '(k=' + j;
366                     if (p !== undefined)
367                         s += ' c=' + t[j] + ' p=' + p
368                     s += ')';
369                 }
370                 console.log(i, values[i], s);
371             }
372         }
373
374         var currentIndex = values.length - 1;
375         var segmentation = new Array(segmentCount);
376         segmentation[0] = values.length;
377         for (var i = 0; i < segmentCount; i++) {
378             currentIndex = previousNode[currentIndex][segmentCount - i];
379             segmentation[i + 1] = currentIndex;
380         }
381
382         return {segmentation: segmentation.reverse(), cost: cost[values.length - 1][segmentCount]};
383     }
384
385     function SampleVarianceUpperTriangularMatrix(values) {
386         // The cost of segment (i, j].
387         var costMatrix = new Array(values.length - 1);
388         for (var i = 0; i < values.length - 1; i++) {
389             var remainingValueCount = values.length - i - 1;
390             costMatrix[i] = new Float32Array(remainingValueCount);
391             var sum = values[i];
392             var squareSum = sum * sum;
393             costMatrix[i][0] = 0;
394             for (var j = i + 1; j < values.length; j++) {
395                 var currentValue = values[j];
396                 sum += currentValue;
397                 squareSum += currentValue * currentValue;
398                 var sampleSize = j - i + 1;
399                 var stdev = Statistics.sampleStandardDeviation(sampleSize, sum, squareSum);
400                 costMatrix[i][j - i - 1] = sampleSize * Math.log1p(stdev * stdev - 1);
401             }
402         }
403         this.costMatrix = costMatrix;
404     }
405
406     SampleVarianceUpperTriangularMatrix.prototype.costBetween = function(from, to) {
407         if (from >= this.costMatrix.length || from == to)
408             return 0; // The cost of the segment that starts at the last data point is 0.
409         return this.costMatrix[from][to - from - 1];
410     }
411
412     this.EnvelopingStrategies = [
413         {
414             id: 100,
415             label: 'Average Difference',
416             description: 'The average difference between consecutive values.',
417             execute: function (values, movingAverages) {
418                 if (values.length < 1)
419                     return NaN;
420
421                 var diff = 0;
422                 for (var i = 1; i < values.length; i++)
423                     diff += Math.abs(values[i] - values[i - 1]);
424
425                 return diff / values.length;
426             }
427         },
428         {
429             id: 101,
430             label: 'Moving Average Standard Deviation',
431             description: 'The square root of the average deviation from the moving average with Bessel\'s correction.',
432             execute: function (values, movingAverages) {
433                 if (values.length < 1)
434                     return NaN;
435
436                 var diffSquareSum = 0;
437                 for (var i = 1; i < values.length; i++) {
438                     var diff = (values[i] - movingAverages[i]);
439                     diffSquareSum += diff * diff;
440                 }
441
442                 return Math.sqrt(diffSquareSum / (values.length - 1));
443             }
444         },
445     ];
446
447     function createWesternElectricRule(windowSize, minOutlinerCount, limitFactor) {
448         return function (values, movingAverages, deviation) {
449             var results = new Array(values.length);
450             var limit = limitFactor * deviation;
451             for (var i = 0; i < values.length; i++)
452                 results[i] = countValuesOnSameSide(values, movingAverages, limit, i, windowSize) >= minOutlinerCount ? windowSize : 0;
453             return results;
454         }
455     }
456
457     function countValuesOnSameSide(values, movingAverages, limit, startIndex, windowSize) {
458         var valuesAboveLimit = 0;
459         var valuesBelowLimit = 0;
460         var center = movingAverages[startIndex];
461         for (var i = startIndex; i < startIndex + windowSize && i < values.length; i++) {
462             var diff = values[i] - center;
463             valuesAboveLimit += (diff > limit);
464             valuesBelowLimit += (diff < -limit);
465         }
466         return Math.max(valuesAboveLimit, valuesBelowLimit);
467     }
468     window.countValuesOnSameSide = countValuesOnSameSide;
469
470     this.AnomalyDetectionStrategy = [
471         // Western Electric rules: http://en.wikipedia.org/wiki/Western_Electric_rules
472         {
473             id: 200,
474             label: 'Western Electric: any point beyond 3σ',
475             description: 'Any single point falls outside 3σ limit from the moving average',
476             execute: createWesternElectricRule(1, 1, 3),
477         },
478         {
479             id: 201,
480             label: 'Western Electric: 2/3 points beyond 2σ',
481             description: 'Two out of three consecutive points fall outside 2σ limit from the moving average on the same side',
482             execute: createWesternElectricRule(3, 2, 2),
483         },
484         {
485             id: 202,
486             label: 'Western Electric: 4/5 points beyond σ',
487             description: 'Four out of five consecutive points fall outside 2σ limit from the moving average on the same side',
488             execute: createWesternElectricRule(5, 4, 1),
489         },
490         {
491             id: 203,
492             label: 'Western Electric: 9 points on same side',
493             description: 'Nine consecutive points on the same side of the moving average',
494             execute: createWesternElectricRule(9, 9, 0),
495         },
496         {
497             id: 210,
498             label: 'Mozilla: t-test 5 vs. 20 before that',
499             description: "Use student's t-test to determine whether the mean of the last five data points differs from the mean of the twenty values before that",
500             execute: function (values, movingAverages, deviation) {
501                 var results = new Array(values.length);
502                 var p = false;
503                 for (var i = 20; i < values.length - 5; i++)
504                     results[i] = Statistics.testWelchsT(values.slice(i - 20, i), values.slice(i, i + 5), 0.99) ? 5 : 0;
505                 return results;
506             }
507         },
508     ]
509
510 })();
511
512 if (typeof module != 'undefined') {
513     for (var key in Statistics)
514         module.exports[key] = Statistics[key];
515 }