ImTamsi's picture
Add interactive static demo: chart, dataset, subject PDF
7771acc verified
Raw
History Blame Contribute Delete
10.5 kB
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
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();