Websites/perf.webkit.org/ChangeLog

 12015-04-03 Ryosuke Niwa <rniwa@webkit.org>
 2
 3 Add time series segmentation algorithms as moving averages
 4 https://bugs.webkit.org/show_bug.cgi?id=143362
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 This patch implements two preliminary time series segmentation algorithms as moving averages.
 9
 10 Recursive t-test: Compute Welch's t-statistic at each point in a given segment of the time series.
 11 If Welch's t-test implicates a statistically significance difference, then split the segment into two
 12 sub segments with the maximum t-statistic (i.e. the point at which if split would yield the highest
 13 probability that two segments do not share the same "underlying" mean in classical / frequentist sense).
 14 We repeat this process recursively. See [1] for the evaluation of this particular algorithm.
 15
 16 Schwarz criterion: Use Schwarz or Bayesian information criterion to heuristically find the optimal
 17 segmentation. Intuitively, the problem of finding the best segmentation comes down to minimizing the
 18 residual sum of squares in each segment as in linear regressions. That is, for a given segment with
 19 values y_1 through y_n with mean y_avg, we want to minimize the sum of (y_i - y_avg)^2 over i = 1
 20 through i = n. However, we also don't want to split every data point into a separate segment so we need
 21 to account the "cost" of introducing new segments. We use a cost function that's loosely based on two
 22 models discussed in [2] for simplicity. We will tune this cost function further in the future.
 23
 24 The problem of finding the best segmentation then reduces to a search problem. Unfortunately, our problem
 25 space is exponential with respect to the size of the time series since we could split at each data point.
 26 We workaround this problem by first splitting the time series into a manageable smaller grids, and only
 27 considering segmentation of a fixed size (i.e. the number of segments is constant). Since time series
 28 tend to contain a lot more data points than segments, this strategy finds the optimal solution without
 29 exploring much of the problem space.
 30
 31 Finding the optimal segmentation of a fixed size is, itself, another search problem that is equivalent to
 32 finding the shortest path of a fixed length in DAG. Here, we use dynamic programming with a matrix of size
 33 n by n where n is the length of the time series (grid). Each entry in this matrix at (i, k) stores
 34 the minimum cost of segmenting data points 1 through i using k segments. We start our search at i = 0.
 35 Clearly C(1, 0) = 0 (note the actual code uses 0-based index). In i-th iteration, we compute the cost
 36 S(i, j) of each segment starting at i and ending at another point j after i and update C(j, k + 1) by
 37 min( C(j, k + 1), C(i, k) + S(i, j) ) for all values of j above i.
 38
 39 [1] Kensuke Fukuda, H. Eugene Stanley, and Luis A. Nunes Amaral, "Heuristic segmentation of
 40 a nonstationary time series", Physical Review E 69, 021108 (2004)
 41
 42 [2] Marc Lavielle, Gilles Teyssi`ere, "Detection of Multiple Change–Points in Multivariate Time Series"
 43 Lithuanian Mathematical Journal, vol 46, 2006
 44
 45 * public/v2/index.html: Show the optional description for the chosen moving average strategy.
 46 * public/v2/js/statistics.js:
 47 (Statistics.testWelchsT):
 48 (Statistics.computeWelchsT): Extracted from testWelchsT. Generalized to take the offset and the length
 49 of each value array between which Welch's t-statistic is computed. This generalization helps the
 50 Schwarz criterion segmentation algorithm avoid splitting values array O(n^2) times.
 51 (.sampleMeanAndVarianceForValues): Ditto for the generalization.
 52 (.recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent): Added. Implements recursive t-test.
 53 (.splitIntoSegmentsUntilGoodEnough): Added. Implements Schwarz criterion.
 54 (.findOptimalSegmentation): Added. Implements the algorithm to find the optimal segmentation of a fixed
 55 segment count.
 56 (.SampleVarianceUpperTriangularMatrix): Added. Stores S(i, j) used by findOptimalSegmentation.
 57 (.SampleVarianceUpperTriangularMatrix.prototype.costBetween): Added.
 58
1592015-04-02 Ryosuke Niwa <rniwa@webkit.org>
260
361 Perf dashboard should have UI to test out anomaly detection strategies
182308

Websites/perf.webkit.org/public/v2/index.html

370370 optionValuePath='content'
371371 optionLabelPath='content.label'
372372 selection=chosenMovingAverageStrategy}}</label>
 373 {{#if chosenMovingAverageStrategy.description}}
 374 <p class="description">{{chosenMovingAverageStrategy.description}}</p>
 375 {{/if}}
373376 {{#each chosenMovingAverageStrategy.parameterList}}
374377 <label>{{label}}: {{input type="number" value=value min=min max=max step=step}}</label>
375378 {{/each}}
182308

Websites/perf.webkit.org/public/v2/js/statistics.js

@@var Statistics = new (function () {
5858
5959 // Welch's t-test (http://en.wikipedia.org/wiki/Welch%27s_t_test)
6060 this.testWelchsT = function (values1, values2, probability) {
61  var stat1 = sampleMeanAndVarianceForValues(values1);
62  var stat2 = sampleMeanAndVarianceForValues(values2);
 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);
6367 var sumOfSampleVarianceOverSampleSize = stat1.variance / stat1.size + stat2.variance / stat2.size;
64  var t = (stat1.mean - stat2.mean) / Math.sqrt(sumOfSampleVarianceOverSampleSize);
 68 var t = Math.abs((stat1.mean - stat2.mean) / Math.sqrt(sumOfSampleVarianceOverSampleSize));
6569
6670 // http://en.wikipedia.org/wiki/Welch–Satterthwaite_equation
6771 var degreesOfFreedom = sumOfSampleVarianceOverSampleSize * sumOfSampleVarianceOverSampleSize
6872 / (stat1.variance * stat1.variance / stat1.size / stat1.size / stat1.degreesOfFreedom
6973 + stat2.variance * stat2.variance / stat2.size / stat2.size / stat2.degreesOfFreedom);
70 
71  // They're different beyond the confidence interval of the specified probability.
72  return Math.abs(t) > tDistributionQuantiles[probability || 0.9][Math.round(degreesOfFreedom - 1)];
 74 return {
 75 t: t,
 76 degreesOfFreedom: degreesOfFreedom,
 77 significantlyDifferent: t > tDistributionQuantiles[probability || 0.9][Math.round(degreesOfFreedom - 1)],
 78 };
7379 }
7480
75  function sampleMeanAndVarianceForValues(values) {
76  var sum = Statistics.sum(values);
77  var squareSum = Statistics.squareSum(values);
78  var sampleMean = sum / values.length;
 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;
7989 // FIXME: Maybe we should be using the biased sample variance.
80  var unbiasedSampleVariance = (squareSum - sum * sum / values.length) / (values.length - 1);
 90 var unbiasedSampleVariance = (squareSum - sum * sum / length) / (length - 1);
8191 return {
8292 mean: sampleMean,
8393 variance: unbiasedSampleVariance,
84  size: values.length,
85  degreesOfFreedom: values.length - 1,
 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;
86116 }
 117 recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, startIndex, argTMax, minLength, segments);
 118 recursivelySplitIntoTwoSegmentsAtMaxTIfSignificantlyDifferent(values, startIndex + argTMax, length - argTMax, minLength, segments);
87119 }
88120
89121 // One-sided t-distribution.

@@var Statistics = new (function () {
193225 return averages;
194226 }
195227 },
 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: 'Adoptive algorithnm 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 },
196292 ];
197293
 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
198412 this.EnvelopingStrategies = [
199413 {
200414 id: 100,
182308