<div class="controls">
<label for="lam">Regularisation strength λ = <span id="lamVal">0.00</span></label>
<input type="range" id="lam" min="0" max="200" value="0" step="1">
<div class="legend">
<span class="dot ridge"></span> Ridge (L2)
<span class="dot lasso"></span> Lasso (L1)
</div>
</div>
<div id="chart" class="chart"></div>
<div id="info" class="info">λ = 0: both methods equal ordinary least squares.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .9rem; font-weight: 600; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.legend { display: flex; gap: 1.2rem; font-size: .85rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.ridge { background: #457b9d; }
.dot.lasso { background: #e76f51; }
.chart { display: flex; flex-direction: column; gap: 6px; }
.row { display: flex; align-items: center; gap: 8px; }
.label { width: 72px; font-size: .8rem; text-align: right; flex-shrink: 0; color: #555; }
.bars { flex: 1; display: flex; gap: 4px; flex-direction: column; }
.bar-wrap { display: flex; align-items: center; height: 14px; }
.bar { height: 14px; border-radius: 3px; transition: width .15s; min-width: 2px; }
.bar.ridge { background: #457b9d; }
.bar.lasso { background: #e76f51; }
.val { width: 44px; font-size: .75rem; text-align: right; color: #444; flex-shrink: 0; }
.info { margin-top: .7rem; font-size: .85rem; color: #444; min-height: 1.4em; }
.zero { opacity: .3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Synthetic dataset: 30 samples, 6 features, true weights [3, -2, 0.5, -0.3, 0, 0]
// Ridge: closed-form (XtX + λI)^{-1} Xt y
// Lasso: coordinate descent with soft-thresholding
const N = 30, P = 6;
const TRUE_W = [3, -2, 0.5, -0.3, 0, 0];
const FEAT_LABELS = ['Feature 1\n(strong+)', 'Feature 2\n(strong−)', 'Feature 3\n(weak+)', 'Feature 4\n(weak−)', 'Feature 5\n(noise)', 'Feature 6\n(noise)'];
const SHORT_LABELS = ['F1 (strong+)', 'F2 (strong−)', 'F3 (weak+)', 'F4 (weak−)', 'F5 (noise)', 'F6 (noise)'];
// Seeded PRNG (mulberry32) for reproducibility
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const rand = mulberry32(42);
function randn() {
const u = 1 - rand(), v = rand();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Build data matrix X (standardised) and response y
const X = [], y = [];
for (let i = 0; i < N; i++) {
const row = [];
for (let j = 0; j < P; j++) row.push(randn());
X.push(row);
let yi = 0;
for (let j = 0; j < P; j++) yi += TRUE_W[j] * row[j];
yi += 0.5 * randn(); // noise
y.push(yi);
}
// Column-standardise X
const means = Array(P).fill(0), stds = Array(P).fill(0);
for (let j = 0; j < P; j++) {
for (let i = 0; i < N; i++) means[j] += X[i][j];
means[j] /= N;
for (let i = 0; i < N; i++) stds[j] += (X[i][j] - means[j]) ** 2;
stds[j] = Math.sqrt(stds[j] / N) || 1;
for (let i = 0; i < N; i++) X[i][j] = (X[i][j] - means[j]) / stds[j];
}
const yMean = y.reduce((a, b) => a + b, 0) / N;
for (let i = 0; i < N; i++) y[i] -= yMean;
// Matrix helpers
function matMul(A, B) {
const r = A.length, k = A[0].length, c = B[0].length;
const C = Array.from({length: r}, () => Array(c).fill(0));
for (let i = 0; i < r; i++) for (let j = 0; j < c; j++) for (let l = 0; l < k; l++) C[i][j] += A[i][l] * B[l][j];
return C;
}
function transpose(A) { return A[0].map((_, j) => A.map(r => r[j])); }
function matVec(A, v) { return A.map(r => r.reduce((s, x, j) => s + x * v[j], 0)); }
// Ridge: (XtX + λI)^{-1} Xt y — 6x6 inversion via Gauss-Jordan
function invertMatrix(M) {
const n = M.length;
const A = M.map(r => [...r]);
const I = Array.from({length: n}, (_, i) => Array.from({length: n}, (_, j) => i === j ? 1 : 0));
for (let col = 0; col < n; col++) {
let pivot = col;
for (let row = col + 1; row < n; row++) if (Math.abs(A[row][col]) > Math.abs(A[pivot][col])) pivot = row;
[A[col], A[pivot]] = [A[pivot], A[col]]; [I[col], I[pivot]] = [I[pivot], I[col]];
const d = A[col][col];
for (let j = 0; j < n; j++) { A[col][j] /= d; I[col][j] /= d; }
for (let row = 0; row < n; row++) if (row !== col) {
const f = A[row][col];
for (let j = 0; j < n; j++) { A[row][j] -= f * A[col][j]; I[row][j] -= f * I[col][j]; }
}
}
return I;
}
function ridgeCoeffs(lambda) {
const Xt = transpose(X);
const XtX = matMul(Xt, X);
for (let j = 0; j < P; j++) XtX[j][j] += lambda;
const inv = invertMatrix(XtX);
const Xty = matVec(Xt, y);
return matVec(inv, Xty);
}
// Lasso: coordinate descent with soft-threshold
function softThresh(z, gamma) {
if (z > gamma) return z - gamma;
if (z < -gamma) return z + gamma;
return 0;
}
function lassoCoeffs(lambda) {
const w = Array(P).fill(0);
// Precompute column norms squared (all 1 since X is standardised, N cols)
const colNorm2 = Array(P).fill(0);
for (let j = 0; j < P; j++) for (let i = 0; i < N; i++) colNorm2[j] += X[i][j] ** 2;
// Residuals
const r = [...y];
for (let iter = 0; iter < 500; iter++) {
let maxDelta = 0;
for (let j = 0; j < P; j++) {
const wOld = w[j];
// partial residual: r + X_j * w_j
let rho = 0;
for (let i = 0; i < N; i++) rho += X[i][j] * (r[i] + X[i][j] * wOld);
const wNew = softThresh(rho / colNorm2[j], lambda / (2 * colNorm2[j]));
// update residuals
for (let i = 0; i < N; i++) r[i] += X[i][j] * (wOld - wNew);
maxDelta = Math.max(maxDelta, Math.abs(wNew - wOld));
w[j] = wNew;
}
if (maxDelta < 1e-6) break;
}
return w;
}
// UI
const chart = document.getElementById('chart');
SHORT_LABELS.forEach(lbl => {
const row = document.createElement('div'); row.className = 'row';
row.innerHTML = `<div class="label">${lbl.replace('\n','<br>')}</div>
<div class="bars">
<div class="bar-wrap"><div class="bar ridge" id="r${lbl[1]}"></div></div>
<div class="bar-wrap"><div class="bar lasso" id="l${lbl[1]}"></div></div>
</div>
<div class="val" id="vr${lbl[1]}"></div>
<div class="val" id="vl${lbl[1]}"></div>`;
chart.appendChild(row);
});
// Re-index bars by feature index for clarity
const ridgeBars = [], lassoBars = [], ridgeVals = [], lassoVals = [];
for (let j = 0; j < P; j++) {
ridgeBars.push(document.querySelectorAll('.bar.ridge')[j]);
lassoBars.push(document.querySelectorAll('.bar.lasso')[j]);
ridgeVals.push(document.querySelectorAll('.val')[j * 2]);
lassoVals.push(document.querySelectorAll('.val')[j * 2 + 1]);
}
const SCALE = 30; // pixels per unit coefficient
function update() {
const raw = +document.getElementById('lam').value;
const lambda = raw / 10; // map 0..200 -> 0..20
document.getElementById('lamVal').textContent = lambda.toFixed(1);
const rw = ridgeCoeffs(lambda);
const lw = lassoCoeffs(lambda);
let zeroCount = 0;
for (let j = 0; j < P; j++) {
const rv = rw[j], lv = lw[j];
const rPx = Math.max(2, Math.abs(rv) * SCALE);
const lPx = Math.max(2, Math.abs(lv) * SCALE);
ridgeBars[j].style.width = rPx + 'px';
lassoBars[j].style.width = lPx + 'px';
ridgeVals[j].textContent = rv.toFixed(2);
lassoVals[j].textContent = lv.toFixed(2);
if (Math.abs(lv) < 0.005) {
lassoBars[j].classList.add('zero');
lassoVals[j].classList.add('zero');
zeroCount++;
} else {
lassoBars[j].classList.remove('zero');
lassoVals[j].classList.remove('zero');
}
}
const info = document.getElementById('info');
if (lambda === 0) {
info.textContent = 'λ = 0: both methods equal ordinary least squares.';
} else if (zeroCount === 0) {
info.textContent = fmt('λ = %lambda%: Ridge shrinks all coefficients; Lasso has not yet zeroed any.', { lambda: lambda.toFixed(1) });
} else {
info.textContent = fmt('λ = %lambda%: Lasso has zeroed %zeroCount% of 6 feature(s) — automatic feature selection in action.', { lambda: lambda.toFixed(1), zeroCount: zeroCount });
}
console.log(fmt('λ=%lambda% | Ridge: [%map%] | Lasso: [%map2%]', { lambda: lambda.toFixed(1), map: rw.map(v=>v.toFixed(3)).join(', '), map2: lw.map(v=>v.toFixed(3)).join(', ') }));
}
document.getElementById('lam').addEventListener('input', update);
update();
<div class="controls">
<label for="lam">Regularisation strength λ = <span id="lamVal">0.00</span></label>
<input type="range" id="lam" min="0" max="200" value="0" step="1">
<div class="legend">
<span class="dot ridge"></span> Ridge (L2)
<span class="dot lasso"></span> Lasso (L1)
</div>
</div>
<div id="chart" class="chart"></div>
<div id="info" class="info">λ = 0: both methods equal ordinary least squares.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .9rem; font-weight: 600; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.legend { display: flex; gap: 1.2rem; font-size: .85rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.ridge { background: #457b9d; }
.dot.lasso { background: #e76f51; }
.chart { display: flex; flex-direction: column; gap: 6px; }
.row { display: flex; align-items: center; gap: 8px; }
.label { width: 72px; font-size: .8rem; text-align: right; flex-shrink: 0; color: #555; }
.bars { flex: 1; display: flex; gap: 4px; flex-direction: column; }
.bar-wrap { display: flex; align-items: center; height: 14px; }
.bar { height: 14px; border-radius: 3px; transition: width .15s; min-width: 2px; }
.bar.ridge { background: #457b9d; }
.bar.lasso { background: #e76f51; }
.val { width: 44px; font-size: .75rem; text-align: right; color: #444; flex-shrink: 0; }
.info { margin-top: .7rem; font-size: .85rem; color: #444; min-height: 1.4em; }
.zero { opacity: .3; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Synthetic dataset: 30 samples, 6 features, true weights [3, -2, 0.5, -0.3, 0, 0]
// Ridge: closed-form (XtX + λI)^{-1} Xt y
// Lasso: coordinate descent with soft-thresholding
const N = 30, P = 6;
const TRUE_W = [3, -2, 0.5, -0.3, 0, 0];
const FEAT_LABELS = ['Feature 1\n(strong+)', 'Feature 2\n(strong−)', 'Feature 3\n(weak+)', 'Feature 4\n(weak−)', 'Feature 5\n(noise)', 'Feature 6\n(noise)'];
const SHORT_LABELS = ['F1 (strong+)', 'F2 (strong−)', 'F3 (weak+)', 'F4 (weak−)', 'F5 (noise)', 'F6 (noise)'];
// Seeded PRNG (mulberry32) for reproducibility
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const rand = mulberry32(42);
function randn() {
const u = 1 - rand(), v = rand();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Build data matrix X (standardised) and response y
const X = [], y = [];
for (let i = 0; i < N; i++) {
const row = [];
for (let j = 0; j < P; j++) row.push(randn());
X.push(row);
let yi = 0;
for (let j = 0; j < P; j++) yi += TRUE_W[j] * row[j];
yi += 0.5 * randn(); // noise
y.push(yi);
}
// Column-standardise X
const means = Array(P).fill(0), stds = Array(P).fill(0);
for (let j = 0; j < P; j++) {
for (let i = 0; i < N; i++) means[j] += X[i][j];
means[j] /= N;
for (let i = 0; i < N; i++) stds[j] += (X[i][j] - means[j]) ** 2;
stds[j] = Math.sqrt(stds[j] / N) || 1;
for (let i = 0; i < N; i++) X[i][j] = (X[i][j] - means[j]) / stds[j];
}
const yMean = y.reduce((a, b) => a + b, 0) / N;
for (let i = 0; i < N; i++) y[i] -= yMean;
// Matrix helpers
function matMul(A, B) {
const r = A.length, k = A[0].length, c = B[0].length;
const C = Array.from({length: r}, () => Array(c).fill(0));
for (let i = 0; i < r; i++) for (let j = 0; j < c; j++) for (let l = 0; l < k; l++) C[i][j] += A[i][l] * B[l][j];
return C;
}
function transpose(A) { return A[0].map((_, j) => A.map(r => r[j])); }
function matVec(A, v) { return A.map(r => r.reduce((s, x, j) => s + x * v[j], 0)); }
// Ridge: (XtX + λI)^{-1} Xt y — 6x6 inversion via Gauss-Jordan
function invertMatrix(M) {
const n = M.length;
const A = M.map(r => [...r]);
const I = Array.from({length: n}, (_, i) => Array.from({length: n}, (_, j) => i === j ? 1 : 0));
for (let col = 0; col < n; col++) {
let pivot = col;
for (let row = col + 1; row < n; row++) if (Math.abs(A[row][col]) > Math.abs(A[pivot][col])) pivot = row;
[A[col], A[pivot]] = [A[pivot], A[col]]; [I[col], I[pivot]] = [I[pivot], I[col]];
const d = A[col][col];
for (let j = 0; j < n; j++) { A[col][j] /= d; I[col][j] /= d; }
for (let row = 0; row < n; row++) if (row !== col) {
const f = A[row][col];
for (let j = 0; j < n; j++) { A[row][j] -= f * A[col][j]; I[row][j] -= f * I[col][j]; }
}
}
return I;
}
function ridgeCoeffs(lambda) {
const Xt = transpose(X);
const XtX = matMul(Xt, X);
for (let j = 0; j < P; j++) XtX[j][j] += lambda;
const inv = invertMatrix(XtX);
const Xty = matVec(Xt, y);
return matVec(inv, Xty);
}
// Lasso: coordinate descent with soft-threshold
function softThresh(z, gamma) {
if (z > gamma) return z - gamma;
if (z < -gamma) return z + gamma;
return 0;
}
function lassoCoeffs(lambda) {
const w = Array(P).fill(0);
// Precompute column norms squared (all 1 since X is standardised, N cols)
const colNorm2 = Array(P).fill(0);
for (let j = 0; j < P; j++) for (let i = 0; i < N; i++) colNorm2[j] += X[i][j] ** 2;
// Residuals
const r = [...y];
for (let iter = 0; iter < 500; iter++) {
let maxDelta = 0;
for (let j = 0; j < P; j++) {
const wOld = w[j];
// partial residual: r + X_j * w_j
let rho = 0;
for (let i = 0; i < N; i++) rho += X[i][j] * (r[i] + X[i][j] * wOld);
const wNew = softThresh(rho / colNorm2[j], lambda / (2 * colNorm2[j]));
// update residuals
for (let i = 0; i < N; i++) r[i] += X[i][j] * (wOld - wNew);
maxDelta = Math.max(maxDelta, Math.abs(wNew - wOld));
w[j] = wNew;
}
if (maxDelta < 1e-6) break;
}
return w;
}
// UI
const chart = document.getElementById('chart');
SHORT_LABELS.forEach(lbl => {
const row = document.createElement('div'); row.className = 'row';
row.innerHTML = `<div class="label">${lbl.replace('\n','<br>')}</div>
<div class="bars">
<div class="bar-wrap"><div class="bar ridge" id="r${lbl[1]}"></div></div>
<div class="bar-wrap"><div class="bar lasso" id="l${lbl[1]}"></div></div>
</div>
<div class="val" id="vr${lbl[1]}"></div>
<div class="val" id="vl${lbl[1]}"></div>`;
chart.appendChild(row);
});
// Re-index bars by feature index for clarity
const ridgeBars = [], lassoBars = [], ridgeVals = [], lassoVals = [];
for (let j = 0; j < P; j++) {
ridgeBars.push(document.querySelectorAll('.bar.ridge')[j]);
lassoBars.push(document.querySelectorAll('.bar.lasso')[j]);
ridgeVals.push(document.querySelectorAll('.val')[j * 2]);
lassoVals.push(document.querySelectorAll('.val')[j * 2 + 1]);
}
const SCALE = 30; // pixels per unit coefficient
function update() {
const raw = +document.getElementById('lam').value;
const lambda = raw / 10; // map 0..200 -> 0..20
document.getElementById('lamVal').textContent = lambda.toFixed(1);
const rw = ridgeCoeffs(lambda);
const lw = lassoCoeffs(lambda);
let zeroCount = 0;
for (let j = 0; j < P; j++) {
const rv = rw[j], lv = lw[j];
const rPx = Math.max(2, Math.abs(rv) * SCALE);
const lPx = Math.max(2, Math.abs(lv) * SCALE);
ridgeBars[j].style.width = rPx + 'px';
lassoBars[j].style.width = lPx + 'px';
ridgeVals[j].textContent = rv.toFixed(2);
lassoVals[j].textContent = lv.toFixed(2);
if (Math.abs(lv) < 0.005) {
lassoBars[j].classList.add('zero');
lassoVals[j].classList.add('zero');
zeroCount++;
} else {
lassoBars[j].classList.remove('zero');
lassoVals[j].classList.remove('zero');
}
}
const info = document.getElementById('info');
if (lambda === 0) {
info.textContent = 'λ = 0: both methods equal ordinary least squares.';
} else if (zeroCount === 0) {
info.textContent = fmt('λ = %lambda%: Ridge shrinks all coefficients; Lasso has not yet zeroed any.', { lambda: lambda.toFixed(1) });
} else {
info.textContent = fmt('λ = %lambda%: Lasso has zeroed %zeroCount% of 6 feature(s) — automatic feature selection in action.', { lambda: lambda.toFixed(1), zeroCount: zeroCount });
}
console.log(fmt('λ=%lambda% | Ridge: [%map%] | Lasso: [%map2%]', { lambda: lambda.toFixed(1), map: rw.map(v=>v.toFixed(3)).join(', '), map2: lw.map(v=>v.toFixed(3)).join(', ') }));
}
document.getElementById('lam').addEventListener('input', update);
update();
Notice the difference in shape. Ridge (blue) shrinks all coefficients smoothly and proportionally — they approach zero but never touch it. Lasso (orange) behaves differently: as λ grows, weak coefficients snap to exactly zero, one by one. At high λ, only the truly useful features survive. That automatic feature selection is Lasso's superpower.
Comments
Loading comments...