Spaces:
Sleeping
Sleeping
zofiasmolenasana
Merge value+attribute labels, add header label, RAG eval improvements
d051588 unverified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Feature EDA – MLOps</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-0: #0b1120; --bg-1: #0f172a; --bg-2: #1e293b; --bg-3: #334155; | |
| --border: #334155; --border-hover: #475569; | |
| --text-0: #f1f5f9; --text-1: #e2e8f0; --text-2: #cbd5e1; --text-3: #94a3b8; --text-4: #64748b; | |
| --blue: #3b82f6; --blue-dim: #1e3a5f; | |
| --green: #34d399; --green-dim: #14532d; | |
| --red: #f87171; --red-dim: #450a0a; | |
| --amber: #f59e0b; --purple: #a855f7; --cyan: #06b6d4; | |
| --pink: #ec4899; --orange: #f97316; | |
| --sidebar-w: 220px; --topbar-h: 52px; | |
| --radius: 10px; --radius-sm: 6px; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;background:var(--bg-0);color:var(--text-1);min-height:100vh} | |
| .topbar{position:fixed;top:0;left:0;right:0;z-index:200;height:var(--topbar-h);background:var(--bg-2); | |
| border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 20px;gap:14px} | |
| .topbar .logo{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700;color:var(--text-0);letter-spacing:-.3px} | |
| .topbar .logo svg{width:22px;height:22px;color:var(--blue)} | |
| .topbar .spacer{flex:1} | |
| .topbar .back{color:var(--text-3);font-size:13px;text-decoration:none;padding:6px 12px;border-radius:var(--radius-sm);transition:all .15s} | |
| .topbar .back:hover{color:var(--text-1);background:var(--bg-3)} | |
| .main{margin-top:var(--topbar-h);padding:28px 32px;max-width:1400px;margin-left:auto;margin-right:auto} | |
| .page-title{font-size:22px;font-weight:700;color:var(--text-0);margin-bottom:4px} | |
| .page-subtitle{font-size:13px;color:var(--text-3);margin-bottom:24px} | |
| .stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:24px} | |
| .stat-card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px} | |
| .stat-card .label{font-size:11px;font-weight:600;color:var(--text-4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px} | |
| .stat-card .value{font-size:22px;font-weight:700;color:var(--text-0)} | |
| .stat-card .sub{font-size:11px;color:var(--text-3);margin-top:2px} | |
| .card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;margin-bottom:20px} | |
| .card-title{font-size:15px;font-weight:700;color:var(--text-0);margin-bottom:16px;display:flex;align-items:center;gap:8px} | |
| .card-title svg{width:16px;height:16px;color:var(--text-3)} | |
| .tabs{display:flex;gap:2px;margin-bottom:16px;background:var(--bg-1);border-radius:var(--radius-sm);padding:3px;overflow-x:auto} | |
| .tab{padding:7px 14px;border-radius:4px;font-size:12px;font-weight:600;color:var(--text-3);cursor:pointer; | |
| transition:all .15s;white-space:nowrap;border:none;background:none} | |
| .tab:hover{color:var(--text-1)} | |
| .tab.active{background:var(--bg-3);color:var(--text-0)} | |
| .chart-container{position:relative;height:340px;margin-bottom:8px} | |
| .chart-container canvas{width:100%;height:100%} | |
| .heatmap-grid{overflow-x:auto} | |
| table.heatmap{border-collapse:collapse;font-size:12px;width:100%} | |
| table.heatmap th{padding:6px 10px;font-weight:600;color:var(--text-0);background:var(--bg-1); | |
| position:sticky;top:0;text-align:left;white-space:nowrap;border:1px solid var(--bg-3)} | |
| table.heatmap td{padding:5px 10px;border:1px solid var(--bg-3);text-align:center;font-variant-numeric:tabular-nums} | |
| table.heatmap .feature-name{text-align:left;font-weight:500;color:var(--text-2);white-space:nowrap; | |
| position:sticky;left:0;background:var(--bg-2);z-index:1} | |
| .top-features-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px} | |
| .top-feature-card{background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px 16px} | |
| .top-feature-card .class-name{font-size:13px;font-weight:700;color:var(--text-0);margin-bottom:2px; | |
| display:flex;align-items:center;gap:8px} | |
| .top-feature-card .class-count{font-size:11px;color:var(--text-4);margin-bottom:10px} | |
| .feat-row{display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)} | |
| .feat-row:last-child{border-bottom:none} | |
| .feat-rank{font-size:10px;font-weight:700;color:var(--text-4);width:18px;text-align:right} | |
| .feat-name{font-size:12px;color:var(--text-2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .feat-bar-wrap{width:100px;height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden} | |
| .feat-bar{height:100%;border-radius:3px} | |
| .feat-val{font-size:11px;font-weight:600;color:var(--text-1);width:44px;text-align:right;font-variant-numeric:tabular-nums} | |
| .filter-row{display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap} | |
| .filter-row label{font-size:12px;color:var(--text-3);font-weight:600} | |
| .filter-row select{padding:5px 10px;border-radius:var(--radius-sm);border:1px solid var(--border); | |
| background:var(--bg-1);color:var(--text-1);font-size:12px} | |
| .loading{text-align:center;padding:60px;color:var(--text-3);font-size:14px} | |
| .loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--bg-3); | |
| border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .group-label{font-size:10px;font-weight:700;color:var(--text-4);text-transform:uppercase;letter-spacing:1px; | |
| padding:8px 0 4px;border-top:1px solid var(--bg-3);margin-top:4px} | |
| .legend-row{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px} | |
| .legend-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-3)} | |
| .legend-dot{width:10px;height:10px;border-radius:2px} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar"> | |
| <div class="logo"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/></svg> | |
| Excel Chunker | |
| </div> | |
| <div class="spacer"></div> | |
| <a class="back" href="/train">← Training</a> | |
| <a class="back" href="/">Labeler</a> | |
| </div> | |
| <div class="main"> | |
| <h1 class="page-title">Feature EDA</h1> | |
| <p class="page-subtitle">Exploratory analysis of handcrafted features by cell class — understand which features discriminate each class</p> | |
| <div id="loading" class="loading"> | |
| <div class="spinner"></div><br> | |
| Computing feature statistics across all labeled data… | |
| </div> | |
| <div id="content" style="display:none"> | |
| <!-- Summary stats --> | |
| <div class="stats-row" id="stats-row"></div> | |
| <!-- Top discriminative features per class --> | |
| <div class="card"> | |
| <div class="card-title"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg> | |
| Top Discriminative Features per Class | |
| </div> | |
| <p style="font-size:12px;color:var(--text-3);margin-bottom:14px"> | |
| Top 10 features whose class mean differs most from the global mean — higher abs diff = more distinctive for this class | |
| </p> | |
| <div class="top-features-grid" id="top-features"></div> | |
| </div> | |
| <!-- Radar / bar chart comparison --> | |
| <div class="card"> | |
| <div class="card-title"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg> | |
| Feature Comparison by Class | |
| </div> | |
| <div class="filter-row"> | |
| <label>Feature group:</label> | |
| <select id="group-select"> | |
| <option value="all">All features</option> | |
| <option value="position">Position (0-2)</option> | |
| <option value="content">Content stats (3-11)</option> | |
| <option value="formatting">Formatting (12-27)</option> | |
| <option value="formula">Formula (28-31)</option> | |
| <option value="content_type">Content type (32-43)</option> | |
| <option value="keywords">Keywords (44-54)</option> | |
| <option value="textual">Textual/Punctuation (56-70)</option> | |
| <option value="numerical">TUTA numerical (71-74)</option> | |
| <option value="col_row">Column/Row context (75-82)</option> | |
| <option value="cohesion">TableSense cohesion (83-88)</option> | |
| </select> | |
| <label>Mode:</label> | |
| <select id="mode-select"> | |
| <option value="mean">Mean activation</option> | |
| <option value="prevalence">Prevalence (% > 0)</option> | |
| <option value="diff">Diff from global mean</option> | |
| </select> | |
| </div> | |
| <div class="chart-container"><canvas id="comparison-chart"></canvas></div> | |
| </div> | |
| <!-- Class distribution bar --> | |
| <div class="card"> | |
| <div class="card-title"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10M12 20V4M6 20v-6"/></svg> | |
| Class Distribution | |
| </div> | |
| <div class="chart-container" style="height:260px"><canvas id="class-chart"></canvas></div> | |
| </div> | |
| <!-- Full heatmap table --> | |
| <div class="card"> | |
| <div class="card-title"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg> | |
| Full Feature Heatmap (Mean Activation) | |
| </div> | |
| <p style="font-size:12px;color:var(--text-3);margin-bottom:12px"> | |
| Color intensity = mean value for that class. Hover for exact values. | |
| </p> | |
| <div class="heatmap-grid" id="heatmap-container"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const CLASS_COLORS = { | |
| value: '#3b82f6', | |
| attribute: '#f59e0b', | |
| col_header: '#34d399', | |
| row_header: '#06b6d4', | |
| aggregation: '#a855f7', | |
| metadata: '#ec4899', | |
| comment: '#f97316', | |
| empty: '#64748b', | |
| junk: '#f87171', | |
| }; | |
| const FEATURE_GROUPS = { | |
| position: [0, 2], | |
| content: [3, 11], | |
| formatting: [12, 27], | |
| formula: [28, 31], | |
| content_type: [32, 43], | |
| keywords: [44, 54], | |
| merge_span: [55, 55], | |
| textual: [56, 70], | |
| numerical: [71, 74], | |
| col_row: [75, 82], | |
| cohesion: [83, 88], | |
| }; | |
| let DATA = null; | |
| let comparisonChart = null; | |
| async function loadData() { | |
| const resp = await fetch('/api/train/feature_eda'); | |
| DATA = await resp.json(); | |
| document.getElementById('loading').style.display = 'none'; | |
| document.getElementById('content').style.display = 'block'; | |
| renderAll(); | |
| } | |
| function renderAll() { | |
| renderStats(); | |
| renderTopFeatures(); | |
| renderComparisonChart(); | |
| renderClassChart(); | |
| renderHeatmap(); | |
| } | |
| function renderStats() { | |
| const el = document.getElementById('stats-row'); | |
| const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0); | |
| el.innerHTML = ` | |
| <div class="stat-card"><div class="label">Total Cells</div><div class="value">${DATA.total_cells.toLocaleString()}</div></div> | |
| <div class="stat-card"><div class="label">Features</div><div class="value">${DATA.num_features}</div></div> | |
| <div class="stat-card"><div class="label">Classes</div><div class="value">${classes.length}</div></div> | |
| <div class="stat-card"><div class="label">Largest Class</div><div class="value">${(() => { | |
| let max = 0, name = ''; | |
| for (const [k,v] of Object.entries(DATA.class_counts)) { if (v > max) { max = v; name = k; } } | |
| return name; | |
| })()}</div><div class="sub">${Math.max(...Object.values(DATA.class_counts)).toLocaleString()} cells</div></div> | |
| <div class="stat-card"><div class="label">Smallest Class</div><div class="value">${(() => { | |
| let min = Infinity, name = ''; | |
| for (const [k,v] of Object.entries(DATA.class_counts)) { if (v > 0 && v < min) { min = v; name = k; } } | |
| return name; | |
| })()}</div><div class="sub">${(() => { | |
| let min = Infinity; for (const v of Object.values(DATA.class_counts)) { if (v > 0 && v < min) min = v; } return min.toLocaleString(); | |
| })()} cells</div></div> | |
| `; | |
| } | |
| function renderTopFeatures() { | |
| const el = document.getElementById('top-features'); | |
| const classes = Object.keys(DATA.top_features_per_class); | |
| el.innerHTML = classes.map(cls => { | |
| const feats = DATA.top_features_per_class[cls]; | |
| const count = DATA.class_counts[cls]; | |
| const color = CLASS_COLORS[cls] || '#94a3b8'; | |
| const maxDiff = Math.max(...feats.map(f => f.abs_diff), 0.01); | |
| return ` | |
| <div class="top-feature-card"> | |
| <div class="class-name"><span class="legend-dot" style="background:${color}"></span>${cls}</div> | |
| <div class="class-count">${count.toLocaleString()} cells</div> | |
| ${feats.map((f, i) => ` | |
| <div class="feat-row"> | |
| <span class="feat-rank">${i+1}</span> | |
| <span class="feat-name" title="${f.feature}">${f.feature}</span> | |
| <div class="feat-bar-wrap"> | |
| <div class="feat-bar" style="width:${(f.abs_diff / maxDiff * 100).toFixed(1)}%;background:${f.class_mean > f.global_mean ? color : 'var(--red)'}"></div> | |
| </div> | |
| <span class="feat-val" title="class: ${f.class_mean}, global: ${f.global_mean}">${f.class_mean > f.global_mean ? '+' : ''}${f.abs_diff.toFixed(3)}</span> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function getFilteredFeatures() { | |
| const group = document.getElementById('group-select').value; | |
| if (group === 'all') return DATA.features; | |
| const [lo, hi] = FEATURE_GROUPS[group] || [0, DATA.num_features - 1]; | |
| return DATA.features.filter(f => f.index >= lo && f.index <= hi); | |
| } | |
| function renderComparisonChart() { | |
| const mode = document.getElementById('mode-select').value; | |
| const feats = getFilteredFeatures(); | |
| const labels = feats.map(f => f.name); | |
| const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0); | |
| const datasets = classes.map(cls => { | |
| const color = CLASS_COLORS[cls] || '#94a3b8'; | |
| return { | |
| label: cls, | |
| data: feats.map(f => { | |
| const ci = f.classes[cls]; | |
| if (!ci) return 0; | |
| if (mode === 'mean') return ci.mean; | |
| if (mode === 'prevalence') return ci.prevalence; | |
| return ci.diff_from_global; | |
| }), | |
| backgroundColor: color + '99', | |
| borderColor: color, | |
| borderWidth: 1, | |
| }; | |
| }); | |
| const ctx = document.getElementById('comparison-chart').getContext('2d'); | |
| if (comparisonChart) comparisonChart.destroy(); | |
| comparisonChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { labels, datasets }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 11 }, boxWidth: 12 } }, | |
| tooltip: { | |
| callbacks: { | |
| label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(4)}`, | |
| } | |
| }, | |
| }, | |
| scales: { | |
| x: { | |
| ticks: { color: '#64748b', font: { size: 10 }, maxRotation: 55, minRotation: 30 }, | |
| grid: { color: 'rgba(255,255,255,.04)' }, | |
| }, | |
| y: { | |
| ticks: { color: '#64748b', font: { size: 10 } }, | |
| grid: { color: 'rgba(255,255,255,.06)' }, | |
| title: { display: true, text: mode === 'prevalence' ? 'Fraction of cells with feature > 0' : mode === 'diff' ? 'Difference from global mean' : 'Mean value', color: '#94a3b8', font: { size: 11 } }, | |
| }, | |
| }, | |
| interaction: { mode: 'index', intersect: false }, | |
| }, | |
| }); | |
| } | |
| function renderClassChart() { | |
| const ctx = document.getElementById('class-chart').getContext('2d'); | |
| const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0); | |
| const counts = classes.map(k => DATA.class_counts[k]); | |
| const colors = classes.map(k => CLASS_COLORS[k] || '#94a3b8'); | |
| new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: classes, | |
| datasets: [{ | |
| data: counts, | |
| backgroundColor: colors.map(c => c + 'cc'), | |
| borderColor: colors, | |
| borderWidth: 1, | |
| }], | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { legend: { display: false } }, | |
| scales: { | |
| x: { ticks: { color: '#94a3b8', font: { size: 12, weight: 600 } }, grid: { display: false } }, | |
| y: { ticks: { color: '#64748b' }, grid: { color: 'rgba(255,255,255,.06)' }, title: { display: true, text: 'Count', color: '#94a3b8' } }, | |
| }, | |
| }, | |
| }); | |
| } | |
| function heatColor(val) { | |
| const t = Math.min(Math.max(val, 0), 1); | |
| if (t < 0.01) return 'transparent'; | |
| const r = Math.round(59 + t * 100); | |
| const g = Math.round(130 - t * 40); | |
| const b = Math.round(246); | |
| const a = 0.15 + t * 0.65; | |
| return `rgba(${r},${g},${b},${a})`; | |
| } | |
| function renderHeatmap() { | |
| const el = document.getElementById('heatmap-container'); | |
| const classes = Object.keys(DATA.class_counts).filter(k => DATA.class_counts[k] > 0); | |
| const groupNames = { | |
| 0: 'Position', 3: 'Content Stats', 12: 'Formatting', 28: 'Formula', | |
| 32: 'Content Type', 44: 'Keywords', 55: 'Merge', 56: 'Textual/Punctuation', 71: 'TUTA Numerical', | |
| 75: 'Column/Row Context', 83: 'TableSense Cohesion', | |
| }; | |
| let html = '<table class="heatmap"><thead><tr><th>Feature</th>'; | |
| classes.forEach(c => { html += `<th style="color:${CLASS_COLORS[c]||'#94a3b8'}">${c}</th>`; }); | |
| html += '<th style="color:var(--text-4)">Global</th></tr></thead><tbody>'; | |
| DATA.features.forEach(f => { | |
| if (groupNames[f.index] !== undefined) { | |
| html += `<tr><td colspan="${classes.length + 2}" class="group-label">${groupNames[f.index]}</td></tr>`; | |
| } | |
| html += `<tr><td class="feature-name">${f.name}</td>`; | |
| classes.forEach(cls => { | |
| const ci = f.classes[cls]; | |
| const val = ci ? ci.mean : 0; | |
| html += `<td style="background:${heatColor(val)};color:${val > 0.5 ? 'var(--text-0)' : 'var(--text-2)'}" title="${cls}: mean=${val.toFixed(4)}, prev=${ci ? ci.prevalence.toFixed(3) : 0}">${val.toFixed(3)}</td>`; | |
| }); | |
| html += `<td style="color:var(--text-4)">${f.global_mean.toFixed(3)}</td></tr>`; | |
| }); | |
| html += '</tbody></table>'; | |
| el.innerHTML = html; | |
| } | |
| document.getElementById('group-select').addEventListener('change', renderComparisonChart); | |
| document.getElementById('mode-select').addEventListener('change', renderComparisonChart); | |
| loadData(); | |
| </script> | |
| </body> | |
| </html> | |