Spaces:
Running
Running
| 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]) => `<tr><td>${k.toLocaleString("en-US")}</td><td>${p.toLocaleString("en-US")}</td></tr>` | |
| ).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 = "<p class='muted'>Course notes could not be loaded.</p>"; | |
| } | |
| } | |
| 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 += `<pre><code>${escapeHtml(codeBuf.join("\n"))}</code></pre>`; | |
| codeBuf = []; | |
| inCode = false; | |
| }; | |
| for (const line of lines) { | |
| if (line.startsWith("```")) { | |
| if (inCode) flushCode(); | |
| else { | |
| if (inTable) { | |
| html += "</table>"; | |
| 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 += "<table><thead><tr>" + cells.map((c) => `<th>${inlineMd(c)}</th>`).join("") + "</tr></thead><tbody>"; | |
| inTable = true; | |
| } else { | |
| html += "<tr>" + cells.map((c) => `<td>${inlineMd(c)}</td>`).join("") + "</tr>"; | |
| } | |
| continue; | |
| } | |
| if (inTable) { | |
| html += "</tbody></table>"; | |
| inTable = false; | |
| } | |
| if (!line.trim()) { | |
| html += ""; | |
| continue; | |
| } | |
| if (line.startsWith("# ")) html += `<h1>${inlineMd(line.slice(2))}</h1>`; | |
| else if (line.startsWith("## ")) html += `<h2>${inlineMd(line.slice(3))}</h2>`; | |
| else if (line.startsWith("### ")) html += `<h3>${inlineMd(line.slice(4))}</h3>`; | |
| else if (line.startsWith("- ") || line.startsWith("* ")) html += `<li>${inlineMd(line.slice(2))}</li>`; | |
| else html += `<p>${inlineMd(line)}</p>`; | |
| } | |
| flushCode(); | |
| if (inTable) html += "</tbody></table>"; | |
| return html; | |
| } | |
| function inlineMd(text) { | |
| return escapeHtml(text) | |
| .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>") | |
| .replace(/`(.+?)`/g, "<code>$1</code>") | |
| .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); | |
| } | |
| buildCharts(); | |
| bindControls(); | |
| bindTabs(); | |
| fillDataset(); | |
| loadCourse(); | |
| runTrain(); | |