Add better smoothing with ema rounding of the ends of the loss graph for first and last instance do not dominate the smoothing factors.
This commit is contained in:
@@ -42,20 +42,58 @@ function computeCanvasSize(host: HTMLElement): { width: number; height: number }
|
|||||||
return { width, height: Math.max(MIN_CANVAS_HEIGHT, height - legendH) };
|
return { width, height: Math.max(MIN_CANVAS_HEIGHT, height - legendH) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// EMA over a (number|null)[] series. Nulls are preserved as gaps and do not
|
// One-directional bias-corrected EMA. The accumulator starts at 0 and each
|
||||||
// advance the running average.
|
// output is divided by w = 1-(1-alpha)^n (n = valid points seen so far) so early
|
||||||
function emaWithNulls(ys: (number | null)[], alpha: number): (number | null)[] {
|
// outputs reflect the running mean rather than the raw accumulator. `w` doubles
|
||||||
const out: (number | null)[] = new Array(ys.length);
|
// as a confidence weight (→0 with one point seen, →1 once warmed up) used to
|
||||||
let prev: number | null = null;
|
// combine the two passes below. Nulls are preserved as gaps and do not advance
|
||||||
for (let i = 0; i < ys.length; i++) {
|
// the average. `reverse` walks the series back-to-front (the backward pass).
|
||||||
|
function emaPass(
|
||||||
|
ys: (number | null)[],
|
||||||
|
alpha: number,
|
||||||
|
reverse: boolean,
|
||||||
|
): { vals: (number | null)[]; weights: number[] } {
|
||||||
|
const vals: (number | null)[] = new Array(ys.length).fill(null);
|
||||||
|
const weights: number[] = new Array(ys.length).fill(0);
|
||||||
|
let s = 0; // raw EMA accumulator
|
||||||
|
let n = 0; // valid points incorporated so far
|
||||||
|
const start = reverse ? ys.length - 1 : 0;
|
||||||
|
const step = reverse ? -1 : 1;
|
||||||
|
for (let i = start; i >= 0 && i < ys.length; i += step) {
|
||||||
const v = ys[i];
|
const v = ys[i];
|
||||||
if (v === null || !Number.isFinite(v)) {
|
if (v === null || !Number.isFinite(v)) continue;
|
||||||
|
s = alpha * (v as number) + (1 - alpha) * s;
|
||||||
|
n += 1;
|
||||||
|
const w = 1 - Math.pow(1 - alpha, n);
|
||||||
|
vals[i] = s / w;
|
||||||
|
weights[i] = w;
|
||||||
|
}
|
||||||
|
return { vals, weights };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-phase (forward-backward) EMA, combined by each pass's confidence weight.
|
||||||
|
// A one-sided EMA pins the first point to its raw value and the last point is a
|
||||||
|
// pure causal (lagging) estimate. Running a forward and backward pass and
|
||||||
|
// blending them by how much data each has seen at that index gives the best of
|
||||||
|
// both: at the start the forward pass has ~1 point (distrusted) so the
|
||||||
|
// backward, future-informed pass dominates; at the latest points the backward
|
||||||
|
// pass has ~1 point so the forward (causal) estimate dominates; the middle is
|
||||||
|
// ~50/50, which also cancels EMA's lag. Nulls stay null (both passes align).
|
||||||
|
function emaWithNulls(ys: (number | null)[], alpha: number): (number | null)[] {
|
||||||
|
const fwd = emaPass(ys, alpha, false);
|
||||||
|
const bwd = emaPass(ys, alpha, true);
|
||||||
|
const out: (number | null)[] = new Array(ys.length);
|
||||||
|
for (let i = 0; i < ys.length; i++) {
|
||||||
|
const f = fwd.vals[i];
|
||||||
|
const b = bwd.vals[i];
|
||||||
|
if (f === null || b === null) {
|
||||||
out[i] = null;
|
out[i] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (prev === null) prev = v as number;
|
const wf = fwd.weights[i];
|
||||||
else prev = alpha * (v as number) + (1 - alpha) * prev;
|
const wb = bwd.weights[i];
|
||||||
out[i] = prev;
|
const wsum = wf + wb;
|
||||||
|
out[i] = wsum > 0 ? (wf * (f as number) + wb * (b as number)) / wsum : ((f as number) + (b as number)) / 2;
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user