lighthouse/shared/statistics.js

104 строки
3.8 KiB
JavaScript
Исходник Постоянная ссылка Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// The exact double values for the max and min scores possible in each range.
const MIN_PASSING_SCORE = 0.90000000000000002220446049250313080847263336181640625;
const MAX_AVERAGE_SCORE = 0.899999999999999911182158029987476766109466552734375;
const MIN_AVERAGE_SCORE = 0.5;
const MAX_FAILING_SCORE = 0.499999999999999944488848768742172978818416595458984375;
/**
* Approximates the Gauss error function, the probability that a random variable
* from the standard normal distribution lies within [-x, x]. Moved from
* traceviewer.b.math.erf, based on Abramowitz and Stegun, formula 7.1.26.
* @param {number} x
* @return {number}
*/
function erf(x) {
// erf(-x) = -erf(x);
const sign = Math.sign(x);
x = Math.abs(x);
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const t = 1 / (1 + p * x);
const y = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5))));
return sign * (1 - y * Math.exp(-x * x));
}
/**
* Returns the score (1 - percentile) of `value` in a log-normal distribution
* specified by the `median` value, at which the score will be 0.5, and a 10th
* percentile value, at which the score will be 0.9. The score represents the
* amount of the distribution greater than `value`. All values should be in the
* same units (e.g. milliseconds). See
* https://www.desmos.com/calculator/o98tbeyt1t
* for an interactive view of the relationship between these parameters and the
* typical parameterization (location and shape) of the log-normal distribution.
* @param {{median: number, p10: number}} parameters
* @param {number} value
* @return {number}
*/
function getLogNormalScore({median, p10}, value) {
// Required for the log-normal distribution.
if (median <= 0) throw new Error('median must be greater than zero');
if (p10 <= 0) throw new Error('p10 must be greater than zero');
// Not strictly required, but if p10 > median, it flips around and becomes the p90 point.
if (p10 >= median) throw new Error('p10 must be less than the median');
// Non-positive values aren't in the distribution, so always 1.
if (value <= 0) return 1;
// Closest double to `erfc-1(1/5)`.
const INVERSE_ERFC_ONE_FIFTH = 0.9061938024368232;
// Shape (σ) is `|log(p10/median) / (sqrt(2)*erfc^-1(1/5))|` and
// standardizedX is `1/2 erfc(log(value/median) / (sqrt(2)*σ))`, so simplify a bit.
const xRatio = Math.max(Number.MIN_VALUE, value / median); // value and median are > 0, so is ratio.
const xLogRatio = Math.log(xRatio);
const p10Ratio = Math.max(Number.MIN_VALUE, p10 / median); // p10 and median are > 0, so is ratio.
const p10LogRatio = -Math.log(p10Ratio); // negate to keep σ positive.
const standardizedX = xLogRatio * INVERSE_ERFC_ONE_FIFTH / p10LogRatio;
const complementaryPercentile = (1 - erf(standardizedX)) / 2;
// Clamp to avoid floating-point out-of-bounds issues and keep score in expected range.
let score;
if (value <= p10) {
// Passing. Clamp to [0.9, 1].
score = Math.max(MIN_PASSING_SCORE, Math.min(1, complementaryPercentile));
} else if (value <= median) {
// Average. Clamp to [0.5, 0.9).
score = Math.max(MIN_AVERAGE_SCORE, Math.min(MAX_AVERAGE_SCORE, complementaryPercentile));
} else {
// Failing. Clamp to [0, 0.5).
score = Math.max(0, Math.min(MAX_FAILING_SCORE, complementaryPercentile));
}
return score;
}
/**
* Interpolates the y value at a point x on the line defined by (x0, y0) and (x1, y1)
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} x
* @return {number}
*/
function linearInterpolation(x0, y0, x1, y1, x) {
const slope = (y1 - y0) / (x1 - x0);
return y0 + (x - x0) * slope;
}
export {
linearInterpolation,
getLogNormalScore,
};