ML_course / static /logistic_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>Logistic 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;
--purple:#8E44AD; --purple-light:#F5EEF8;
--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);
}
*,*::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{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(--blue)}
.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(--blue-dark)}
.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(--blue-dark);display:flex;align-items:center;gap:8px;margin-bottom:1rem}
.ph-eye::before{content:'';display:block;width:28px;height:1.5px;background:var(--blue)}
.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-clf{background:#E6F1FB;color:#185FA5;border:1px solid #A8CAEE}
.main{max-width:1200px;margin:0 auto;padding:0 2.5rem 6rem}
.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-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(--blue-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(--blue);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(--blue-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}
.sig-panel{background:var(--bg-muted);border:1px solid var(--border);border-radius:12px;padding:1.4rem;margin-bottom:1.5rem;display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:center}
.sig-ctrl{display:flex;flex-direction:column;gap:10px}
.sig-ctrl label{font-family:'DM Mono',monospace;font-size:10.5px;color:var(--text2);display:flex;justify-content:space-between}
.sig-ctrl label span{color:var(--blue-dark);font-weight:500}
.sig-output{font-family:'DM Mono',monospace;font-size:26px;color:var(--blue-dark);text-align:center;margin-top:.5rem}
.sig-sub{font-family:'DM Mono',monospace;font-size:11px;color:var(--muted);text-align:center}
.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}
.ds-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:1.2rem}
.ds-grid-real{display:grid;grid-template-columns:repeat(3,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(--blue);background:var(--blue-light)}
.ds-btn.active{border-color:var(--blue);background:var(--blue-light);box-shadow:0 0 0 3px rgba(55,138,221,.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(--blue-dark);margin-top:2px}
.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(--blue-dark);font-weight:500}
.pg input[type=range]{width:100%;accent-color:var(--blue);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(--blue)}
.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)}
.c-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(--blue);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(--blue-dark);transform:translateY(-1px)}
.btn-train:disabled{background:var(--muted);cursor:not-allowed;transform:none}
.mbar{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:1.5rem}
.mc{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.8rem .9rem;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:16px;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-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)}
.b-info{background:var(--blue-light);color:var(--blue-dark)}
.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}
.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:110px;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.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(--blue);transition:width .6s ease}
.perm-val{font-family:'DM Mono',monospace;font-size:11px;color:var(--muted);min-width:50px}
.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}
.thresh-panel{background:var(--bg-muted);border:1px solid var(--border);border-radius:12px;padding:1.4rem 1.6rem;margin-bottom:1.5rem}
.thresh-ctrl{display:flex;align-items:center;gap:14px;margin-bottom:1.2rem;flex-wrap:wrap}
.thresh-val{font-family:'DM Mono',monospace;font-size:22px;color:var(--blue-dark);min-width:50px}
.thresh-mbar{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:1rem}
.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)}}
/* 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-5{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
.mrow-2{display:grid;grid-template-columns:repeat(2,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)}
@media(max-width:900px){.theory-grid,.plots-2,.diag-grid,.sig-panel{grid-template-columns:1fr}.mbar{grid-template-columns:repeat(4,1fr)}.ds-grid,.ds-grid-real{grid-template-columns:repeat(2,1fr)}.metrics-2col{grid-template-columns:1fr}.mrow-5{grid-template-columns:repeat(3,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-5{grid-template-columns:repeat(2,1fr)}.mrow-2{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" 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 active-blue" 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> / Logistic Regression</div>
</nav>
<div class="ph">
<div class="ph-eye">Supervised Learning</div>
<h1>Logistic Regression</h1>
<div class="tags">
<span class="tag tag-sup">Supervised</span>
<span class="tag tag-clf">Classification</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>Models the <em>probability</em> that an observation belongs to a class. The log-odds of the positive class is modelled as a linear combination of features; the sigmoid function squashes this to (0, 1), giving a probability. A decision boundary at threshold 0.5 separates classes.</p></div>
<div class="tc"><h3>When to use</h3><p>Binary or multi-class classification where you need well-calibrated class probabilities (e.g. medical diagnosis, credit scoring). Assumes each class is (roughly) linearly separable in feature space. Add regularization (L1 / L2) when features are many or correlated.</p></div>
<div class="tc"><h3>Key strength</h3><p>Outputs <em>calibrated probabilities</em> β€” not just a label. Coefficients are interpretable as log-odds ratios: e˒ᡉ means "the odds multiply by e˒ᡉ for each unit increase in xβ±Ό". <strong>Limitation:</strong> linear decision boundary; fails on XOR-type data (use kernel or neural net).</p></div>
<div class="tc"><h3>Regularization β€” note the sign flip</h3><p><strong>C = 1/Ξ»</strong> β€” the <em>inverse</em> of regularization strength. Smaller C β†’ stronger regularization β†’ simpler boundary. <strong>L2</strong> shrinks all coefficients. <strong>L1</strong> zeroes irrelevant features (feature selection). <strong>ElasticNet</strong> blends both.</p></div>
</div>
<div class="formula-card">
<h3>Hypothesis, Loss &amp; Multi-class</h3>
<div class="fl lg">Οƒ(z) = 1 / (1 + e⁻ᢻ)&nbsp;&nbsp;&nbsp;&nbsp;z = Ξ²β‚€ + β₁x₁ + … + Ξ²β‚™xβ‚™</div>
<div class="fl">P(y=1|x) = Οƒ(z)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Decision boundary: z = 0 &nbsp;⟺&nbsp; P = 0.5</div>
<div class="fl">Loss (Cross-Entropy) = βˆ’(1/n) Ξ£ [ yα΅’ log Ε·α΅’ + (1βˆ’yα΅’) log(1βˆ’Ε·α΅’) ]</div>
<div class="fl sub">L2: Loss + (1/2C)·Σβⱼ² &nbsp;|&nbsp; L1: Loss + (1/C)·Σ|βⱼ| &nbsp;|&nbsp; Multi-class: One-vs-Rest (OvR) by default</div>
</div>
<!-- Sigmoid visualizer -->
<div class="sl">Interactive β€” Sigmoid Function</div>
<div class="sig-panel">
<div class="sig-ctrl">
<div style="font-family:'DM Mono',monospace;font-size:10.5px;color:var(--text2);margin-bottom:4px">Move z along the log-odds axis to see how it maps to a probability:</div>
<label>z (log-odds) <span id="sigZLbl">0.00</span></label>
<input type="range" id="sigSlider" min="-6" max="6" step="0.1" value="0"
oninput="updateSigmoid(this.value)" style="accent-color:var(--blue)">
<div class="sig-output" id="sigOut">Οƒ(z) = 0.500</div>
<div class="sig-sub" id="sigInterp">Boundary: exactly 50% probability</div>
</div>
<div style="position:relative;height:180px"><canvas id="cSig"></canvas></div>
</div>
<!-- ══ DATASET SELECTION ════════════════════════════════════════════ -->
<div class="sep"></div>
<div class="sl">Dataset Selection</div>
<div class="card">
<div class="card-title">Synthetic Datasets β€” 2D (decision boundary visible)</div>
<div class="ds-grid">
<button class="ds-btn active" data-ds="moons" onclick="selectDS(this,'moons')"> <div class="ds-name">Moons</div> <div class="ds-dim">2D Β· 2 classes</div><div class="ds-type">synthetic</div></button>
<button class="ds-btn" data-ds="circles" onclick="selectDS(this,'circles')"><div class="ds-name">Circles</div> <div class="ds-dim">2D Β· 2 classes</div><div class="ds-type">synthetic</div></button>
<button class="ds-btn" data-ds="blobs" onclick="selectDS(this,'blobs')"> <div class="ds-name">Blobs</div> <div class="ds-dim">2D Β· N classes</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-real">
<button class="ds-btn" data-ds="iris" onclick="selectDS(this,'iris')"> <div class="ds-name">Iris</div> <div class="ds-dim">4D Β· 150 rows Β· 3 classes</div><div class="ds-type">real</div></button>
<button class="ds-btn" data-ds="wine_clf" onclick="selectDS(this,'wine_clf')"> <div class="ds-name">Wine</div> <div class="ds-dim">13D Β· 178 rows Β· 3 classes</div><div class="ds-type">real</div></button>
<button class="ds-btn" data-ds="breast_cancer" onclick="selectDS(this,'breast_cancer')"><div class="ds-name">Breast Cancer</div><div class="ds-dim">30D Β· 569 rows Β· 2 classes</div><div class="ds-type">real</div></button>
</div>
<div id="paramsPanel"></div>
<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>
<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">Regularization</div>
<div class="model-tabs">
<button class="mt active" onclick="selectModel(this,'l2')">L2</button>
<button class="mt" onclick="selectModel(this,'l1')">L1</button>
<button class="mt" onclick="selectModel(this,'elasticnet')">ElasticNet</button>
<button class="mt" onclick="selectModel(this,'none')">None</button>
</div>
</div>
<div id="cRow">
<div style="font-family:'DM Mono',monospace;font-size:10px;letter-spacing:.10em;text-transform:uppercase;color:var(--muted);margin-bottom:6px">C = 1/Ξ» &nbsp;<span style="font-weight:300;color:var(--muted)">← stronger reg | weaker reg β†’</span></div>
<div class="c-wrap">
<input type="range" id="cSlider" min="-3" max="3" step="0.1" value="0"
style="width:130px;accent-color:var(--blue)" oninput="updateC(this.value)">
<span id="cLbl" style="font-family:'DM Mono',monospace;font-size:13px;color:var(--blue-dark)">1.00</span>
</div>
</div>
<div id="l1RatioRow" 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">L1 ratio</div>
<div class="c-wrap">
<input type="range" id="l1RatioSlider" min="0" max="1" step="0.05" value="0.5"
style="width:100px;accent-color:var(--blue)" oninput="updateL1Ratio(this.value)">
<span id="l1RatioLbl" style="font-family:'DM Mono',monospace;font-size:13px;color:var(--blue-dark)">0.50</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>
<!-- MODEL FIT -->
<div class="sl">Model Fit</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title" id="fitTitle">Decision Boundary</div></div>
<!-- 2-D decision boundary (synthetic) -->
<div id="dbWrap" class="cw tall" style="position:relative">
<canvas id="cDB" style="position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
</div>
<!-- Coefficient chart (real high-D) -->
<div id="coefWrap" class="cw tall" style="display:none">
<canvas id="cCoef"></canvas>
</div>
<div class="pnote" id="fitNote"></div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Confusion Matrix</div><div class="badge b-info" id="bCM">β€”</div></div>
<div class="cw tall" style="display:flex;align-items:center;justify-content:center">
<canvas id="cCM"></canvas>
</div>
<div class="pnote">Rows = True class, Columns = Predicted. Diagonal = correct. Off-diagonal = misclassifications.</div>
</div>
</div>
<!-- ROC + PR -->
<div class="sl">Probabilistic Performance</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title">ROC Curve</div></div>
<div class="cw"><canvas id="cROC"></canvas></div>
<div class="pnote">True Positive Rate vs False Positive Rate. AUC = 1 is perfect; AUC = 0.5 is random. Multi-class: One-vs-Rest per class.</div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Precision-Recall Curve</div></div>
<div class="cw"><canvas id="cPR"></canvas></div>
<div class="pnote">Better diagnostic than ROC on imbalanced datasets. AP = area under this curve. High precision + high recall = ideal classifier.</div>
</div>
</div>
<!-- PROB DIST + CALIBRATION -->
<div class="sl">Probability Output</div>
<div class="plots-2">
<div class="pc">
<div class="pc-head"><div class="pc-title" id="probDistTitle">Predicted Probability Distribution</div></div>
<div class="cw"><canvas id="cProbDist"></canvas></div>
<div class="pnote" id="probDistNote">Histograms of predicted probabilities split by true class. A good model produces well-separated distributions.</div>
</div>
<div class="pc">
<div class="pc-head"><div class="pc-title">Calibration Curve (Reliability Diagram)</div></div>
<div class="cw"><canvas id="cCalib"></canvas></div>
<div class="pnote">Mean predicted probability vs fraction of true positives per bin. The diagonal = perfectly calibrated model. Logistic regression is typically well-calibrated.</div>
</div>
</div>
<!-- FEATURE IMPORTANCE -->
<div class="sl">Feature Importance</div>
<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 accuracy when a feature is shuffled (avg over 20 repeats). Larger drop β†’ feature is more important to the model.</div>
</div>
<div class="pc" id="coefChartCard">
<div class="pc-head">
<div class="pc-title">Coefficient Magnitudes</div>
<div id="coefClassSel" style="display:none"></div>
</div>
<div class="cw tall"><canvas id="cCoefBar"></canvas></div>
<div class="pnote" id="coefNote">Positive coefficient β†’ log-odds increase (pushes toward class). Negative β†’ pushes away. Magnitude reflects feature influence after scaling.</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 accuracy as training set grows. Large gap β†’ overfitting (increase regularization / get more data). Both low β†’ underfitting (decrease regularization / add features).</div>
</div>
<div class="pc">
<div class="pc-head">
<div class="pc-title">Regularization Path (C sweep)</div>
<div style="display:flex;gap:6px">
<button class="mt" id="rpCoefsBtn" onclick="showRegPath('coefs')" style="padding:4px 10px;font-size:10px">Coefs</button>
<button class="mt" id="rpAccBtn" onclick="showRegPath('acc')" style="padding:4px 10px;font-size:10px">Accuracy</button>
</div>
</div>
<div class="cw"><canvas id="cRegPath"></canvas></div>
<div class="pnote">Effect of C (= 1/Ξ») on model. Left = strong regularization, right = weak. Coef view: L1 zeroes features. Accuracy view: find the sweet spot between underfitting and overfitting.</div>
</div>
</div>
<!-- THRESHOLD SLIDER (binary only) -->
<div id="threshSection" style="display:none">
<div class="sl">Interactive β€” Decision Threshold</div>
<div class="card">
<div class="card-title">Threshold Analysis β€” Binary Classification</div>
<p style="font-size:13.5px;color:var(--text2);line-height:1.75;margin-bottom:1.2rem;font-weight:300">
The default threshold is 0.5: predict positive if P(class) β‰₯ 0.5. Lowering it increases <strong>Recall</strong> (catches more positives) at the cost of <strong>Precision</strong> (more false alarms). This tradeoff is crucial in medical diagnosis, fraud detection, and other asymmetric cost settings.
</p>
<div class="thresh-ctrl">
<div style="font-family:'DM Mono',monospace;font-size:11px;color:var(--muted)">Threshold Ο„</div>
<input type="range" id="threshSlider" min="0.05" max="0.95" step="0.01" value="0.5"
style="width:200px;accent-color:var(--blue)" oninput="updateThreshold(this.value)">
<div class="thresh-val" id="threshVal">0.50</div>
<div style="font-family:'DM Mono',monospace;font-size:11px;color:var(--muted)">predict positive if P β‰₯ Ο„</div>
</div>
<div class="thresh-mbar" id="threshMbar"></div>
<div class="plots-2" style="margin-bottom:0">
<div class="pc" style="margin:0">
<div class="pc-head"><div class="pc-title">Confusion Matrix @ Ο„</div></div>
<div style="display:flex;align-items:center;justify-content:center;height:200px">
<canvas id="cCMThresh"></canvas>
</div>
</div>
<div class="pc" style="margin:0">
<div class="pc-head"><div class="pc-title">Metrics vs Threshold</div></div>
<div class="cw"><canvas id="cThreshMetrics"></canvas></div>
</div>
</div>
</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:'moons', dsType:'synthetic',
modelType:'l2', C:1.0, l1Ratio:0.5, testSize:.2,
result:null, regPathData:null, regPathMode:'coefs',
classNames:[], featureNames:[], isBinary:true, nClasses:2,
};
const SYN = new Set(['moons','circles','blobs']);
let CH = {};
function dc(id){ if(CH[id]){CH[id].destroy();delete CH[id];} }
// Class colour palette β€” consistent across all plots
const CLS_RGB = [
[29,158,117], // teal
[55,138,221], // blue
[224,122,48], // orange
[192,57,43], // red
[142,68,173], // purple
];
function clsRgba(i, a){ const [r,g,b]=CLS_RGB[i%CLS_RGB.length]; return `rgba(${r},${g},${b},${a})`; }
const REG_COLORS = CLS_RGB.map(([r,g,b])=>`rgba(${r},${g},${b},0.85)`);
const legOpts = { labels:{font:{family:"'DM Mono',monospace",size:11},padding:12} };
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)'}},
});
// ═══════════════════════════════════════════════════════════════
// SIGMOID VISUALIZER (theory section β€” static init)
// ═══════════════════════════════════════════════════════════════
function sigmoid(z){ return 1/(1+Math.exp(-z)); }
function initSigmoid(){
const zVals = Array.from({length:121},(_,i)=>-6+i*0.1);
const sigVals = zVals.map(sigmoid);
CH.sig = new Chart(document.getElementById('cSig'),{
type:'line',
data:{
labels: zVals.map(v=>v.toFixed(1)),
datasets:[
{label:'Οƒ(z)',data:sigVals,borderColor:'rgba(55,138,221,1)',borderWidth:2.5,
pointRadius:0,fill:true,backgroundColor:'rgba(55,138,221,0.08)',tension:0.4},
{label:'Current z',data:[],borderColor:'rgba(224,122,48,1)',borderWidth:0,
pointRadius:8,pointBackgroundColor:'rgba(224,122,48,1)',fill:false},
]
},
options:{
responsive:true,maintainAspectRatio:false,
plugins:{legend:{display:false},
annotation:{annotations:{
line1:{type:'line',xMin:'0.0',xMax:'0.0',borderColor:'rgba(28,43,58,0.2)',borderWidth:1,borderDash:[4,4]},
}}},
scales:{
x:{title:{display:true,text:'z',font:{size:10}},grid:{color:'rgba(28,43,58,0.04)'},
ticks:{maxTicksLimit:7,font:{size:9}}},
y:{title:{display:true,text:'Οƒ(z)',font:{size:10}},grid:{color:'rgba(28,43,58,0.04)'},
min:0,max:1,ticks:{stepSize:.25,font:{size:9}}},
}
}
});
updateSigmoid(0);
}
function updateSigmoid(z){
z = parseFloat(z);
document.getElementById('sigZLbl').textContent = z.toFixed(2);
const sv = sigmoid(z);
document.getElementById('sigOut').textContent = `Οƒ(z) = ${sv.toFixed(4)}`;
const pct = (sv*100).toFixed(1);
let interp;
if(sv > 0.8) interp = `Strong positive signal β€” ${pct}% probability of positive class`;
else if(sv > 0.6) interp = `Moderate positive β€” ${pct}% probability`;
else if(sv > 0.4) interp = `Near boundary β€” ${pct}% probability`;
else if(sv > 0.2) interp = `Moderate negative β€” ${pct}% probability`;
else interp = `Strong negative signal β€” ${pct}% probability of positive class`;
document.getElementById('sigInterp').textContent = interp;
if(!CH.sig) return;
// highlight the current z point on the curve
const idx = Math.round((z+6)/0.1);
CH.sig.data.datasets[1].data = [{x: z.toFixed(1), y: sv}];
CH.sig.update('none');
}
// ═══════════════════════════════════════════════════════════════
// 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');
if(S.dsType==='synthetic'){
p.innerHTML=buildSynParams(S.dataset);
} else {
p.innerHTML=`<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>`;
fetch(`/api/classification-dataset-info/${S.dataset}`)
.then(r=>r.json()).then(d=>{S.featureNames=d.features||[];S.classNames=d.classes||[];});
}
}
function buildSynParams(ds){
let extra = '';
if(ds==='blobs') extra = `
<div class="pg"><label>N clusters <span id="cenLbl">3</span></label>
<input type="range" id="cenSlider" min="2" max="5" step="1" value="3"
oninput="document.getElementById('cenLbl').textContent=this.value"></div>
<div class="pg"><label>Cluster std <span id="cstdLbl">1.0</span></label>
<input type="range" id="cstdSlider" min="0.3" max="3.0" step="0.1" value="1.0"
oninput="document.getElementById('cstdLbl').textContent=parseFloat(this.value).toFixed(1)"></div>`;
if(ds==='circles') extra = `
<div class="pg"><label>Inner/Outer ratio <span id="factorLbl">0.50</span></label>
<input type="range" id="factorSlider" min="0.1" max="0.8" step="0.05" value="0.5"
oninput="document.getElementById('factorLbl').textContent=parseFloat(this.value).toFixed(2)"></div>`;
return `<div class="params-panel"><div class="pg-grid">
<div class="pg"><label>N samples <span id="nLbl">300</span></label>
<input type="range" id="nSlider" min="80" max="800" step="20" value="300"
oninput="document.getElementById('nLbl').textContent=this.value"></div>
<div class="pg"><label>Noise Οƒ <span id="noiseLbl">0.20</span></label>
<input type="range" id="noiseSlider" min="0.01" max="0.5" step="0.01" value="0.20"
oninput="document.getElementById('noiseLbl').textContent=parseFloat(this.value).toFixed(2)"></div>
${extra}
</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('cRow').style.display = mt==='none'?'none':'flex';
document.getElementById('l1RatioRow').style.display = mt==='elasticnet'?'flex':'none';
}
function updateC(v){ S.C=Math.pow(10,parseFloat(v)); document.getElementById('cLbl').textContent=S.C.toFixed(S.C<0.1?4:S.C<10?2:1); }
function updateL1Ratio(v){ S.l1Ratio=parseFloat(v); document.getElementById('l1RatioLbl').textContent=v; }
// ═══════════════════════════════════════════════════════════════
// 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, C:S.C, l1_ratio:S.l1Ratio,
};
if(S.dsType==='synthetic'){
body.synthetic_config={
dataset_type: S.dataset,
n_samples: parseInt(document.getElementById('nSlider')?.value||300),
noise: parseFloat(document.getElementById('noiseSlider')?.value||0.2),
n_centers: parseInt(document.getElementById('cenSlider')?.value||3),
factor: parseFloat(document.getElementById('factorSlider')?.value||0.5),
cluster_std: parseFloat(document.getElementById('cstdSlider')?.value||1.0),
};
} else {
body.real_config={dataset_name:S.dataset};
}
try{
const res=await fetch('/api/logistic-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.classNames=d.class_names||[];
S.featureNames=d.feature_names||[];
S.isBinary=d.is_binary;
S.nClasses=d.n_classes;
S.regPathData=d.reg_path;
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);
renderFitPanel(d);
renderConfusionMatrix(d.confusion_matrix, d.class_names, 'cCM', 'bCM');
renderROC(d.roc);
renderPR(d.pr);
renderProbDist(d.prob_dist);
renderCalibration(d.calibration);
renderPermImportance(d.perm_importance);
renderCoefBar(d.coefs);
renderLC(d.learning_curve);
showRegPath(S.regPathMode);
renderThresholdPanel(d);
renderDiag(d);
document.getElementById('results').scrollIntoView({behavior:'smooth',block:'start'});
}
// ═══════════════════════════════════════════════════════════════
// METRICS BAR
// ═══════════════════════════════════════════════════════════════
function renderMetrics(m){
const pct = v => (v*100).toFixed(1)+'%';
const ac_tr = m.accuracy_train>0.85?'good':m.accuracy_train>0.65?'':'warn';
const ac_te = m.accuracy_test >0.85?'good':m.accuracy_test >0.65?'':'warn';
const f1c = m.f1_test>0.8?'good':m.f1_test>0.6?'':'warn';
const auc_tr = m.roc_auc_train>0.9?'good':m.roc_auc_train>0.7?'':'warn';
const auc_te = m.roc_auc_test >0.9?'good':m.roc_auc_test >0.7?'':'warn';
const sub = S.nClasses>2 ? 'macro' : '';
const aucSub = S.nClasses>2 ? 'OvR macro' : '';
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-5">
<div class="mc ${ac_tr}"><div class="lbl">Accuracy</div><div class="val">${pct(m.accuracy_train)}</div></div>
<div class="mc"><div class="lbl">Precision</div><div class="val">${m.precision_train.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc"><div class="lbl">Recall</div><div class="val">${m.recall_train.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc"><div class="lbl">F1</div><div class="val">${m.f1_train.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc ${auc_tr}"><div class="lbl">ROC-AUC</div><div class="val">${m.roc_auc_train.toFixed(3)}</div><div class="sub">${aucSub}</div></div>
</div>
</div>
<div class="metrics-group">
<div class="mg-label" style="color:var(--blue-dark)">Testing</div>
<div class="mrow-5">
<div class="mc ${ac_te}"><div class="lbl">Accuracy</div><div class="val">${pct(m.accuracy_test)}</div></div>
<div class="mc"><div class="lbl">Precision</div><div class="val">${m.precision_test.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc"><div class="lbl">Recall</div><div class="val">${m.recall_test.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc ${f1c}"><div class="lbl">F1</div><div class="val">${m.f1_test.toFixed(3)}</div><div class="sub">${sub}</div></div>
<div class="mc ${auc_te}"><div class="lbl">ROC-AUC</div><div class="val">${m.roc_auc_test.toFixed(3)}</div><div class="sub">${aucSub}</div></div>
</div>
</div>
</div>
<div class="mrow-2">
<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>`;
}
// ═══════════════════════════════════════════════════════════════
// FIT PANEL: decision boundary (2D) vs coef chart (high-D)
// ═══════════════════════════════════════════════════════════════
function renderFitPanel(d){
const isSyn = d.is_synthetic;
document.getElementById('dbWrap').style.display = isSyn ? '' : 'none';
document.getElementById('coefWrap').style.display = isSyn ? 'none' : '';
document.getElementById('fitTitle').textContent = isSyn ? 'Decision Boundary' : 'Top Features (Coefficient)';
document.getElementById('fitNote').textContent = isSyn
? 'Shaded regions = predicted class (deeper = higher confidence). Circles = train, rings = test.'
: 'Coefficient values for top features (OvR for multi-class). Positive = increases log-odds of that class.';
if(isSyn){
renderDecisionBoundary(d.decision_boundary);
} else {
renderCoefTopChart(d.coefs);
}
}
// ─── Decision Boundary (custom canvas) ────────────────────────
function renderDecisionBoundary(db){
const canvas = document.getElementById('cDB');
const wrap = document.getElementById('dbWrap');
const W = wrap.offsetWidth || 480;
const H = wrap.offsetHeight || 300;
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,W,H);
const mg = {top:14, right:14, bottom:36, left:40};
const pw = W - mg.left - mg.right;
const ph = H - mg.top - mg.bottom;
const x1Min = db.xx1[0], x1Max = db.xx1[db.xx1.length-1];
const x2Min = db.xx2[0], x2Max = db.xx2[db.xx2.length-1];
const nx = db.xx1.length, ny = db.xx2.length;
const cw = pw/nx, ch = ph/ny;
const toX = v => mg.left + (v-x1Min)/(x1Max-x1Min)*pw;
const toY = v => mg.top + ph - (v-x2Min)/(x2Max-x2Min)*ph;
// draw mesh
for(let j=0;j<ny;j++){
for(let i=0;i<nx;i++){
const cls = db.zz[j][i];
const prob = db.proba[j][i];
const [r,g,b] = CLS_RGB[cls%CLS_RGB.length];
ctx.fillStyle = `rgba(${r},${g},${b},${(0.08+prob*0.28).toFixed(2)})`;
ctx.fillRect(mg.left+i*cw, mg.top+(ny-1-j)*ch, cw+1, ch+1);
}
}
// axes
ctx.strokeStyle='rgba(28,43,58,0.15)'; ctx.lineWidth=1;
ctx.strokeRect(mg.left, mg.top, pw, ph);
// axis ticks
ctx.fillStyle='#8298AE'; ctx.font="9px 'DM Mono',monospace"; ctx.textAlign='center';
[0,.25,.5,.75,1].forEach(t=>{
const xv = x1Min + t*(x1Max-x1Min);
const cx = toX(xv);
ctx.fillText(xv.toFixed(1), cx, H-8);
});
ctx.textAlign='right';
[0,.25,.5,.75,1].forEach(t=>{
const yv = x2Min + t*(x2Max-x2Min);
ctx.fillText(yv.toFixed(1), mg.left-4, toY(yv)+3);
});
// axis labels
ctx.textAlign='center'; ctx.fillStyle='#4A5F74'; ctx.font="11px 'DM Mono',monospace";
ctx.fillText('x₁', mg.left+pw/2, H-1);
ctx.save(); ctx.translate(10, mg.top+ph/2); ctx.rotate(-Math.PI/2);
ctx.fillText('xβ‚‚',0,0); ctx.restore();
// data points
const drawPts=(x1s,x2s,labels,isTrain)=>{
x1s.forEach((x1,i)=>{
const [r,g,b] = CLS_RGB[labels[i]%CLS_RGB.length];
ctx.beginPath();
ctx.arc(toX(x1), toY(x2s[i]), isTrain?4.5:3.5, 0, Math.PI*2);
ctx.fillStyle=`rgba(${r},${g},${b},0.88)`;
ctx.fill();
ctx.strokeStyle=isTrain?'rgba(28,43,58,0.6)':'rgba(255,255,255,0.9)';
ctx.lineWidth=isTrain?0.8:1.5; ctx.stroke();
});
};
drawPts(db.x1_train, db.x2_train, db.labels_train, true);
drawPts(db.x1_test, db.x2_test, db.labels_test, false);
// legend
const lx = mg.left+pw-4, ly = mg.top+8;
db.class_names.forEach((cn,i)=>{
const [r,g,b]=CLS_RGB[i%CLS_RGB.length];
ctx.fillStyle=`rgba(${r},${g},${b},0.85)`;
ctx.fillRect(lx-62, ly+i*16, 10, 10);
ctx.fillStyle='#4A5F74'; ctx.font="9px 'DM Mono',monospace"; ctx.textAlign='left';
ctx.fillText(cn, lx-48, ly+i*16+9);
});
}
// ─── Coefficient top-features chart (high-D, for the fit panel slot) ─
function renderCoefTopChart(coefs){
dc('coefTop');
const canvas = document.getElementById('coefWrap').querySelector('canvas') ||
(() => { const c=document.createElement('canvas'); document.getElementById('coefWrap').appendChild(c); return c; })();
canvas.id='coefTopCanvas';
if(coefs.type==='binary'){
const vals = coefs.values.slice(0,12);
const feats = coefs.features.slice(0,12);
CH.coefTop = new Chart(canvas,{
type:'bar',
data:{labels:feats, datasets:[{
label:`log-odds coef (${coefs.class_names[1]})`,
data:vals,
backgroundColor:vals.map(v=>v>=0?clsRgba(0,.7):clsRgba(3,.7)),
borderRadius:4,
}]},
options:{
indexAxis:'y',responsive:true,maintainAspectRatio:false,
plugins:{legend:{display:false}},
scales:{
x:{title:{display:true,text:'Coefficient',font:{size:10}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{ticks:{font:{family:"'DM Mono',monospace",size:9}}},
}
}
});
} else {
const feats = coefs.features.slice(0,10);
const datasets = coefs.class_names.map((cn,ci)=>({
label:cn, data:coefs.values_per_class[ci].slice(0,10),
backgroundColor:clsRgba(ci,.7), borderRadius:3,
}));
CH.coefTop = new Chart(canvas,{
type:'bar',
data:{labels:feats, datasets},
options:{
indexAxis:'y',responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:{
x:{title:{display:true,text:'Coefficient',font:{size:10}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{ticks:{font:{family:"'DM Mono',monospace",size:9}}},
}
}
});
}
}
// ─── Confusion Matrix (custom canvas) ─────────────────────────
function renderConfusionMatrix(cm, classNames, canvasId, badgeId){
const canvas = document.getElementById(canvasId);
const n = classNames.length;
const cell = Math.min(90, Math.floor(220/n));
const mL=70, mT=60;
const W = mL + cell*n + 20;
const H = mT + cell*n + 30;
canvas.width=W; canvas.height=H;
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,W,H);
// row sums for normalization
const rowSum = cm.map(row=>row.reduce((s,v)=>s+v,0));
const total = rowSum.reduce((s,v)=>s+v,0);
let correct = 0;
cm.forEach((row,i)=>{ correct+=row[i]; });
const acc = total>0 ? correct/total : 0;
if(badgeId){
const b = document.getElementById(badgeId);
b.textContent = `${(acc*100).toFixed(1)}% acc`;
b.className = 'badge '+(acc>0.85?'b-ok':acc>0.65?'b-info':'b-warn');
}
ctx.font="10px 'DM Mono',monospace";
// "Predicted" label on top
ctx.fillStyle='#4A5F74'; ctx.textAlign='center';
ctx.font="bold 10px 'DM Mono',monospace";
ctx.fillText('Predicted', mL + (cell*n)/2, 14);
// "True" label on left (rotated)
ctx.save(); ctx.translate(11, mT+(cell*n)/2); ctx.rotate(-Math.PI/2);
ctx.fillText('True', 0, 0); ctx.restore();
// column headers (predicted)
ctx.font="9px 'DM Mono',monospace"; ctx.textAlign='center';
classNames.forEach((cn,j)=>{
ctx.fillStyle='#4A5F74';
ctx.fillText(cn, mL+j*cell+cell/2, mT-6);
});
// row labels (true)
ctx.textAlign='right';
classNames.forEach((cn,i)=>{
ctx.fillStyle='#4A5F74'; ctx.font="9px 'DM Mono',monospace";
ctx.fillText(cn, mL-6, mT+i*cell+cell/2+4);
});
// cells
cm.forEach((row,i)=>{
row.forEach((val,j)=>{
const norm = rowSum[i]>0 ? val/rowSum[i] : 0;
const diag = i===j;
const [r,g,b] = diag ? [29,158,117] : [192,57,43];
ctx.fillStyle = `rgba(${r},${g},${b},${(norm*0.82+0.05).toFixed(2)})`;
ctx.fillRect(mL+j*cell, mT+i*cell, cell, cell);
// border
ctx.strokeStyle='rgba(255,255,255,0.6)'; ctx.lineWidth=1;
ctx.strokeRect(mL+j*cell, mT+i*cell, cell, cell);
// count
const txtColor = norm>0.45 ? '#fff' : '#1C2B3A';
ctx.fillStyle=txtColor; ctx.textAlign='center'; ctx.font=`bold ${Math.min(18,cell*0.32)}px 'DM Mono',monospace`;
ctx.fillText(val, mL+j*cell+cell/2, mT+i*cell+cell/2-4);
// percentage
ctx.font=`${Math.min(11,cell*0.19)}px 'DM Mono',monospace`;
ctx.fillStyle = norm>0.45?'rgba(255,255,255,0.8)':'#8298AE';
ctx.fillText(`${(norm*100).toFixed(0)}%`, mL+j*cell+cell/2, mT+i*cell+cell/2+12);
});
});
}
// ═══════════════════════════════════════════════════════════════
// ROC CURVE
// ═══════════════════════════════════════════════════════════════
function renderROC(roc){
dc('roc');
const datasets = roc.curves.map((c,i)=>({
label:`${c.cls} (AUC=${c.auc.toFixed(3)})`,
data: c.fpr.map((f,j)=>({x:f,y:c.tpr[j]})),
borderColor: clsRgba(i,1), backgroundColor:'transparent',
borderWidth:2.2, pointRadius:0, fill:false, tension:0,
}));
datasets.push({
label:'Random (AUC=0.5)',
data:[{x:0,y:0},{x:1,y:1}],
borderColor:'rgba(28,43,58,0.25)', borderWidth:1.5,
borderDash:[5,4], pointRadius:0, fill:false,
});
CH.roc = new Chart(document.getElementById('cROC'),{
type:'scatter',
data:{datasets},
options:{responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:axOpts('False Positive Rate','True Positive Rate')}
});
}
// ═══════════════════════════════════════════════════════════════
// PRECISION-RECALL CURVE
// ═══════════════════════════════════════════════════════════════
function renderPR(pr){
dc('pr');
const datasets = pr.curves.map((c,i)=>({
label:`${c.cls} (AP=${c.ap.toFixed(3)})`,
data: c.recall.map((r,j)=>({x:r,y:c.precision[j]})),
borderColor: clsRgba(i,1), backgroundColor:'transparent',
borderWidth:2.2, pointRadius:0, fill:false, tension:0,
}));
CH.pr = new Chart(document.getElementById('cPR'),{
type:'scatter',
data:{datasets},
options:{responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:axOpts('Recall','Precision')}
});
}
// ═══════════════════════════════════════════════════════════════
// PROBABILITY DISTRIBUTION
// ═══════════════════════════════════════════════════════════════
function renderProbDist(pd){
dc('probDist');
document.getElementById('probDistTitle').textContent =
`Predicted Probability Distribution`;
document.getElementById('probDistNote').textContent =
pd.x_label + ' β€” well-separated histograms indicate confident, accurate predictions.';
const datasets = pd.histograms.map((h,i)=>({
label: h.cls,
data: h.counts,
backgroundColor: clsRgba(i, 0.55),
borderColor: clsRgba(i, 0.9),
borderWidth: 1, borderRadius: 3,
}));
CH.probDist = new Chart(document.getElementById('cProbDist'),{
type:'bar',
data:{labels:pd.bin_centers.map(v=>v.toFixed(2)), datasets},
options:{
responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:{
x:{title:{display:true,text:pd.x_label,font:{size:11}},
grid:{display:false},ticks:{maxTicksLimit:10,font:{family:"'DM Mono',monospace",size:9}}},
y:{title:{display:true,text:'Count',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'}},
}
}
});
}
// ═══════════════════════════════════════════════════════════════
// CALIBRATION CURVE
// ═══════════════════════════════════════════════════════════════
function renderCalibration(calib){
dc('calib');
const datasets = [];
// perfect calibration reference
datasets.push({
label:'Perfect calibration',
data:[{x:0,y:0},{x:1,y:1}],
borderColor:'rgba(28,43,58,0.25)',borderWidth:1.5,
borderDash:[5,4],pointRadius:0,fill:false,type:'line',
});
calib.curves.forEach((c,i)=>{
datasets.push({
label:`${c.cls}`,
data: c.mean_pred.map((mp,j)=>({x:mp,y:c.frac_pos[j]})),
borderColor: clsRgba(i,1),
backgroundColor: clsRgba(i,0.7),
borderWidth:2, pointRadius:6, fill:false, tension:0.2,
});
});
CH.calib = new Chart(document.getElementById('cCalib'),{
type:'scatter',
data:{datasets},
options:{responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:{
x:{...axOpts('Mean Predicted Probability','Fraction of Positives').x, min:0,max:1},
y:{...axOpts('Mean Predicted Probability','Fraction of Positives').y, min:0,max:1},
}}
});
}
// ═══════════════════════════════════════════════════════════════
// 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 top = imp.slice(0,15);
const maxV = Math.max(...top.map(i=>Math.max(i.mean,0)))||1;
document.getElementById('permBars').innerHTML = top.map(it=>`
<div class="perm-row">
<div class="perm-feat" title="${it.feature}">${it.feature}</div>
<div class="perm-bar-w"><div class="perm-bar-f" style="width:${Math.max(0,it.mean/maxV*100).toFixed(1)}%;background:${it.mean<0?'var(--orange)':'var(--blue)'}"></div></div>
<div class="perm-val">${it.mean.toFixed(3)}</div>
</div>`).join('');
}
// ═══════════════════════════════════════════════════════════════
// COEFFICIENT BAR CHART (Feature Importance section)
// ═══════════════════════════════════════════════════════════════
function renderCoefBar(coefs){
dc('coefBar');
if(coefs.type==='binary'){
const vals = coefs.values;
const feats = coefs.features;
CH.coefBar = new Chart(document.getElementById('cCoefBar'),{
type:'bar',
data:{labels:feats, datasets:[{
label:`log-odds (${coefs.class_names[1]})`,
data: vals,
backgroundColor: vals.map(v=>v>=0?clsRgba(0,.72):clsRgba(3,.72)),
borderRadius:4,
}]},
options:{
indexAxis:'y',responsive:true,maintainAspectRatio:false,
plugins:{legend:{display:false}},
scales:{
x:{title:{display:true,text:'Coefficient (log-odds)',font:{size:10}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{ticks:{font:{family:"'DM Mono',monospace",size:9}}},
}
}
});
document.getElementById('coefNote').textContent =
`Positive = increases log-odds of "${coefs.class_names[1]}". All features z-scored before fitting.`;
} else {
const datasets = coefs.class_names.map((cn,ci)=>({
label:cn, data:coefs.values_per_class[ci],
backgroundColor:clsRgba(ci,.65), borderRadius:3,
}));
CH.coefBar = new Chart(document.getElementById('cCoefBar'),{
type:'bar',
data:{labels:coefs.features, datasets},
options:{
indexAxis:'y',responsive:true,maintainAspectRatio:false,
plugins:{legend:legOpts},
scales:{
x:{title:{display:true,text:'Coefficient',font:{size:10}},grid:{color:'rgba(28,43,58,0.05)'}},
y:{ticks:{font:{family:"'DM Mono',monospace",size:9}}},
}
}
});
document.getElementById('coefNote').textContent =
`OvR coefficients per class. Positive = pushes toward that class. All features z-scored.`;
}
}
// ═══════════════════════════════════════════════════════════════
// LEARNING CURVE
// ═══════════════════════════════════════════════════════════════
function renderLC(lc){
dc('lc');
CH.lc = new Chart(document.getElementById('cLC'),{
type:'line',
data:{labels:lc.sizes, datasets:[
{label:'Train Acc', data:lc.train, borderColor:clsRgba(0,1), backgroundColor:clsRgba(0,.1), borderWidth:2.2,fill:true,tension:.4,pointRadius:4},
{label:'Val Acc', data:lc.val, borderColor:clsRgba(1,1), backgroundColor:clsRgba(1,.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:'Accuracy',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},min:0,max:1.05},
}}
});
}
// ═══════════════════════════════════════════════════════════════
// REGULARIZATION PATH
// ═══════════════════════════════════════════════════════════════
function showRegPath(mode){
S.regPathMode = mode;
['Coefs','Acc'].forEach(m=>{
const b = document.getElementById(`rp${m}Btn`);
const active = mode===(m==='Coefs'?'coefs':'acc');
b.style.background = active?'var(--blue)':'';
b.style.color = active?'#fff':'';
b.style.borderColor = active?'var(--blue)':'';
});
if(!S.regPathData) return;
dc('regPath');
const rp = S.regPathData;
if(mode==='acc'){
CH.regPath = new Chart(document.getElementById('cRegPath'),{
type:'line',
data:{labels:rp.log_C.map(v=>v.toFixed(1)), datasets:[
{label:'Train Acc',data:rp.train_accs,borderColor:clsRgba(0,1),backgroundColor:clsRgba(0,.08),borderWidth:2,fill:true,tension:.3,pointRadius:0},
{label:'Test Acc', data:rp.test_accs, borderColor:clsRgba(1,1),backgroundColor:clsRgba(1,.08),borderWidth:2,fill:true,tension:.3,pointRadius:0},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},
scales:{
x:{title:{display:true,text:'log₁₀(C) ← more reg | less reg β†’',font:{size:10}},grid:{color:'rgba(28,43,58,0.05)'},ticks:{maxTicksLimit:8}},
y:{title:{display:true,text:'Accuracy',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},min:0,max:1.05},
}}
});
} else {
const fn = rp.feature_names.slice(0,10);
const datasets = fn.map((name,j)=>({
label:name,
data:rp.coef_paths.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.log_C.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₁₀(C) ← more reg | less reg β†’',font:{size:10}},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)'}},
}}
});
}
}
// ═══════════════════════════════════════════════════════════════
// THRESHOLD ANALYSIS (binary only)
// ═══════════════════════════════════════════════════════════════
let TH = { probaPos:[], yTrue:[], classNames:[] };
function renderThresholdPanel(d){
const sec = document.getElementById('threshSection');
if(!d.is_binary){ sec.style.display='none'; return; }
sec.style.display='';
TH.probaPos = d.y_proba_test.map(p=>p[1]);
TH.yTrue = d.y_true_test;
TH.classNames= d.class_names;
// precompute metrics-vs-threshold curve
const thresholds = Array.from({length:90},(_,i)=>(i+5)/100);
const accs=[], precs=[], recs=[], f1s=[];
thresholds.forEach(t=>{
const {acc,prec,rec,f1}=computeAtThreshold(t);
accs.push(acc); precs.push(prec); recs.push(rec); f1s.push(f1);
});
dc('threshMetrics');
CH.threshMetrics = new Chart(document.getElementById('cThreshMetrics'),{
type:'line',
data:{labels:thresholds.map(v=>v.toFixed(2)), datasets:[
{label:'Accuracy', data:accs, borderColor:clsRgba(0,1),borderWidth:2,pointRadius:0,fill:false,tension:.3},
{label:'Precision', data:precs, borderColor:clsRgba(1,1),borderWidth:2,pointRadius:0,fill:false,tension:.3},
{label:'Recall', data:recs, borderColor:clsRgba(2,1),borderWidth:2,pointRadius:0,fill:false,tension:.3},
{label:'F1', data:f1s, borderColor:clsRgba(3,1),borderWidth:2.5,pointRadius:0,fill:false,tension:.3},
]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:legOpts},
scales:{
x:{title:{display:true,text:'Threshold Ο„',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},ticks:{maxTicksLimit:10}},
y:{title:{display:true,text:'Score',font:{size:11}},grid:{color:'rgba(28,43,58,0.05)'},min:0,max:1.05},
}}
});
document.getElementById('threshSlider').value=0.5;
updateThreshold(0.5);
}
function computeAtThreshold(t){
let tp=0,fp=0,tn=0,fn_=0;
TH.yTrue.forEach((yt,i)=>{
const pred = TH.probaPos[i]>=t ? 1 : 0;
if(yt===1&&pred===1) tp++;
else if(yt===0&&pred===1) fp++;
else if(yt===0&&pred===0) tn++;
else fn_++;
});
const n = TH.yTrue.length||1;
const acc = (tp+tn)/n;
const prec = tp+fp>0 ? tp/(tp+fp) : 0;
const rec = tp+fn_>0 ? tp/(tp+fn_) : 0;
const f1 = prec+rec>0 ? 2*prec*rec/(prec+rec) : 0;
return {tp,fp,tn,fn:fn_,acc,prec,rec,f1};
}
function updateThreshold(t){
t=parseFloat(t);
document.getElementById('threshVal').textContent=t.toFixed(2);
const {tp,fp,tn,fn:fn_,acc,prec,rec,f1}=computeAtThreshold(t);
document.getElementById('threshMbar').innerHTML=`
<div class="mc"><div class="lbl">Accuracy</div><div class="val">${(acc*100).toFixed(1)}%</div></div>
<div class="mc"><div class="lbl">Precision</div><div class="val">${prec.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">Recall</div><div class="val">${rec.toFixed(3)}</div></div>
<div class="mc"><div class="lbl">F1</div><div class="val">${f1.toFixed(3)}</div></div>`;
const cm2=[[tn,fp],[fn_,tp]];
renderConfusionMatrix(cm2, TH.classNames, 'cCMThresh', null);
}
// ═══════════════════════════════════════════════════════════════
// DIAGNOSTIC SUMMARY
// ═══════════════════════════════════════════════════════════════
function renderDiag(d){
const m=d.metrics;
const overfitting = (m.accuracy_train - m.accuracy_test) > 0.10;
const topImp = d.perm_importance.length>0 ? d.perm_importance[0] : null;
const calibOk = d.calibration.curves.length>0;
const items=[
{ok:m.accuracy_test>0.7, title:'Overall Accuracy',
text:`Test accuracy: ${(m.accuracy_test*100).toFixed(1)}%. ${m.accuracy_test>0.85?'Excellent classification.':m.accuracy_test>0.7?'Good classification.':m.accuracy_test>0.5?'Moderate β€” consider feature engineering or non-linear model.':'Poor β€” data may not be linearly separable.'}`},
{ok:m.f1_test>0.7, title:'F1 Score (macro)',
text:`F1=${m.f1_test.toFixed(3)}, Precision=${m.precision_test.toFixed(3)}, Recall=${m.recall_test.toFixed(3)}. ${m.precision_test>m.recall_test?'Precision >> Recall: model is conservative (misses positives).':'Recall >> Precision: model is aggressive (many false alarms).'}`},
{ok:m.roc_auc_test>0.8, title:'ROC-AUC',
text:`AUC=${m.roc_auc_test.toFixed(3)}. ${m.roc_auc_test>0.9?'Excellent discriminative ability.':m.roc_auc_test>0.8?'Good.':m.roc_auc_test>0.7?'Acceptable. Consider feature engineering.':'Near random β€” logistic regression may be insufficient for this task.'}`},
{ok:m.log_loss_test<0.5, title:'Log Loss (calibration)',
text:`Log Loss=${m.log_loss_test.toFixed(3)}. ${m.log_loss_test<0.3?'Model is well-calibrated with confident correct predictions.':m.log_loss_test<0.5?'Reasonable probability estimates.':'High uncertainty in predictions. Check calibration curve.'}`},
{ok:!overfitting, title:'Overfitting',
text:overfitting?`Train acc=${(m.accuracy_train*100).toFixed(1)}% >> Test acc=${(m.accuracy_test*100).toFixed(1)}%. Increase regularization (↓C) or get more data.`:`Train accβ‰ˆTest acc (${(m.accuracy_train*100).toFixed(1)}% vs ${(m.accuracy_test*100).toFixed(1)}%). Good generalization. βœ“`},
{ok:true, title:'Feature Insights',
text:topImp?`Top feature: "${topImp.feature}" (importance drop=${topImp.mean.toFixed(3)}). ${d.is_binary?'Decision threshold can be adjusted to trade precision vs recall.':'Multiclass: check per-class ROC AUC for imbalanced performance.'}`:
'Feature importance not available.'},
];
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();
initSigmoid();
</script>
</body>
</html>