const DATA = [ [240000, 3650], [139800, 3800], [150500, 4400], [185530, 4450], [176000, 5250], [114800, 5350], [166800, 5800], [89000, 5990], [144500, 5999], [84000, 6200], [82029, 6390], [63060, 6390], [74000, 6600], [97500, 6800], [67000, 6800], [76025, 6900], [48235, 6900], [93000, 6990], [60949, 7490], [65674, 7555], [54000, 7990], [68500, 7990], [22899, 7990], [61789, 8290], ]; const km = DATA.map((d) => d[0]); const price = DATA.map((d) => d[1]); const kmMin = Math.min(...km); const kmMax = Math.max(...km); const priceMin = Math.min(...price); const priceMax = Math.max(...price); let theta0 = 0; let theta1 = 0; let regChart; let costChart; function estimate(x, t0, t1) { return t0 + t1 * x; } function mse(t0, t1) { let total = 0; for (let i = 0; i < km.length; i++) { const err = estimate(km[i], t0, t1) - price[i]; total += err * err; } return total / km.length; } function train(learningRate, iterations) { const scaled = km.map((x) => (x - kmMin) / (kmMax - kmMin)); let t0n = 0; let t1n = 0; const history = []; const sampleEvery = Math.max(1, Math.floor(iterations / 100)); const m = scaled.length; for (let step = 0; step < iterations; step++) { let sumErr = 0; let sumErrX = 0; for (let i = 0; i < m; i++) { const err = estimate(scaled[i], t0n, t1n) - price[i]; sumErr += err; sumErrX += err * scaled[i]; } t0n -= learningRate * (sumErr / m); t1n -= learningRate * (sumErrX / m); if (step % sampleEvery === 0 || step === iterations - 1) { const t1 = t1n / (kmMax - kmMin); const t0 = t0n - t1 * kmMin; history.push(mse(t0, t1)); } } const t1 = t1n / (kmMax - kmMin); const t0 = t0n - t1 * kmMin; return { t0, t1, history }; } function formatNum(n, digits = 2) { return Number(n).toLocaleString("en-US", { maximumFractionDigits: digits, minimumFractionDigits: digits, }); } function updateMetrics(mileage) { const pred = estimate(mileage, theta0, theta1); document.getElementById("metrics").textContent = `θ₀ (bias) = ${theta0.toFixed(4)}\n` + `θ₁ (slope) = ${theta1.toFixed(8)}\n` + `Final MSE = ${mse(theta0, theta1).toFixed(2)}\n` + `Price @ ${Math.round(mileage).toLocaleString("en-US")} km → ${pred.toFixed(2)} €`; } function buildCharts() { const scatter = km.map((x, i) => ({ x, y: price[i] })); const lineX = Array.from({ length: 80 }, (_, i) => kmMin + ((kmMax - kmMin) * i) / 79); regChart = new Chart(document.getElementById("reg-chart"), { data: { datasets: [ { type: "scatter", label: "Dataset cars", data: scatter, backgroundColor: "#1f6f8b", pointRadius: 5, pointHoverRadius: 7, }, { type: "line", label: "Regression line", data: lineX.map((x) => ({ x, y: 0 })), borderColor: "#c23b22", borderWidth: 2.5, pointRadius: 0, tension: 0, }, { type: "scatter", label: "Prediction", data: [], backgroundColor: "#d4a017", borderColor: "#14212b", borderWidth: 1.5, pointRadius: 8, pointHoverRadius: 10, }, ], }, options: { responsive: true, maintainAspectRatio: true, aspectRatio: 1.55, plugins: { title: { display: true, text: "Price vs mileage", font: { size: 15, family: "DM Sans" }, color: "#14212b", }, legend: { labels: { font: { family: "DM Sans" } } }, }, scales: { x: { type: "linear", title: { display: true, text: "Mileage (km)", font: { family: "DM Sans" } }, grid: { color: "rgba(20,33,43,0.06)" }, }, y: { title: { display: true, text: "Price (€)", font: { family: "DM Sans" } }, grid: { color: "rgba(20,33,43,0.06)" }, }, }, }, }); costChart = new Chart(document.getElementById("cost-chart"), { type: "line", data: { labels: [], datasets: [ { label: "MSE cost", data: [], borderColor: "#1f6f8b", borderWidth: 2, pointRadius: 0, tension: 0.2, }, ], }, options: { responsive: true, maintainAspectRatio: true, aspectRatio: 2.6, plugins: { title: { display: true, text: "Cost decrease during gradient descent", font: { size: 14, family: "DM Sans" }, }, legend: { display: false }, }, scales: { x: { title: { display: true, text: "Training progress (%)" }, grid: { color: "rgba(20,33,43,0.06)" }, }, y: { title: { display: true, text: "MSE" }, grid: { color: "rgba(20,33,43,0.06)" }, }, }, }, }); } function refreshCharts(history, mileage) { const lineX = Array.from({ length: 80 }, (_, i) => kmMin + ((kmMax - kmMin) * i) / 79); regChart.data.datasets[1].data = lineX.map((x) => ({ x, y: estimate(x, theta0, theta1) })); regChart.data.datasets[2].data = [{ x: mileage, y: estimate(mileage, theta0, theta1) }]; regChart.options.plugins.title.text = `price ≈ ${theta0.toFixed(2)} + (${theta1.toFixed(6)} × km)`; regChart.update(); if (history) { costChart.data.labels = history.map((_, i) => ((i / Math.max(1, history.length - 1)) * 100).toFixed(0) ); costChart.data.datasets[0].data = history; costChart.update(); } } function runTrain() { const lr = Number(document.getElementById("lr").value); const iterations = Number(document.getElementById("iters").value); const mileage = Number(document.getElementById("mileage").value); const result = train(lr, iterations); theta0 = result.t0; theta1 = result.t1; updateMetrics(mileage); refreshCharts(result.history, mileage); } function bindControls() { const lr = document.getElementById("lr"); const iters = document.getElementById("iters"); const mileage = document.getElementById("mileage"); const sync = () => { document.getElementById("lr-val").textContent = Number(lr.value).toFixed(3); document.getElementById("iters-val").textContent = iters.value; document.getElementById("mileage-val").textContent = Number(mileage.value).toLocaleString("en-US"); }; lr.addEventListener("input", sync); iters.addEventListener("input", sync); mileage.addEventListener("input", () => { sync(); updateMetrics(Number(mileage.value)); refreshCharts(null, Number(mileage.value)); }); document.getElementById("train-btn").addEventListener("click", runTrain); sync(); } function bindTabs() { document.querySelectorAll(".tab").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((b) => { b.classList.remove("active"); b.setAttribute("aria-selected", "false"); }); document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); btn.setAttribute("aria-selected", "true"); document.getElementById(`panel-${btn.dataset.tab}`).classList.add("active"); }); }); } function fillDataset() { document.getElementById("dataset-summary").textContent = `${DATA.length} cars · mileage ${kmMin.toLocaleString("en-US")}–${kmMax.toLocaleString("en-US")} km · ` + `price ${priceMin.toLocaleString("en-US")}–${priceMax.toLocaleString("en-US")} €`; const body = document.getElementById("dataset-body"); body.innerHTML = DATA.map( ([k, p]) => `${k.toLocaleString("en-US")}${p.toLocaleString("en-US")}` ).join(""); } async function loadCourse() { const el = document.getElementById("course-content"); try { const res = await fetch("linear-regression-course.md"); if (!res.ok) throw new Error("missing"); const md = await res.text(); el.innerHTML = renderMarkdown(md); } catch { el.innerHTML = "

Course notes could not be loaded.

"; } } function escapeHtml(s) { return s .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function renderMarkdown(md) { const lines = md.replace(/\r\n/g, "\n").split("\n"); let html = ""; let inCode = false; let inTable = false; let codeBuf = []; const flushCode = () => { if (!inCode) return; html += `
${escapeHtml(codeBuf.join("\n"))}
`; codeBuf = []; inCode = false; }; for (const line of lines) { if (line.startsWith("```")) { if (inCode) flushCode(); else { if (inTable) { html += ""; inTable = false; } inCode = true; } continue; } if (inCode) { codeBuf.push(line); continue; } if (line.trim().startsWith("|") && line.includes("|")) { if (line.replace(/[|\-\s:]/g, "") === "") continue; const cells = line .trim() .replace(/^\|/, "") .replace(/\|$/, "") .split("|") .map((c) => c.trim()); if (!inTable) { html += "" + cells.map((c) => ``).join("") + ""; inTable = true; } else { html += "" + cells.map((c) => ``).join("") + ""; } continue; } if (inTable) { html += "
${inlineMd(c)}
${inlineMd(c)}
"; inTable = false; } if (!line.trim()) { html += ""; continue; } if (line.startsWith("# ")) html += `

${inlineMd(line.slice(2))}

`; else if (line.startsWith("## ")) html += `

${inlineMd(line.slice(3))}

`; else if (line.startsWith("### ")) html += `

${inlineMd(line.slice(4))}

`; else if (line.startsWith("- ") || line.startsWith("* ")) html += `
  • ${inlineMd(line.slice(2))}
  • `; else html += `

    ${inlineMd(line)}

    `; } flushCode(); if (inTable) html += ""; return html; } function inlineMd(text) { return escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/`(.+?)`/g, "$1") .replace(/\[(.+?)\]\((.+?)\)/g, '$1'); } buildCharts(); bindControls(); bindTabs(); fillDataset(); loadCourse(); runTrain();