ML_course / static /linear_regression.html
livieris's picture
Upload 15 files
be64da1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linear Regression β€” Statistical Machine Learning</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
:root {
--bg:#EEF3F8; --bg-card:#FFFFFF; --bg-muted:#E2EAF3;
--teal:#1D9E75; --teal-light:#E1F5EE; --teal-dark:#0F6E56;
--blue:#378ADD; --blue-light:#E6F1FB; --blue-dark:#185FA5;
--orange:#E07A30; --orange-light:#FEF0E3;
--red:#C0392B; --red-light:#FDECEA;
--text:#1C2B3A; --text2:#4A5F74; --muted:#8298AE;
--border:rgba(28,43,58,0.10); --border-s:rgba(28,43,58,0.18);
--sh-sm:0 1px 3px rgba(28,43,58,0.07),0 1px 2px rgba(28,43,58,0.05);
--sh-md:0 4px 12px rgba(28,43,58,0.09),0 2px 4px rgba(28,43,58,0.06);
--sh-lg:0 12px 32px rgba(28,43,58,0.11),0 4px 8px rgba(28,43,58,0.07);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{font-family:'Outfit',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
/* NAV */
nav{position:sticky;top:0;z-index:100;background:rgba(238,243,248,0.92);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2.5rem;height:60px;display:flex;align-items:center;justify-content:space-between}
.nav-brand{display:flex;align-items:center;gap:10px;font-family:'DM Mono',monospace;font-size:13px;font-weight:500;color:var(--text2);letter-spacing:.04em;text-decoration:none}
.nav-brand span{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--teal)}
.breadcrumb{font-family:'DM Mono',monospace;font-size:12px;color:var(--muted);display:flex;align-items:center;gap:8px}
.breadcrumb a{color:var(--muted);text-decoration:none}
.breadcrumb a:hover{color:var(--teal-dark)}
/* PAGE HEADER */
.ph{padding:3.5rem 2.5rem 2rem;max-width:1200px;margin:0 auto}
.ph-eye{font-family:'DM Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--teal-dark);display:flex;align-items:center;gap:8px;margin-bottom:1rem}
.ph-eye::before{content:'';display:block;width:28px;height:1.5px;background:var(--teal)}
.ph h1{font-family:'DM Serif Display',serif;font-size:clamp(2.2rem,4vw,3rem);line-height:1.1;letter-spacing:-.02em;margin-bottom:.6rem}
.tags{display:flex;gap:8px;flex-wrap:wrap;margin-top:1rem}
.tag{font-family:'DM Mono',monospace;font-size:11px;font-weight:500;padding:4px 12px;border-radius:20px;letter-spacing:.06em;text-transform:uppercase}
.tag-sup{background:#E1F5EE;color:#0F6E56;border:1px solid #A8DFC8}
.tag-reg{background:#EEF3F8;color:#4A5F74;border:1px solid var(--border-s)}
/* MAIN */
.main{max-width:1200px;margin:0 auto;padding:0 2.5rem 6rem}
/* CARDS */
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:2rem;box-shadow:var(--sh-sm);margin-bottom:1.5rem}
.card-title{font-family:'DM Mono',monospace;font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:1.2rem}
/* THEORY */
.theory-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-bottom:1.5rem}
.tc{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:1.6rem;box-shadow:var(--sh-sm)}
.tc h3{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--teal-dark);margin-bottom:.8rem}
.tc p{font-size:14px;line-height:1.75;color:var(--text2);font-weight:300}
.formula-card{background:var(--bg-card);border:1px solid var(--border);border-left:4px solid var(--teal);border-radius:14px;padding:1.6rem;box-shadow:var(--sh-sm);margin-bottom:1.5rem}
.formula-card h3{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--teal-dark);margin-bottom:1rem}
.fl{font-family:'DM Mono',monospace;font-size:15px;color:var(--text);margin-bottom:.5rem}
.fl.lg{font-size:19px;margin-bottom:.8rem}
.fl.sub{font-size:12px;color:var(--muted);margin-top:.3rem}
/* SECTION LABEL */
.sl{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;display:flex;align-items:center;gap:8px}
.sl::after{content:'';flex:1;height:1px;background:var(--border)}
.sep{height:1px;background:var(--border);margin:2rem 0}
/* DATASET BUTTONS */
.ds-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:1.2rem}
.ds-btn{border:1.5px solid var(--border-s);border-radius:10px;padding:.9rem .6rem;background:var(--bg-card);cursor:pointer;transition:all .2s;display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center}
.ds-btn:hover{border-color:var(--teal);background:var(--teal-light)}
.ds-btn.active{border-color:var(--teal);background:var(--teal-light);box-shadow:0 0 0 3px rgba(29,158,117,.12)}
.ds-name{font-family:'DM Mono',monospace;font-size:12px;font-weight:500;color:var(--text)}
.ds-dim{font-size:11px;color:var(--muted)}
.ds-type{font-size:10px;font-family:'DM Mono',monospace;color:var(--teal-dark);margin-top:2px}
/* PARAMS PANEL */
.params-panel{background:var(--bg-muted);border:1px solid var(--border);border-radius:12px;padding:1.4rem 1.6rem;margin-bottom:1.2rem;animation:slideDown .25s ease}
@keyframes slideDown{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
.pg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem}
.pg{display:flex;flex-direction:column;gap:4px}
.pg label{font-family:'DM Mono',monospace;font-size:10.5px;letter-spacing:.08em;color:var(--text2);display:flex;justify-content:space-between}
.pg label span{color:var(--teal-dark);font-weight:500}
.pg input[type=range]{width:100%;accent-color:var(--teal);cursor:pointer}
.pg select,.pg input[type=number]{padding:6px 10px;border:1px solid var(--border-s);border-radius:7px;background:var(--bg-card);font-family:'Outfit',sans-serif;font-size:13px;color:var(--text);outline:none;width:100%}
.pg select:focus,.pg input:focus{border-color:var(--teal)}
/* MODEL TABS */
.train-row{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:1.5rem;margin-top:1rem}
.model-tabs{display:flex;gap:8px}
.mt{padding:8px 18px;border:1.5px solid var(--border-s);border-radius:8px;background:var(--bg-card);font-family:'DM Mono',monospace;font-size:12px;cursor:pointer;transition:all .2s;color:var(--text2)}
.mt.active{border-color:var(--blue);background:var(--blue-light);color:var(--blue-dark)}
.alpha-wrap{display:flex;align-items:center;gap:8px;font-family:'DM Mono',monospace;font-size:12px;color:var(--text2)}
.btn-train{padding:11px 32px;background:var(--teal);color:#fff;border:none;border-radius:9px;font-family:'Outfit',sans-serif;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:8px;margin-left:auto}
.btn-train:hover{background:var(--teal-dark);transform:translateY(-1px)}
.btn-train:disabled{background:var(--muted);cursor:not-allowed;transform:none}
/* METRICS */
.mbar{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:1.5rem}
.mc{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.9rem 1rem;box-shadow:var(--sh-sm)}
.mc .lbl{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.08em;color:var(--muted);text-transform:uppercase;margin-bottom:4px}
.mc .val{font-family:'DM Mono',monospace;font-size:17px;font-weight:500;color:var(--text)}
.mc .sub{font-size:11px;color:var(--muted);margin-top:2px}
.mc.good .val{color:var(--teal-dark)}
.mc.warn .val{color:var(--orange)}
.mc.bad .val{color:var(--red)}
/* PLOTS */
.plots-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-bottom:1.5rem}
.plots-1{display:grid;grid-template-columns:1fr;gap:1.5rem;margin-bottom:1.5rem}
.pc{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:1.4rem;box-shadow:var(--sh-sm)}
.pc-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}
.pc-title{font-family:'DM Mono',monospace;font-size:10.5px;letter-spacing:.10em;text-transform:uppercase;color:var(--text2)}
.badge{font-size:10px;font-family:'DM Mono',monospace;padding:3px 9px;border-radius:20px}
.b-ok{background:var(--teal-light);color:var(--teal-dark)}
.b-warn{background:var(--orange-light);color:var(--orange)}
.b-bad{background:var(--red-light);color:var(--red)}
.cw{position:relative;height:240px}
.cw.tall{height:300px}
.pnote{font-size:11.5px;color:var(--muted);margin-top:.7rem;line-height:1.6;font-style:italic}
/* axis selectors */
.ax-sel{display:flex;gap:10px;margin-bottom:.8rem;align-items:center;flex-wrap:wrap}
.ax-sel label{font-family:'DM Mono',monospace;font-size:10.5px;color:var(--muted)}
.ax-sel select{padding:4px 8px;border:1px solid var(--border-s);border-radius:6px;font-size:12px;font-family:'DM Mono',monospace;background:var(--bg-card);color:var(--text)}
/* COEF TABLE */
.coef-table{width:100%;border-collapse:collapse;font-size:13px}
.coef-table th{font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:6px 12px;border-bottom:1px solid var(--border)}
.coef-table td{padding:8px 12px;border-bottom:1px solid var(--border);color:var(--text2);font-family:'DM Mono',monospace;font-size:12.5px}
.coef-table tr:last-child td{border-bottom:none}
.cbar-w{width:100px;height:6px;background:var(--bg-muted);border-radius:3px;display:inline-block;vertical-align:middle}
.cbar{height:100%;border-radius:3px;background:var(--teal)}
.cbar.neg{background:var(--orange)}
.sig{font-size:11px;color:var(--teal-dark);margin-left:4px}
.nsig{font-size:11px;color:var(--muted);margin-left:4px}
/* DIAG SUMMARY */
.diag-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
.dc{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:1.2rem;box-shadow:var(--sh-sm)}
.dc h4{font-family:'DM Mono',monospace;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px}
.dc p{font-size:12.5px;color:var(--muted);line-height:1.65;font-weight:300}
/* GD ANIMATION */
.gd-controls{display:flex;align-items:center;gap:12px;margin-bottom:1rem;flex-wrap:wrap}
.gd-btn{padding:7px 16px;border:1.5px solid var(--border-s);border-radius:7px;background:var(--bg-card);font-family:'DM Mono',monospace;font-size:12px;cursor:pointer;transition:all .2s;color:var(--text2)}
.gd-btn.play{background:var(--teal);color:#fff;border-color:var(--teal)}
.gd-btn:hover:not(.play){border-color:var(--teal);color:var(--teal-dark)}
.gd-iter{font-family:'DM Mono',monospace;font-size:13px;color:var(--text2)}
.gd-speed{display:flex;align-items:center;gap:6px;font-family:'DM Mono',monospace;font-size:11px;color:var(--muted)}
.gd-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}
/* SPINNER */
.spinner{width:18px;height:18px;border:2.5px solid rgba(255,255,255,.4);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* PERM IMPORTANCE */
.perm-bars{display:flex;flex-direction:column;gap:8px;padding-top:.5rem}
.perm-row{display:flex;align-items:center;gap:10px}
.perm-feat{font-family:'DM Mono',monospace;font-size:11px;color:var(--text2);min-width:80px;text-align:right}
.perm-bar-w{flex:1;height:8px;background:var(--bg-muted);border-radius:4px;overflow:hidden}
.perm-bar-f{height:100%;border-radius:4px;background:var(--teal);transition:width .6s ease}
.perm-val{font-family:'DM Mono',monospace;font-size:11px;color:var(--muted);min-width:50px}
/* GROUPED METRICS */
.metrics-2col{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
.metrics-group{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:1rem;box-shadow:var(--sh-sm)}
.mg-label{font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.14em;text-transform:uppercase;margin-bottom:.65rem;display:flex;align-items:center;gap:8px}
.mg-label::after{content:'';flex:1;height:1px;background:var(--border)}
.mrow-4{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.mrow-3{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:1.5rem}
/* SIDEBAR */
.sb{position:fixed;left:-172px;top:50%;transform:translateY(-50%);z-index:500;display:flex;align-items:stretch;transition:left .3s cubic-bezier(.4,0,.2,1)}
.sb:hover{left:0}
.sb-panel{width:172px;background:var(--bg-card);border:1px solid var(--border-s);border-right:none;border-radius:14px 0 0 14px;box-shadow:var(--sh-lg);padding:.8rem 0;display:flex;flex-direction:column;gap:1px;overflow:hidden}
.sb-head{font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);padding:.3rem 1rem .35rem}
.sb-sep{height:1px;background:var(--border);margin:.35rem .7rem}
.sb-link{display:flex;align-items:center;gap:10px;padding:.55rem 1rem;text-decoration:none;color:var(--text2);font-family:'Outfit',sans-serif;font-size:12.5px;transition:background .15s,color .15s;white-space:nowrap}
.sb-link:hover{background:var(--bg-muted);color:var(--text)}
.sb-link.active-teal{background:var(--teal-light);color:var(--teal-dark);font-weight:500}
.sb-link.active-blue{background:var(--blue-light);color:var(--blue-dark);font-weight:500}
.sb-icon{width:18px;height:18px;flex-shrink:0;opacity:.65}
.sb-link:hover .sb-icon,.sb-link.active-teal .sb-icon,.sb-link.active-blue .sb-icon{opacity:1}
.sb-tab{width:26px;background:var(--bg-card);border:1px solid var(--border-s);border-left:none;border-radius:0 10px 10px 0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:14px 0;cursor:default;box-shadow:3px 0 8px rgba(28,43,58,.07)}
/* RESPONSIVE */
@media(max-width:900px){.theory-grid,.plots-2,.gd-grid,.diag-grid{grid-template-columns:1fr}.mbar{grid-template-columns:repeat(3,1fr)}.ds-grid{grid-template-columns:repeat(2,1fr)}.metrics-2col{grid-template-columns:1fr}.mrow-4{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){nav{padding:0 1rem}.ph,.main{padding-left:1rem;padding-right:1rem}.mbar{grid-template-columns:repeat(2,1fr)}.mrow-4,.mrow-3{grid-template-columns:repeat(2,1fr)}}
</style>
</head>
<body>
<!-- ══ AUTO-HIDE SIDEBAR ══════════════════════════════════════════ -->
<div class="sb">
<div class="sb-panel">
<div class="sb-head">Navigation</div>
<a class="sb-link" href="/">
<svg class="sb-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9L10 2l8 7v9h-5v-5H7v5H2z"/></svg>
<span>Home</span>
</a>
<div class="sb-sep"></div>
<div class="sb-head">Models</div>
<a class="sb-link active-teal" href="/linear-regression">
<svg class="sb-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
<circle cx="3" cy="15" r="1.4" fill="currentColor" stroke="none"/>
<circle cx="7" cy="11" r="1.4" fill="currentColor" stroke="none"/>
<circle cx="12" cy="8" r="1.4" fill="currentColor" stroke="none"/>
<circle cx="16" cy="4" r="1.4" fill="currentColor" stroke="none"/>
<line x1="1" y1="17" x2="18" y2="2"/>
</svg>
<span>Linear Regression</span>
</a>
<a class="sb-link" href="/logistic-regression">
<svg class="sb-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
<path d="M2 16C3 16 5 15 7 13C9 11 11 9 13 7C15 5 17 4 18 4"/>
<line x1="1" y1="10" x2="19" y2="10" stroke-dasharray="2.5 2.5" opacity="0.45"/>
</svg>
<span>Logistic Regression</span>
</a>
</div>
<div class="sb-tab">
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" stroke="var(--muted)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,2 7,7 2,12"/></svg>
</div>
</div>
<nav>
<a class="nav-brand" href="/"><span></span> SML Β· UniPiraeus</a>
<div class="breadcrumb"><a href="/">Home</a> / Linear Regression</div>
</nav>
<div class="ph">
<div class="ph-eye">Supervised Learning</div>
<h1>Linear Regression</h1>
<div class="tags">
<span class="tag tag-sup">Supervised</span>
<span class="tag tag-reg">Regression</span>
</div>
</div>
<div class="main">
<!-- ══ THEORY ══════════════════════════════════════════════════════ -->
<div class="sl">Theory</div>
<div class="theory-grid">
<div class="tc"><h3>What it does</h3><p>Estimates a continuous output by fitting the best-fit hyperplane through data, minimizing the sum of squared residuals (MSE). Each feature receives a coefficient Ξ² that quantifies its exact linear contribution to the prediction.</p></div>
<div class="tc"><h3>When to use</h3><p>Target is continuous (yield, price, log-survival). Relationship between features and target is roughly linear. Check residuals are normally distributed (Q-Q plot). Add regularization (Ridge/Lasso) when many features are correlated.</p></div>
<div class="tc"><h3>Key strength</h3><p>Fully interpretable β€” Ξ²<sub>i</sub> means "y changes by Ξ²<sub>i</sub> for every unit increase in x<sub>i</sub>". Fastest model to train. Best first baseline. <strong>Limitation:</strong> assumes linearity; fails on non-linear data and is sensitive to outliers.</p></div>
<div class="tc"><h3>Regularization</h3><p><strong>Ridge (L2)</strong> shrinks all coefficients toward zero β€” handles multicollinearity. <strong>Lasso (L1)</strong> zeroes out irrelevant features entirely β€” automatic feature selection. Both add a penalty λ·‖β‖ to the loss.</p></div>
</div>
<div class="formula-card">
<h3>Hypothesis &amp; Loss</h3>
<div class="fl lg">Ε· = Ξ²β‚€ + β₁x₁ + Ξ²β‚‚xβ‚‚ + … + Ξ²β‚™xβ‚™</div>
<div class="fl">Loss (MSE) = (1/n) Ξ£ (yα΅’ βˆ’ Ε·α΅’)Β²&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Ξ² = (Xα΅€X)⁻¹ Xα΅€y &nbsp;[Normal Eq.]</div>
<div class="fl sub">Ridge: Loss + λ·Σβⱼ²&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;Lasso: Loss + λ·Σ|βⱼ|</div>
</div>
<!-- ══ DATASET SELECTION ════════════════════════════════════════════ -->
<div class="sep"></div>
<div class="sl">Dataset Selection</div>
<div class="card">
<div class="card-title">Synthetic Datasets β€” 2D</div>
<div class="ds-grid">
<button class="ds-btn active" data-ds="linear" onclick="selectDS(this,'linear')"><div class="ds-name">Linear</div><div class="ds-dim">1 feature</div><div class="ds-type">synthetic</div></button>
<button class="ds-btn" data-ds="polynomial" onclick="selectDS(this,'polynomial')"><div class="ds-name">Polynomial</div><div class="ds-dim">1 feature</div><div class="ds-type">synthetic</div></button>
<button class="ds-btn" data-ds="sinusoidal" onclick="selectDS(this,'sinusoidal')"><div class="ds-name">Sinusoidal</div><div class="ds-dim">1 feature</div><div class="ds-type">synthetic</div></button>
<button class="ds-btn" data-ds="heteroscedastic" onclick="selectDS(this,'heteroscedastic')"><div class="ds-name">Heteroscedastic</div><div class="ds-dim">1 feature</div><div class="ds-type">synthetic</div></button>
</div>
<div class="card-title" style="margin-top:1.2rem">Real Datasets β€” Multi-feature</div>
<div class="ds-grid">
<button class="ds-btn" data-ds="diabetes" onclick="selectDS(this,'diabetes')"><div class="ds-name">Diabetes</div><div class="ds-dim">10D Β· 442 rows</div><div class="ds-type">real</div></button>
<button class="ds-btn" data-ds="wine_quality" onclick="selectDS(this,'wine_quality')"><div class="ds-name">Wine</div><div class="ds-dim">13D Β· 178 rows</div><div class="ds-type">real</div></button>
<button class="ds-btn" data-ds="linnerud" onclick="selectDS(this,'linnerud')"><div class="ds-name">Linnerud</div><div class="ds-dim">3D Β· 20 rows</div><div class="ds-type">real</div></button>
</div>
<div id="paramsPanel"></div>
<!-- Split -->
<div class="params-panel" style="margin-bottom:.8rem">
<div class="pg-grid">
<div class="pg">
<label>Train / Test split <span id="splitLbl">80 / 20 %</span></label>
<input type="range" id="splitSlider" min="50" max="90" step="5" value="80" oninput="updateSplit(this.value)">
</div>
</div>
</div>
<!-- Model + Train -->
<div class="train-row">
<div>
<div style="font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.10em;text-transform:uppercase;color:var(--muted);margin-bottom:6px">Model</div>
<div class="model-tabs">
<button class="mt active" onclick="selectModel(this,'linear')">OLS</button>
<button class="mt" onclick="selectModel(this,'ridge')">Ridge</button>
<button class="mt" onclick="selectModel(this,'lasso')">Lasso</button>
</div>
</div>
<div id="alphaRow" style="display:none">
<div style="font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.10em;text-transform:uppercase;color:var(--muted);margin-bottom:6px">Ξ» (alpha)</div>
<div class="alpha-wrap">
<input type="range" id="alphaSlider" min="-3" max="3" step="0.1" value="0" style="width:120px;accent-color:var(--blue)" oninput="updateAlpha(this.value)">
<span id="alphaLbl" style="font-family:'DM Mono',monospace;font-size:13px;color:var(--blue-dark)">1.00</span>
</div>
</div>
<button class="btn-train" id="trainBtn" onclick="runTrain()">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><polygon points="5,3 19,12 5,21"/></svg>
Train Model
</button>
</div>
</div>
<!-- ══ RESULTS ══════════════════════════════════════════════════════ -->
<div id="results" style="display:none">
<!-- METRICS -->
<div class="sl">Performance Metrics</div>
<div id="metricsContainer"></div>
<!-- SCATTER + AVP -->
<div class="sl">Model Fit</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Scatter + Regression Line</div></div>
<div id="axSel" class="ax-sel" style="display:none"></div>
<div class="cw"><canvas id="cScatter"></canvas></div>
<div class="pnote" id="scatterNote"></div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Actual vs Predicted</div></div>
<div class="cw"><canvas id="cAvp"></canvas></div>
<div class="pnote">Perfect predictions lie on the diagonal. Systematic deviation β†’ model bias.</div>
</div>
</div>
<!-- COEFFICIENTS + CI -->
<div class="sl">Coefficients &amp; Confidence Intervals</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Coefficient Values</div></div>
<div style="overflow-x:auto">
<table class="coef-table">
<thead><tr><th>Feature</th><th>Coef</th><th>95% CI</th><th>p-value</th><th>Magnitude</th></tr></thead>
<tbody id="coefBody"></tbody>
</table>
</div>
<div class="pnote">* p&lt;0.05 significant. CI only available for OLS (not Ridge/Lasso).</div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Coefficient Confidence Intervals (OLS)</div></div>
<div class="cw"><canvas id="cCI"></canvas></div>
<div class="pnote">Error bars = 95% CI. Bars crossing zero indicate non-significant features.</div>
</div>
</div>
<!-- LEARNING CURVE + REG PATH -->
<div class="sl">Model Understanding</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Learning Curve</div></div>
<div class="cw"><canvas id="cLC"></canvas></div>
<div class="pnote">Train vs validation RΒ² as training set grows. Large gap β†’ high variance (overfit). Both low β†’ high bias (underfit).</div>
</div>
<div class="pc">
<div class="pc-head">
<div class="pc-title">Regularization Path</div>
<div style="display:flex;gap:6px">
<button class="gd-btn" id="regPathRidgeBtn" onclick="showRegPath('ridge')" style="padding:4px 10px;font-size:10px">Ridge</button>
<button class="gd-btn" id="regPathLassoBtn" onclick="showRegPath('lasso')" style="padding:4px 10px;font-size:10px">Lasso</button>
</div>
</div>
<div class="cw"><canvas id="cRegPath"></canvas></div>
<div class="pnote">Coefficients vs log₁₀(Ξ»). Lasso zeroes features (feature selection); Ridge shrinks them gradually.</div>
</div>
</div>
<!-- PERMUTATION IMPORTANCE -->
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Permutation Feature Importance</div></div>
<div id="permBars" class="perm-bars"></div>
<div class="pnote">Drop in RΒ² when a feature is shuffled (avg over 20 repeats). Larger drop β†’ more important feature.</div>
</div>
<div class="pc" id="partialRegCard" style="display:none">
<div class="pc-head">
<div class="pc-title">Partial Regression Plots</div>
<select id="prSel" onchange="showPartialReg(this.value)" style="padding:4px 8px;border:1px solid var(--border-s);border-radius:6px;font-size:12px;font-family:'DM Mono',monospace;background:var(--bg-card)"></select>
</div>
<div class="cw"><canvas id="cPR"></canvas></div>
<div class="pnote">Effect of selected feature after removing influence of all other features. Slope = partial regression coefficient.</div>
</div>
<div class="pc" id="permSyntheticCard" style="display:none">
<div class="pc-head"><div class="pc-title">Residuals Distribution</div></div>
<div class="cw"><canvas id="cHist"></canvas></div>
<div class="pnote">Distribution of test-set residuals. A symmetric bell shape supports normality assumption.</div>
</div>
</div>
<!-- DIAGNOSTIC PLOTS -->
<div class="sl">Assumption Diagnostics</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Residuals vs Fitted</div><div class="badge" id="bRvf">β€”</div></div>
<div class="cw"><canvas id="cRvf"></canvas></div>
<div class="pnote">Should be random around zero. A funnel or curve β†’ heteroscedasticity or non-linearity.</div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Q-Q Plot (Normality)</div><div class="badge" id="bQQ">β€”</div></div>
<div class="cw"><canvas id="cQQ"></canvas></div>
<div class="pnote" id="qqNote">Points near diagonal β†’ residuals approximately normal.</div>
</div>
</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">Scale-Location (Homoscedasticity)</div><div class="badge" id="bSL">β€”</div></div>
<div class="cw"><canvas id="cSL"></canvas></div>
<div class="pnote">Flat red trend β†’ equal variance. Rising trend β†’ heteroscedastic errors.</div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Residuals vs Leverage (Cook's Distance)</div><div class="badge" id="bCooks">β€”</div></div>
<div class="cw"><canvas id="cCooks"></canvas></div>
<div class="pnote">Points beyond the dashed threshold are influential β€” they disproportionately affect the fit.</div>
</div>
</div>
<!-- GRADIENT DESCENT ANIMATION -->
<div class="sl">Interactive β€” Gradient Descent</div>
<div class="card">
<div class="card-title">Gradient Descent Animation (1-D Regression)</div>
<div class="gd-controls">
<button class="gd-btn play" id="gdPlayBtn" onclick="gdToggle()">β–Ά Play</button>
<button class="gd-btn" onclick="gdReset()">β†Ί Reset</button>
<div class="gd-iter">Iteration: <span id="gdIter">0</span> / <span id="gdTotal">β€”</span></div>
<div class="gd-speed">
Speed:
<input type="range" id="gdSpeed" min="50" max="500" step="50" value="200" style="width:80px;accent-color:var(--teal)">
</div>
<div style="font-family:'DM Mono',monospace;font-size:12px;color:var(--text2)">
Ξ²β‚€=<span id="gdB0">β€”</span> &nbsp; β₁=<span id="gdB1">β€”</span> &nbsp; MSE=<span id="gdMSE">β€”</span>
</div>
</div>
<div class="gd-grid">
<div class="pc" style="margin:0"><div class="pc-head"><div class="pc-title">Data + Regression Line</div></div><div class="cw"><canvas id="gdLine"></canvas></div></div>
<div class="pc" style="margin:0"><div class="pc-head"><div class="pc-title">MSE over Iterations</div></div><div class="cw"><canvas id="gdLoss"></canvas></div></div>
<div class="pc" style="margin:0"><div class="pc-head"><div class="pc-title">Loss Surface (Ξ²β‚€, β₁)</div></div><div class="cw"><canvas id="gdSurface"></canvas></div></div>
</div>
<div class="pnote" style="margin-top:1rem">Gradient descent updates Ξ²β‚€, β₁ iteratively: Ξ² ← Ξ² βˆ’ Ξ·Β·βˆ‡MSE. The path on the loss surface shows how quickly it converges to the minimum.</div>
</div>
<!-- DIAGNOSTIC SUMMARY -->
<div class="sl">Diagnostic Summary</div>
<div class="diag-grid" id="diagGrid"></div>
</div><!-- /results -->
</div><!-- /main -->
<script>
// ═══════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════
const S = {
dataset:'linear', dsType:'synthetic',
modelType:'linear', alpha:1.0, testSize:.2,
featureNames:[], scatterFx:0,
result:null, regPathData:null, regPathMode:'ridge',
partialRegs:[], currentPR:0,
};
const SYN = new Set(['linear','polynomial','sinusoidal','heteroscedastic']);
let CH = {}; // chart instances
function dc(id){ if(CH[id]){CH[id].destroy();delete CH[id];} }
const COLORS = {
teal:'rgba(29,158,117,', blue:'rgba(55,138,221,',
orange:'rgba(224,122,48,', red:'rgba(192,57,43,',
text:'#1C2B3A', muted:'#8298AE',
};
const axOpts = (xl,yl) => ({
x:{title:{display:true,text:xl,font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{title:{display:true,text:yl,font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}},
});
const legOpts = { labels:{font:{family:"'DM Mono',monospace",size:11},padding:12} };
// ═══════════════════════════════════════════════════════════════
// DATASET / PARAMS
// ═══════════════════════════════════════════════════════════════
function selectDS(btn, ds){
document.querySelectorAll('.ds-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
S.dataset=ds; S.dsType=SYN.has(ds)?'synthetic':'real';
renderParams();
document.getElementById('results').style.display='none';
}
function renderParams(){
const p=document.getElementById('paramsPanel');
p.innerHTML = S.dsType==='synthetic' ? buildSynParams(S.dataset)
: `<div class="params-panel"><div class="pg-grid">
<div class="pg"><label>Dataset</label>
<div style="font-size:13px;color:var(--text2);padding-top:4px">${S.dataset} β€” all features used for training</div></div>
</div></div>`;
if(S.dsType==='real')
fetch(`/api/dataset-info/${S.dataset}`).then(r=>r.json()).then(d=>{ S.featureNames=d.features||[]; });
}
function buildSynParams(ds){
let ex='';
if(ds==='polynomial') ex=`
<div class="pg"><label>Degree <span id="degLbl">2</span></label><input type="range" id="degSlider" min="2" max="6" step="1" value="2" oninput="document.getElementById('degLbl').textContent=this.value"></div>
<div class="pg"><label>Outlier fraction <span id="outLbl">0%</span></label><input type="range" id="outSlider" min="0" max="0.25" step="0.01" value="0" oninput="document.getElementById('outLbl').textContent=Math.round(this.value*100)+'%'"></div>`;
if(ds==='sinusoidal') ex=`
<div class="pg"><label>Frequency <span id="freqLbl">1.0</span></label><input type="range" id="freqSlider" min="0.2" max="4" step="0.1" value="1.0" oninput="document.getElementById('freqLbl').textContent=parseFloat(this.value).toFixed(1)"></div>
<div class="pg"><label>Amplitude <span id="ampLbl">1.0</span></label><input type="range" id="ampSlider" min="0.5" max="5" step="0.25" value="1.0" oninput="document.getElementById('ampLbl').textContent=parseFloat(this.value).toFixed(2)"></div>
<div class="pg"><label>Phase Ο† <span id="phaseLbl">0.00</span></label><input type="range" id="phaseSlider" min="0" max="6.28" step="0.1" value="0" oninput="document.getElementById('phaseLbl').textContent=parseFloat(this.value).toFixed(2)"></div>`;
if(ds==='linear'||ds==='heteroscedastic') ex=`
<div class="pg"><label>Slope <span id="slopeLbl">2.0</span></label><input type="range" id="slopeSlider" min="-5" max="5" step="0.25" value="2.0" oninput="document.getElementById('slopeLbl').textContent=parseFloat(this.value).toFixed(2)"></div>
<div class="pg"><label>Intercept <span id="intLbl">1.0</span></label><input type="range" id="intSlider" min="-5" max="5" step="0.25" value="1.0" oninput="document.getElementById('intLbl').textContent=parseFloat(this.value).toFixed(2)"></div>
<div class="pg"><label>Outlier fraction <span id="outLbl">0%</span></label><input type="range" id="outSlider" min="0" max="0.25" step="0.01" value="0" oninput="document.getElementById('outLbl').textContent=Math.round(this.value*100)+'%'"></div>`;
return `<div class="params-panel"><div class="pg-grid">
<div class="pg"><label>N samples <span id="nLbl">200</span></label><input type="range" id="nSlider" min="50" max="800" step="25" value="200" oninput="document.getElementById('nLbl').textContent=this.value"></div>
<div class="pg"><label>Noise Οƒ <span id="noiseLbl">0.30</span></label><input type="range" id="noiseSlider" min="0" max="2" step="0.05" value="0.30" oninput="document.getElementById('noiseLbl').textContent=parseFloat(this.value).toFixed(2)"></div>
${ex}
</div></div>`;
}
function updateSplit(v){ S.testSize=(100-parseInt(v))/100; document.getElementById('splitLbl').textContent=`${v} / ${100-parseInt(v)} %`; }
function selectModel(btn,mt){ document.querySelectorAll('.mt').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); S.modelType=mt; document.getElementById('alphaRow').style.display=mt==='linear'?'none':'flex'; }
function updateAlpha(v){ S.alpha=Math.pow(10,parseFloat(v)); document.getElementById('alphaLbl').textContent=S.alpha.toFixed(S.alpha<0.1?4:S.alpha<10?2:1); }
// ═══════════════════════════════════════════════════════════════
// TRAINING
// ═══════════════════════════════════════════════════════════════
async function runTrain(){
const btn=document.getElementById('trainBtn');
btn.disabled=true; btn.innerHTML='<div class="spinner"></div> Training…';
const body={
dataset_type:S.dsType, test_size:S.testSize,
model_type:S.modelType, alpha:S.alpha,
feature_x:String(S.scatterFx),
};
if(S.dsType==='synthetic'){
body.synthetic_config={
dataset_type:S.dataset,
n_samples:parseInt(document.getElementById('nSlider')?.value||200),
noise:parseFloat(document.getElementById('noiseSlider')?.value||.3),
outlier_fraction:parseFloat(document.getElementById('outSlider')?.value||0),
degree:parseInt(document.getElementById('degSlider')?.value||2),
frequency:parseFloat(document.getElementById('freqSlider')?.value||1),
amplitude:parseFloat(document.getElementById('ampSlider')?.value||1),
phase:parseFloat(document.getElementById('phaseSlider')?.value||0),
slope:parseFloat(document.getElementById('slopeSlider')?.value||2),
intercept:parseFloat(document.getElementById('intSlider')?.value||1),
};
} else {
body.real_config={dataset_name:S.dataset};
}
try{
const res=await fetch('/api/linear-regression/train',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await res.json();
if(!d.ok){alert('Error: '+d.error);return;}
S.result=d; S.featureNames=d.feature_names||[];
S.regPathData=d.reg_path; S.partialRegs=d.partial_regression||[];
renderAll(d);
}catch(e){alert('Network error: '+e.message);}
finally{btn.disabled=false;btn.innerHTML='<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><polygon points="5,3 19,12 5,21"/></svg> Train Model';}
}
// ═══════════════════════════════════════════════════════════════
// RENDER ALL
// ═══════════════════════════════════════════════════════════════
function renderAll(d){
document.getElementById('results').style.display='';
renderMetrics(d.metrics, d.shapiro);
renderScatter(d.scatter, d.feature_names);
renderAvP(d.avp);
renderCoefs(d.coefs, d.coef_ci, d.feature_names);
renderCI(d.coef_ci, d.feature_names);
renderLC(d.learning_curve);
showRegPath(S.regPathMode);
renderPermImportance(d.perm_importance);
renderPartialRegPanel(d.partial_regression, d.is_synthetic);
renderRvF(d.rvf);
renderQQ(d.qq, d.shapiro);
renderSL(d.sl);
renderCooks(d.cooks);
gdInit(d.gradient_descent);
renderDiag(d);
document.getElementById('results').scrollIntoView({behavior:'smooth',block:'start'});
}
// ═══════════════════════════════════════════════════════════════
// METRICS
// ═══════════════════════════════════════════════════════════════
function renderMetrics(m, sw){
const r2c_tr = m.r2_train>0.7?'good':m.r2_train>0.4?'':'warn';
const r2c_te = m.r2_test >0.7?'good':m.r2_test >0.4?'':'warn';
const mapeStr = v => isFinite(v) ? v.toFixed(1)+'%' : 'N/A';
document.getElementById('metricsContainer').innerHTML=`
<div class="metrics-2col">
<div class="metrics-group">
<div class="mg-label" style="color:var(--teal-dark)">Training</div>
<div class="mrow-4">
<div class="mc ${r2c_tr}"><div class="lbl">RΒ²</div><div class="val">${m.r2_train.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">RMSE</div><div class="val">${m.rmse_train.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">MAE</div><div class="val">${m.mae_train.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">MAPE</div><div class="val">${mapeStr(m.mape_train)}</div></div>
</div>
</div>
<div class="metrics-group">
<div class="mg-label" style="color:var(--blue-dark)">Testing</div>
<div class="mrow-4">
<div class="mc ${r2c_te}"><div class="lbl">RΒ²</div><div class="val">${m.r2_test.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">RMSE</div><div class="val">${m.rmse_test.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">MAE</div><div class="val">${m.mae_test.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">MAPE</div><div class="val">${mapeStr(m.mape_test)}</div></div>
</div>
</div>
</div>
<div class="mrow-3">
<div class="mc"><div class="lbl">N Train</div><div class="val">${m.n_train}</div><div class="sub">samples</div></div>
<div class="mc"><div class="lbl">N Test</div><div class="val">${m.n_test}</div><div class="sub">samples</div></div>
<div class="mc ${sw.normal?'good':'warn'}"><div class="lbl">Shapiro p</div><div class="val">${sw.p.toFixed(4)}</div><div class="sub">${sw.normal?'Normal βœ“':'Non-normal !'}</div></div>
</div>`;
}
// ═══════════════════════════════════════════════════════════════
// SCATTER
// ═══════════════════════════════════════════════════════════════
function renderScatter(sc, fn){
dc('scatter');
const isSyn=!!sc.x_line;
const axDiv=document.getElementById('axSel');
if(!isSyn && fn.length>1){
axDiv.style.display='flex';
axDiv.innerHTML=`<label>X-axis:</label><select id="fxSel" onchange="changeAxis(this.value)">${fn.map((f,i)=>`<option value="${i}" ${i===S.scatterFx?'selected':''}>${f}</option>`).join('')}</select>`;
} else { axDiv.style.display='none'; }
const datasets=[
{label:'Train',data:sc.x_train.map((x,i)=>({x,y:sc.y_train[i]})),backgroundColor:COLORS.teal+'0.5)',pointRadius:3.5},
{label:'Test', data:sc.x_test.map((x,i)=>({x,y:sc.y_test[i]})), backgroundColor:COLORS.blue+'0.55)',pointRadius:3.5},
];
if(isSyn) datasets.push({label:'Regression line',data:sc.x_line.map((x,i)=>({x,y:sc.y_line[i]})),type:'line',borderColor:'#E07A30',borderWidth:2.5,pointRadius:0,fill:false,tension:0});
CH.scatter=new Chart(document.getElementById('cScatter'),{
type:'scatter',data:{datasets},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts(isSyn?'x':(sc.fx_name||fn[0]),'y')}
});
document.getElementById('scatterNote').textContent=isSyn?'Orange = fitted line. Green = train, Blue = test.':
`Showing "${sc.fx_name||fn[0]}" vs target. All features used in model.`;
}
function changeAxis(idx){ S.scatterFx=parseInt(idx); if(S.result) runTrain(); }
// ═══════════════════════════════════════════════════════════════
// ACTUAL vs PREDICTED
// ═══════════════════════════════════════════════════════════════
function renderAvP(avp){
dc('avp');
const mn=Math.min(...avp.actual,...avp.predicted), mx=Math.max(...avp.actual,...avp.predicted);
CH.avp=new Chart(document.getElementById('cAvp'),{
type:'scatter',
data:{datasets:[
{label:'Predictions',data:avp.actual.map((a,i)=>({x:a,y:avp.predicted[i]})),backgroundColor:COLORS.blue+'0.45)',pointRadius:3},
{label:'Perfect fit',data:[{x:mn,y:mn},{x:mx,y:mx}],type:'line',borderColor:'rgba(28,43,58,0.3)',borderWidth:1.5,borderDash:[5,4],pointRadius:0},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts('Actual','Predicted')}
});
}
// ═══════════════════════════════════════════════════════════════
// COEFFICIENTS TABLE + CI CHART
// ═══════════════════════════════════════════════════════════════
function renderCoefs(coefs, ci, fn){
const vals=Object.values(coefs); const maxA=Math.max(...vals.map(Math.abs));
document.getElementById('coefBody').innerHTML=Object.entries(coefs).map(([name,val])=>{
const pct=maxA>0?Math.abs(val)/maxA*100:0; const neg=val<0;
const info=ci[name]; const pStr=info?info.p_val.toFixed(3):'β€”';
const ciStr=info?`[${info.ci_lo.toFixed(3)}, ${info.ci_hi.toFixed(3)}]`:'β€”';
const sig=info&&info.p_val<.05;
return `<tr>
<td style="color:var(--text);font-weight:500">${name}</td>
<td style="color:${neg?'var(--orange)':'var(--teal-dark)'}">${val>=0?'+':''}${val.toFixed(4)}</td>
<td style="font-size:11px;color:var(--muted)">${ciStr}</td>
<td>${pStr}${info?(sig?'<span class="sig">*</span>':'<span class="nsig">ns</span>'):''}</td>
<td><span class="cbar-w"><span class="cbar${neg?' neg':''}" style="width:${pct.toFixed(1)}%"></span></span></td>
</tr>`;
}).join('');
}
function renderCI(ci, fn){
dc('ci');
if(!ci||Object.keys(ci).length===0){
document.getElementById('cCI').parentElement.innerHTML='<div style="text-align:center;padding:3rem;color:var(--muted);font-size:13px">CI available for OLS only</div>';
return;
}
const names=Object.keys(ci).filter(k=>k!=='intercept');
const coefs=names.map(k=>ci[k].coef);
const ciLo=names.map(k=>ci[k].ci_lo);
const ciHi=names.map(k=>ci[k].ci_hi);
CH.ci=new Chart(document.getElementById('cCI'),{
type:'bar',
data:{labels:names,datasets:[
{label:'Coefficient',data:coefs,backgroundColor:names.map((_,i)=>coefs[i]<0?COLORS.orange+'0.7)':COLORS.teal+'0.7)'),borderRadius:4,
errorBars:names.reduce((o,n,i)=>{o[i]={plus:ciHi[i]-coefs[i],minus:coefs[i]-ciLo[i]};return o;},{})},
]},
options:{
responsive:true,maintainAspectRatio:false,
plugins:{legend:{display:false},
tooltip:{callbacks:{label:ctx=>{const n=names[ctx.dataIndex];return [`Coef: ${ci[n].coef.toFixed(4)}`,`95% CI: [${ci[n].ci_lo.toFixed(4)}, ${ci[n].ci_hi.toFixed(4)}]`,`p = ${ci[n].p_val.toFixed(4)}`];}}}
},
scales:{
x:{grid:{display:false},ticks:{font:{family:"'DM Mono',monospace",size:10}}},
y:{title:{display:true,text:'Coefficient value',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},
afterDataLimits:sc=>{const abs=Math.max(Math.abs(sc.min),Math.abs(sc.max));sc.min=-abs*1.2;sc.max=abs*1.2;}},
}
}
});
// draw CI as custom lines after render
const orig=CH.ci.draw.bind(CH.ci);
CH.ci.draw=function(){
orig();
const ctx2=CH.ci.ctx, meta=CH.ci.getDatasetMeta(0);
ctx2.save(); ctx2.strokeStyle='rgba(28,43,58,0.55)'; ctx2.lineWidth=2;
meta.data.forEach((bar,i)=>{
const x=bar.x;
const yLo=CH.ci.scales.y.getPixelForValue(ciLo[i]);
const yHi=CH.ci.scales.y.getPixelForValue(ciHi[i]);
ctx2.beginPath(); ctx2.moveTo(x,yLo); ctx2.lineTo(x,yHi); ctx2.stroke();
[yLo,yHi].forEach(yy=>{ctx2.beginPath();ctx2.moveTo(x-5,yy);ctx2.lineTo(x+5,yy);ctx2.stroke();});
});
ctx2.restore();
};
}
// ═══════════════════════════════════════════════════════════════
// LEARNING CURVE
// ═══════════════════════════════════════════════════════════════
function renderLC(lc){
dc('lc');
CH.lc=new Chart(document.getElementById('cLC'),{
type:'line',
data:{labels:lc.sizes,datasets:[
{label:'Train RΒ²',data:lc.train,borderColor:COLORS.teal+'1)',backgroundColor:COLORS.teal+'0.1)',borderWidth:2.2,fill:true,tension:.4,pointRadius:4},
{label:'Val RΒ²', data:lc.val, borderColor:COLORS.blue+'1)',backgroundColor:COLORS.blue+'0.1)',borderWidth:2.2,fill:true,tension:.4,pointRadius:4},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},
scales:{x:{title:{display:true,text:'Training set size',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{title:{display:true,text:'RΒ²',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},min:-0.1,max:1.05}}}
});
}
// ═══════════════════════════════════════════════════════════════
// REGULARIZATION PATH
// ═══════════════════════════════════════════════════════════════
const REG_COLORS=['#1D9E75','#378ADD','#E07A30','#C0392B','#8E44AD','#16A085','#D35400','#2980B9','#27AE60','#E74C3C','#F39C12','#2C3E50','#95A5A6'];
function showRegPath(mode){
S.regPathMode=mode;
['Ridge','Lasso'].forEach(m=>{
const b=document.getElementById(`regPath${m}Btn`);
b.style.background=mode===m.toLowerCase()?'var(--teal)':'';
b.style.color=mode===m.toLowerCase()?'#fff':'';
b.style.borderColor=mode===m.toLowerCase()?'var(--teal)':'';
});
if(!S.regPathData) return;
dc('regPath');
const rp=S.regPathData;
const coefs=mode==='ridge'?rp.ridge_coefs:rp.lasso_coefs;
const fn=rp.feature_names;
// transpose: per feature
const datasets=fn.slice(0,10).map((name,j)=>({
label:name,
data:coefs.map(row=>row[j]),
borderColor:REG_COLORS[j%REG_COLORS.length],
borderWidth:1.8,pointRadius:0,fill:false,tension:.3,
}));
CH.regPath=new Chart(document.getElementById('cRegPath'),{
type:'line',
data:{labels:rp.alphas.map(v=>v.toFixed(1)),datasets},
options:{responsive:true,maintainAspectRatio:false,
plugins:{legend:{labels:{font:{family:"'DM Mono',monospace",size:9},padding:8,boxWidth:10}}},
scales:{
x:{title:{display:true,text:'log₁₀(Ξ»)',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},ticks:{maxTicksLimit:8}},
y:{title:{display:true,text:'Coefficient value',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}},
}}
});
}
// ═══════════════════════════════════════════════════════════════
// PERMUTATION IMPORTANCE
// ═══════════════════════════════════════════════════════════════
function renderPermImportance(imp){
if(!imp||imp.length===0){document.getElementById('permBars').innerHTML='<div style="color:var(--muted);font-size:13px">Not available</div>';return;}
const max=Math.max(...imp.map(i=>Math.max(i.mean,0)))||1;
document.getElementById('permBars').innerHTML=imp.map(it=>`
<div class="perm-row">
<div class="perm-feat">${it.feature}</div>
<div class="perm-bar-w"><div class="perm-bar-f" style="width:${Math.max(0,it.mean/max*100).toFixed(1)}%;background:${it.mean<0?'var(--orange)':'var(--teal)'}"></div></div>
<div class="perm-val">${it.mean.toFixed(3)}</div>
</div>`).join('');
}
// ═══════════════════════════════════════════════════════════════
// PARTIAL REGRESSION
// ═══════════════════════════════════════════════════════════════
function renderPartialRegPanel(prs, isSynthetic){
document.getElementById('partialRegCard').style.display = (!isSynthetic&&prs.length>0) ? '' : 'none';
document.getElementById('permSyntheticCard').style.display = isSynthetic ? '' : 'none';
if(isSynthetic){ renderHist(S.result.rvf.residuals); return; }
if(!prs||prs.length===0) return;
const sel=document.getElementById('prSel');
sel.innerHTML=prs.map((p,i)=>`<option value="${i}">${p.feature}</option>`).join('');
showPartialReg(0);
}
function showPartialReg(idx){
S.currentPR=parseInt(idx);
const pr=S.partialRegs[S.currentPR]; if(!pr) return;
dc('pr');
const mn=Math.min(...pr.ex), mx=Math.max(...pr.ex);
const yLine=[mn,mx].map(x=>pr.slope*x);
CH.pr=new Chart(document.getElementById('cPR'),{
type:'scatter',
data:{datasets:[
{label:'Observations',data:pr.ex.map((x,i)=>({x,y:pr.ey[i]})),backgroundColor:COLORS.teal+'0.5)',pointRadius:3},
{label:`slope=${pr.slope.toFixed(3)}`,data:[{x:mn,y:yLine[0]},{x:mx,y:yLine[1]}],type:'line',borderColor:'#E07A30',borderWidth:2,pointRadius:0,fill:false},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},
scales:axOpts(`e(${pr.feature}|others)`,`e(y|others)`)}
});
}
// Histogram (synthetic only)
function renderHist(residuals){
dc('hist');
const bins=20, mn=Math.min(...residuals), mx=Math.max(...residuals), step=(mx-mn)/bins;
const counts=Array(bins).fill(0);
const labels=Array.from({length:bins},(_,i)=>(mn+i*step).toFixed(2));
residuals.forEach(r=>{ let b=Math.floor((r-mn)/step); if(b>=bins)b=bins-1; counts[b]++; });
CH.hist=new Chart(document.getElementById('cHist'),{
type:'bar',data:{labels,datasets:[{label:'Count',data:counts,backgroundColor:COLORS.teal+'0.55)',borderColor:COLORS.teal+'0.9)',borderWidth:1,borderRadius:3}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{title:{display:true,text:'Residual',font:{size:11}},grid:{display:false},ticks:{maxTicksLimit:8}},y:{title:{display:true,text:'Freq',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}}}}
});
}
// ═══════════════════════════════════════════════════════════════
// DIAGNOSTIC PLOTS
// ═══════════════════════════════════════════════════════════════
function corr(a,b){ const ma=a.reduce((s,v)=>s+v,0)/a.length, mb=b.reduce((s,v)=>s+v,0)/b.length; const num=a.reduce((s,v,i)=>s+(v-ma)*(b[i]-mb),0); const da=Math.sqrt(a.reduce((s,v)=>s+(v-ma)**2,0)), db=Math.sqrt(b.reduce((s,v)=>s+(v-mb)**2,0)); return num/(da*db+1e-12); }
function renderRvF(rvf){
dc('rvf');
const c=Math.abs(corr(rvf.fitted,rvf.residuals.map(Math.abs)));
const b=document.getElementById('bRvf');
b.textContent=c>0.25?'Pattern detected':'Random βœ“'; b.className='badge '+(c>0.25?'b-warn':'b-ok');
const mn=Math.min(...rvf.fitted), mx=Math.max(...rvf.fitted);
CH.rvf=new Chart(document.getElementById('cRvf'),{
type:'scatter',data:{datasets:[
{label:'Residual',data:rvf.fitted.map((f,i)=>({x:f,y:rvf.residuals[i]})),backgroundColor:COLORS.teal+'0.4)',pointRadius:2.5},
{label:'Zero',data:[{x:mn,y:0},{x:mx,y:0}],type:'line',borderColor:COLORS.orange+'0.7)',borderWidth:1.5,borderDash:[4,4],pointRadius:0},
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts('Fitted','Residuals')}
});
}
function renderQQ(qq,sw){
dc('qq');
const b=document.getElementById('bQQ');
b.textContent=sw.normal?'Normal βœ“':'Non-normal !'; b.className='badge '+(sw.normal?'b-ok':'b-warn');
document.getElementById('qqNote').textContent=`Shapiro-Wilk: W=${sw.stat.toFixed(4)}, p=${sw.p.toFixed(4)}. `+(sw.normal?'Residuals approx. normal (p>0.05).':'Residuals deviate from normality (p≀0.05).');
const mn=Math.min(...qq.theoretical), mx=Math.max(...qq.theoretical);
const std=Math.sqrt(qq.sample.reduce((s,v)=>s+(v-(qq.sample.reduce((a,b)=>a+b,0)/qq.sample.length))**2,0)/qq.sample.length);
CH.qq=new Chart(document.getElementById('cQQ'),{
type:'scatter',data:{datasets:[
{label:'Q-Q',data:qq.theoretical.map((t,i)=>({x:t,y:qq.sample[i]})),backgroundColor:COLORS.blue+'0.5)',pointRadius:2.5},
{label:'Normal',data:[{x:mn,y:mn*std},{x:mx,y:mx*std}],type:'line',borderColor:COLORS.orange+'0.8)',borderWidth:1.5,borderDash:[4,4],pointRadius:0},
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts('Theoretical Quantiles','Sample Quantiles')}
});
}
function renderSL(sl){
dc('sl');
const c=Math.abs(corr(sl.fitted,sl.sqrt_abs_resid));
const b=document.getElementById('bSL');
b.textContent=c>0.25?'Heteroscedastic !':'Homoscedastic βœ“'; b.className='badge '+(c>0.25?'b-warn':'b-ok');
CH.sl=new Chart(document.getElementById('cSL'),{
type:'scatter',data:{datasets:[{label:'√|Res|',data:sl.fitted.map((f,i)=>({x:f,y:sl.sqrt_abs_resid[i]})),backgroundColor:COLORS.teal+'0.4)',pointRadius:2.5}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:axOpts('Fitted','√|Residuals|')}
});
}
function renderCooks(ck){
dc('cooks');
const n_inf=ck.influential.length;
const b=document.getElementById('bCooks');
b.textContent=n_inf>0?`${n_inf} influential`:'None βœ“'; b.className='badge '+(n_inf>0?'b-warn':'b-ok');
const colors=ck.index.map(i=>ck.influential.includes(i)?COLORS.red+'0.8)':COLORS.blue+'0.45)');
const mn=Math.min(...ck.index), mx=Math.max(...ck.index);
CH.cooks=new Chart(document.getElementById('cCooks'),{
type:'scatter',
data:{datasets:[
{label:"Cook's D",data:ck.index.map((i,ii)=>({x:i,y:ck.distance[ii]})),backgroundColor:colors,pointRadius:4},
{label:'Threshold',data:[{x:mn,y:ck.threshold},{x:mx,y:ck.threshold}],type:'line',borderColor:COLORS.red+'0.7)',borderWidth:1.5,borderDash:[4,4],pointRadius:0},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts('Observation index',"Cook's Distance")}
});
}
// ═══════════════════════════════════════════════════════════════
// GRADIENT DESCENT ANIMATION
// ═══════════════════════════════════════════════════════════════
let GD = { data:null, step:0, timer:null, playing:false };
function gdInit(gd){
GD.data=gd; GD.step=0; GD.playing=false;
if(GD.timer){clearInterval(GD.timer);GD.timer=null;}
document.getElementById('gdPlayBtn').textContent='β–Ά Play';
document.getElementById('gdTotal').textContent=gd.path.length;
gdDrawAll(0);
}
function gdToggle(){
if(!GD.data) return;
GD.playing=!GD.playing;
document.getElementById('gdPlayBtn').textContent=GD.playing?'⏸ Pause':'β–Ά Play';
if(GD.playing){
GD.timer=setInterval(()=>{
if(GD.step>=GD.data.path.length-1){GD.playing=false;document.getElementById('gdPlayBtn').textContent='β–Ά Play';clearInterval(GD.timer);return;}
GD.step++; gdDrawAll(GD.step);
}, 550 - parseInt(document.getElementById('gdSpeed').value));
} else {
clearInterval(GD.timer);
}
}
function gdReset(){ if(GD.timer){clearInterval(GD.timer);GD.timer=null;} GD.playing=false; GD.step=0; document.getElementById('gdPlayBtn').textContent='β–Ά Play'; if(GD.data) gdDrawAll(0); }
function gdDrawAll(step){
if(!GD.data) return;
const p=GD.data.path[step];
document.getElementById('gdIter').textContent=step;
document.getElementById('gdB0').textContent=p.b0.toFixed(4);
document.getElementById('gdB1').textContent=p.b1.toFixed(4);
document.getElementById('gdMSE').textContent=p.mse.toFixed(4);
gdDrawLine(step); gdDrawLoss(step); gdDrawSurface(step);
}
function gdDrawLine(step){
dc('gdLine');
const gd=GD.data, p=gd.path[step];
const xr=[Math.min(...gd.x_data), Math.max(...gd.x_data)];
const yLine=xr.map(x=>p.b0+p.b1*x);
CH.gdLine=new Chart(document.getElementById('gdLine'),{
type:'scatter',
data:{datasets:[
{label:'Data',data:gd.x_data.map((x,i)=>({x,y:gd.y_data[i]})),backgroundColor:COLORS.teal+'0.4)',pointRadius:3},
{label:`Ε· = ${p.b0.toFixed(2)} + ${p.b1.toFixed(2)}x`,data:xr.map((x,i)=>({x,y:yLine[i]})),type:'line',borderColor:'#E07A30',borderWidth:2.5,pointRadius:0,fill:false,tension:0},
]},
options:{animation:false,responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},scales:axOpts('x','y')}
});
}
function gdDrawLoss(step){
dc('gdLoss');
const gd=GD.data;
const allMSE=gd.path.map(p=>p.mse);
const shown=allMSE.slice(0,step+1);
CH.gdLoss=new Chart(document.getElementById('gdLoss'),{
type:'line',
data:{labels:Array.from({length:shown.length},(_,i)=>i),datasets:[{label:'MSE',data:shown,borderColor:COLORS.orange+'1)',backgroundColor:COLORS.orange+'0.1)',borderWidth:2,fill:true,tension:.3,pointRadius:0}]},
options:{animation:false,responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},
scales:{x:{title:{display:true,text:'Iteration',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},ticks:{maxTicksLimit:8}},
y:{title:{display:true,text:'MSE',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},min:0,max:Math.max(...allMSE)*1.05}}}
});
}
function gdDrawSurface(step){
dc('gdSurf');
const gd=GD.data;
// draw contour as scatter coloured by MSE value, then overlay path
const pts=[], cols=[];
let zMin=Infinity, zMax=-Infinity;
gd.Z.forEach(row=>row.forEach(v=>{zMin=Math.min(zMin,v);zMax=Math.max(zMax,v);}));
gd.b0_grid.forEach((b0,i)=>gd.b1_grid.forEach((b1,j)=>{
const z=gd.Z[i][j];
const t=(z-zMin)/(zMax-zMin+1e-12);
const r=Math.round(29+(192-29)*t), g=Math.round(158+(57-158)*t), b=Math.round(117+(43-117)*t);
pts.push({x:b0,y:b1}); cols.push(`rgba(${r},${g},${b},0.7)`);
}));
const path=gd.path.slice(0,step+1);
CH.gdSurf=new Chart(document.getElementById('gdSurface'),{
type:'scatter',
data:{datasets:[
{label:'Loss surface',data:pts,backgroundColor:cols,pointRadius:5,pointStyle:'rect'},
{label:'GD path',data:path.map(p=>({x:p.b0,y:p.b1})),type:'line',borderColor:'#fff',borderWidth:2,pointRadius:3,pointBackgroundColor:'#fff',fill:false,tension:.3},
{label:'Current',data:[{x:path[path.length-1].b0,y:path[path.length-1].b1}],backgroundColor:'#FFD700',pointRadius:8,pointStyle:'star'},
]},
options:{animation:false,responsive:true,maintainAspectRatio:false,
plugins:{legend:{display:false},tooltip:{filter:d=>d.datasetIndex===2}},
scales:axOpts('Ξ²β‚€','β₁')}
});
}
// ═══════════════════════════════════════════════════════════════
// DIAGNOSTIC SUMMARY
// ═══════════════════════════════════════════════════════════════
function renderDiag(d){
const sw=d.shapiro;
const hasPatRvF=Math.abs(corr(d.rvf.fitted,d.rvf.residuals.map(Math.abs)))>0.25;
const hasTrend=Math.abs(corr(d.sl.fitted,d.sl.sqrt_abs_resid))>0.25;
const overfit=(d.metrics.r2_train-d.metrics.r2_test)>0.15;
const nInfl=d.cooks.influential.length;
const items=[
{ok:sw.normal,title:'Normality',text:sw.normal?`Shapiro-Wilk p=${sw.p.toFixed(4)}>0.05. Residuals approximately normal β€” OLS inference valid.`:`p=${sw.p.toFixed(4)}≀0.05. Residuals non-normal. Consider log(y) or robust regression.`},
{ok:!hasTrend,title:'Homoscedasticity',text:hasTrend?'Scale-Location shows increasing spread. Consider log(y) or WLS.':'Residual spread constant across fitted values. βœ“'},
{ok:!hasPatRvF,title:'Linearity',text:hasPatRvF?'Non-random pattern in residuals vs fitted. Consider polynomial features.':'No systematic pattern. Linearity assumption satisfied. βœ“'},
{ok:!overfit,title:'Overfitting',text:overfit?`Train RΒ²=${d.metrics.r2_train.toFixed(3)} >> Test RΒ²=${d.metrics.r2_test.toFixed(3)}. Try Ridge/Lasso.`:`Train RΒ²β‰ˆTest RΒ² (${d.metrics.r2_train.toFixed(3)} vs ${d.metrics.r2_test.toFixed(3)}). Good generalization. βœ“`},
{ok:nInfl===0,title:"Influential Points",text:nInfl>0?`${nInfl} influential observations (Cook's D > threshold). Check for data errors.`:'No influential observations detected. βœ“'},
{ok:d.metrics.r2_test>0.5,title:'Overall Fit',text:`RΒ²=${d.metrics.r2_test.toFixed(3)}, RMSE=${d.metrics.rmse_test.toFixed(3)}, MAE=${d.metrics.mae_test.toFixed(3)}. `+(d.metrics.r2_test>0.7?'Good fit.':d.metrics.r2_test>0.4?'Moderate fit.':'Poor fit β€” linear model may be insufficient.')},
];
document.getElementById('diagGrid').innerHTML=items.map(it=>`
<div class="dc" style="border-left:3px solid ${it.ok?'var(--teal)':'var(--orange)'}">
<h4 style="color:${it.ok?'var(--teal-dark)':'var(--orange)'}">${it.ok?'βœ“':'!'} ${it.title}</h4>
<p>${it.text}</p>
</div>`).join('');
}
// ═══════════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════════
renderParams();
</script>
</body>
</html>