Resources Numerical accuracy Calculation policy

Weibull life data

Weibull shape and scale fitted to failure times, with confidence bounds, mean life and B10 life.

How the precision is kept

The maximum-likelihood shape is found by bisection down to adjacent 64-bit numbers; the scale follows in closed form. Confidence bounds are a normal approximation from a finite-difference Hessian with z values of 4 digits. The distribution functions subtract from 1 and lose digits for very small probabilities.

Open item. Compute z from the confidence level asked for instead of choosing among three values.

Calculations under this policy

6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

weibull.js Shared shell, every app 64-bit 6 calculations Reviewed

Tests bin/test-engines.mjs, tools/risk-test/test.mjs

Gamma function

64-bit

Γ(x), used for the mean life.

Lanczos approximation, g = 7, 9 coefficients; reflection below 0.5

A fixed approximation; the code comment gives a relative error of about 1e-14. It sets the digits of the mean life.

  • weibull.js
  • lines 4 to 11
/** Γ(x) for x > 0, Lanczos approximation (g = 7, n = 9), relative error about 1e-14. */
export function gamma(x) {
  const g = 7, c = [0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7];
  if (x < 0.5) return Math.PI / (Math.sin(Math.PI * x) * gamma(1 - x));
  x -= 1; let a = c[0]; const t = x + g + 0.5;
  for (let i = 1; i < g + 2; i++) a += c[i] / (x + i);
  return Math.sqrt(2 * Math.PI) * Math.pow(t, x + 0.5) * Math.exp(-t) * a;
}

Weibull distribution functions

64-bit

Failure probability, reliability, density, hazard, quantile, mean and B-life.

F = 1 − e^(−(t ÷ η)^β); quantile = η (−ln(1 − F))^(1 ÷ β); mean = η Γ(1 + 1 ÷ β)

The failure probability is 1 minus an exponential, not −expm1, so a probability near 1e-6 keeps about 10 digits and smaller ones fewer. The quantile and B-life use ln(1 − p) without log1p, with the same loss for small p.

  • weibull.js
  • lines 13 to 22
export const W = {
  cdf: (t, b, e) => (t <= 0 ? 0 : 1 - Math.exp(-Math.pow(t / e, b))),
  reliability: (t, b, e) => (t <= 0 ? 1 : Math.exp(-Math.pow(t / e, b))),
  pdf: (t, b, e) => (t <= 0 ? 0 : (b / e) * Math.pow(t / e, b - 1) * Math.exp(-Math.pow(t / e, b))),
  hazard: (t, b, e) => (b / e) * Math.pow(Math.max(t, 0) / e, b - 1),
  quantile: (F, b, e) => e * Math.pow(-Math.log(1 - F), 1 / b),
  mean: (b, e) => e * gamma(1 + 1 / b),
  /** The life by which a share p of units has failed (B10 = life(0.1)). */
  life: (p, b, e) => e * Math.pow(-Math.log(1 - p), 1 / b),
};

Log-likelihood

64-bit

How well a shape and scale explain the failure and suspension times.

Σ failures ln f(t) + Σ suspended ln R(t)

A plain sum of logarithms and powers in data order, in 64-bit.

  • weibull.js
  • lines 37 to 38
/** Log-likelihood of (β, η): Σ_failures ln f(t) + Σ_suspended ln R(t). */
export function logLik(d, b, e) { let s = 0; for (const x of d) s += x.c ? -Math.pow(x.t / e, b) : Math.log(b / e) + (b - 1) * Math.log(x.t / e) - Math.pow(x.t / e, b); return s; }

Maximum-likelihood fit

64-bit

Shape, scale and their confidence bounds from failure times, with suspensions.

g(β) = Σ tᵝ ln t ÷ Σ tᵝ − 1 ÷ β − mean ln t = 0; η = (Σ tᵝ ÷ r)^(1 ÷ β)

Times are divided by the largest before the power, which avoids overflow for large β. Two hundred bisections bring β to adjacent 64-bit numbers, so the shape is the root of g as evaluated. The bounds use second differences of the log-likelihood with a step of 1e-4, whose rounding error is about 1e-16 × |log-likelihood| ÷ 1e-8, and z values of 4 digits; any level other than 0.95 or 0.99 uses 1.645.

  • weibull.js
  • lines 40 to 62
export function fitMLE(times, censored = null, { level = 0.9 } = {}) {
  const { d, fails, probs } = prep(times, censored);
  if (probs.length) return { ok: false, problems: probs, method: M_W };
  const r = fails.length, sumLn = fails.reduce((a, x) => a + Math.log(x.t), 0), tmax = Math.max(...d.map((x) => x.t));
  const g = (b) => { let s0 = 0, s1 = 0; for (const x of d) { const w = Math.pow(x.t / tmax, b); s0 += w; s1 += w * Math.log(x.t); } return s1 / s0 - 1 / b - sumLn / r; };
  let lo = 0.01, hi = 200; // g is increasing in β
  if (g(lo) > 0 || g(hi) < 0) return { ok: false, problems: [problem('no-root', 'error', 'The likelihood has no maximum for these times (all alike?).', [])], method: M_W };
  for (let i = 0; i < 200; i++) { const mid = (lo + hi) / 2; if (g(mid) > 0) hi = mid; else lo = mid; }
  const b = (lo + hi) / 2;
  const e = tmax * Math.pow(d.reduce((a, x) => a + Math.pow(x.t / tmax, b), 0) / r, 1 / b);
  // observed information in (ln β, ln η) by central differences
  const f = (u, v) => logLik(d, Math.exp(u), Math.exp(v)), u0 = Math.log(b), v0 = Math.log(e), h = 1e-4;
  const huu = (f(u0 + h, v0) - 2 * f(u0, v0) + f(u0 - h, v0)) / (h * h), hvv = (f(u0, v0 + h) - 2 * f(u0, v0) + f(u0, v0 - h)) / (h * h);
  const huv = (f(u0 + h, v0 + h) - f(u0 + h, v0 - h) - f(u0 - h, v0 + h) + f(u0 - h, v0 - h)) / (4 * h * h);
  const det = huu * hvv - huv * huv, varU = -hvv / det, varV = -huu / det;
  const z = level === 0.95 ? 1.96 : level === 0.99 ? 2.576 : 1.645;
  const bounds = det > 0 && varU > 0 && varV > 0 ? { level, beta: [Math.exp(u0 - z * Math.sqrt(varU)), Math.exp(u0 + z * Math.sqrt(varU))], eta: [Math.exp(v0 - z * Math.sqrt(varV)), Math.exp(v0 + z * Math.sqrt(varV))] } : null;
  const out = [];
  if (r < 20) out.push(problem('few-failures', 'info', `Only ${r} failure${r === 1 ? '' : 's'}: the confidence bounds are rough.`, [], { found: r, req: 20 }));
  if (!bounds) out.push(problem('no-bounds', 'warning', 'Confidence bounds could not be worked out for these data.', []));
  return { ok: true, beta: b, eta: e, failures: r, suspended: d.length - r, logLik: logLik(d, b, e), bounds, mean: W.mean(b, e), b10: W.life(0.1, b, e),
    shape: b < 0.95 ? 'infant mortality (falling hazard)' : b > 1.05 ? 'wear-out (rising hazard)' : 'about constant hazard (random failures)', problems: out, method: M_W };
}

Median ranks

64-bit

The plotting position of each failure, adjusted for suspended units.

F = (rank − 0.3) ÷ (n + 0.4); rank increment = (n + 1 − previous rank) ÷ (1 + units at or after this one)

Bernard's formula is an approximation to the median rank; it, not rounding, sets how close the plotted positions are to the true median ranks.

  • weibull.js
  • lines 64 to 70
/** Median ranks: Johnson's adjusted rank for suspended data, Bernard's approximation (i − 0.3)/(n + 0.4). */
export function medianRanks(times, censored = null) {
  const d = times.map((t, i) => ({ t, c: !!(censored && censored[i]) })).sort((a, b) => a.t - b.t || (a.c ? 1 : 0) - (b.c ? 1 : 0)), n = d.length, out = [];
  let prev = 0;
  d.forEach((x, k) => { if (x.c) return; const inc = (n + 1 - prev) / (1 + (n - k)); prev += inc; out.push({ t: x.t, order: prev, F: (prev - 0.3) / (n + 0.4) }); });
  return out;
}

Rank regression fit

64-bit

Shape and scale from a straight line on the Weibull plot.

β = S_xy ÷ S_xx on (ln t, ln(−ln(1 − F))); η = e^(−a ÷ β)

Least squares with sums of deviations from the means, so no large totals are subtracted. An R² below 0.9 raises a warning.

  • weibull.js
  • lines 72 to 82
/** Rank regression (Y on X) on the Weibull plot: ln t against ln(−ln(1 − F)). Also returns the points for drawing the plot. */
export function fitMRR(times, censored = null) {
  const { fails, probs } = prep(times, censored);
  if (probs.length) return { ok: false, problems: probs, method: M_W };
  const pts = medianRanks(times, censored), X = pts.map((p) => Math.log(p.t)), Y = pts.map((p) => Math.log(-Math.log(1 - p.F)));
  const n = pts.length, mx = X.reduce((a, b) => a + b, 0) / n, my = Y.reduce((a, b) => a + b, 0) / n;
  let sxy = 0, sxx = 0, syy = 0; for (let i = 0; i < n; i++) { sxy += (X[i] - mx) * (Y[i] - my); sxx += (X[i] - mx) ** 2; syy += (Y[i] - my) ** 2; }
  const b = sxy / sxx, a = my - b * mx, e = Math.exp(-a / b), r2 = syy > 0 ? (sxy * sxy) / (sxx * syy) : 1;
  const out = r2 < 0.9 ? [problem('poor-fit', 'warning', `The points do not lie on a straight Weibull plot (R² ${r2.toFixed(2)}): mixed failure modes, or not Weibull.`, [], { found: r2, req: 0.9 })] : [];
  return { ok: true, beta: b, eta: e, r2, points: pts.map((p, i) => ({ ...p, x: X[i], y: Y[i] })), failures: fails.length, mean: W.mean(b, e), b10: W.life(0.1, b, e), problems: out, method: M_W };
}

In detail

Method
Maximum likelihood with right-censoring (the file cites Nelson 1982 and Meeker and Escobar 1998); median-rank regression with Bernard's approximation and Johnson's adjusted ranks; Lanczos gamma function.
Number types
64-bit numbers.
Tolerances
Bisection on the shape between 0.01 and 200, 200 halvings. Hessian step 1e-4 in ln β and ln η.
Checked by
The engines test checks that an example with 10 failures is fitted with a shape above 1.
Limits
z is 1.645, 1.96 or 2.576; any confidence level other than 0.95 or 0.99 is computed with 1.645 and still returned with the level asked for. The file states that bounds are rough below about 20 failures.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach