| <!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> |
|
|
| |
| <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"> |
|
|
| |
| <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 & Multi-class</h3> |
| <div class="fl lg">Ο(z) = 1 / (1 + eβ»αΆ») z = Ξ²β + Ξ²βxβ + β¦ + Ξ²βxβ</div> |
| <div class="fl">P(y=1|x) = Ο(z) Decision boundary: z = 0 βΊ 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)·Σβⱼ² | L1: Loss + (1/C)·Σ|βⱼ| | Multi-class: One-vs-Rest (OvR) by default</div> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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/Ξ» <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> |
|
|
| |
| <div id="results" style="display:none"> |
|
|
| |
| <div class="sl">Performance Metrics</div> |
| <div id="metricsContainer"></div> |
|
|
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="sl">Diagnostic Summary</div> |
| <div class="diag-grid" id="diagGrid"></div> |
|
|
| </div> |
| </div> |
|
|
| <script> |
| |
| |
| |
| 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];} } |
| |
| |
| const CLS_RGB = [ |
| [29,158,117], |
| [55,138,221], |
| [224,122,48], |
| [192,57,43], |
| [142,68,173], |
| ]; |
| 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)'}}, |
| }); |
| |
| |
| |
| |
| 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; |
| |
| const idx = Math.round((z+6)/0.1); |
| CH.sig.data.datasets[1].data = [{x: z.toFixed(1), y: sv}]; |
| CH.sig.update('none'); |
| } |
| |
| |
| |
| |
| 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; } |
| |
| |
| |
| |
| 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';} |
| } |
| |
| |
| |
| |
| 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'}); |
| } |
| |
| |
| |
| |
| 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>`; |
| } |
| |
| |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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; |
| |
| |
| 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); |
| } |
| } |
| |
| |
| ctx.strokeStyle='rgba(28,43,58,0.15)'; ctx.lineWidth=1; |
| ctx.strokeRect(mg.left, mg.top, pw, ph); |
| |
| |
| 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); |
| }); |
| |
| |
| 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(); |
| |
| |
| 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); |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| 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}}}, |
| } |
| } |
| }); |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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"; |
| |
| |
| ctx.fillStyle='#4A5F74'; ctx.textAlign='center'; |
| ctx.font="bold 10px 'DM Mono',monospace"; |
| ctx.fillText('Predicted', mL + (cell*n)/2, 14); |
| |
| |
| ctx.save(); ctx.translate(11, mT+(cell*n)/2); ctx.rotate(-Math.PI/2); |
| ctx.fillText('True', 0, 0); ctx.restore(); |
| |
| |
| 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); |
| }); |
| |
| |
| 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); |
| }); |
| |
| |
| 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); |
| |
| |
| ctx.strokeStyle='rgba(255,255,255,0.6)'; ctx.lineWidth=1; |
| ctx.strokeRect(mL+j*cell, mT+i*cell, cell, cell); |
| |
| |
| 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); |
| |
| |
| 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); |
| }); |
| }); |
| } |
| |
| |
| |
| |
| 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')} |
| }); |
| } |
| |
| |
| |
| |
| 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')} |
| }); |
| } |
| |
| |
| |
| |
| 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)'}}, |
| } |
| } |
| }); |
| } |
| |
| |
| |
| |
| function renderCalibration(calib){ |
| dc('calib'); |
| const datasets = []; |
| |
| 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}, |
| }} |
| }); |
| } |
| |
| |
| |
| |
| 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(''); |
| } |
| |
| |
| |
| |
| 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.`; |
| } |
| } |
| |
| |
| |
| |
| 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}, |
| }} |
| }); |
| } |
| |
| |
| |
| |
| 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)'}}, |
| }} |
| }); |
| } |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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(''); |
| } |
| |
| |
| |
| |
| renderParams(); |
| initSigmoid(); |
| </script> |
| </body> |
| </html> |
|
|