Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% block title %}Sequential Testing{% endblock %} | |
| {% block page_title %}Sequential Testing{% endblock %} | |
| {% block content %} | |
| <div class="split-panel"> | |
| <!-- Left: Parameters --> | |
| <div class="panel-left"> | |
| <div class="panel-header"> | |
| <span class="panel-header-title">Parameters</span> | |
| <div class="panel-header-actions"> | |
| <button class="icon-btn" id="reset-btn" title="Reset to defaults"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> | |
| <path d="M3 3v5h5"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="panel-body"> | |
| <div class="collapsible-section"> | |
| <button class="collapsible-trigger" aria-expanded="true" data-target="group-effect"> | |
| Experiment Parameters | |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg> | |
| </button> | |
| <div class="collapsible-content expanded" id="group-effect"> | |
| <div class="form-group"> | |
| <label for="true_effect">True underlying effect size</label> | |
| <div class="input-hint">Set to 0 to simulate a null experiment (no real difference)</div> | |
| <input type="number" id="true_effect" value="0.0" step="0.1" min="-2" max="2" /> | |
| </div> | |
| <div class="form-group"> | |
| <label for="obs_per_step">New observations per look</label> | |
| <div class="input-hint">Observations collected at each interim check</div> | |
| <input type="number" id="obs_per_step" value="50" min="10" max="500" /> | |
| </div> | |
| <div class="form-group"> | |
| <label for="total_steps">Total number of interim looks</label> | |
| <div class="input-hint">How many times you check during the experiment</div> | |
| <input type="number" id="total_steps" value="20" min="5" max="50" /> | |
| </div> | |
| <div class="form-group"> | |
| <label for="alpha_seq">Significance level (α)</label> | |
| <input type="number" id="alpha_seq" value="0.05" step="0.01" min="0.01" max="0.20" /> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="collapsible-section"> | |
| <button class="collapsible-trigger" aria-expanded="true" data-target="group-sims"> | |
| Simulation | |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg> | |
| </button> | |
| <div class="collapsible-content expanded" id="group-sims"> | |
| <div class="form-group"> | |
| <label for="num_sims">Number of simulation runs</label> | |
| <div class="input-hint">More runs = more stable estimates (max 50). Changes here require clicking Run.</div> | |
| <input type="number" id="num_sims" value="30" min="5" max="50" /> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="btn-primary" id="sim-btn" onclick="runModule()"> | |
| <span class="btn-spinner"></span> | |
| Run Simulation | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right: Results --> | |
| <div class="panel-right"> | |
| <div id="empty-state" class="empty-state"> | |
| <div class="empty-state-icon">📈</div> | |
| <div class="empty-state-text">Configure simulation parameters and click Run to see results.</div> | |
| </div> | |
| <div id="results-section" style="display:none;"> | |
| <div class="kpi-row"> | |
| <div class="kpi-tile highlight-bad"> | |
| <div class="kpi-label">Naive Repeated Testing<br/>False Positive Rate</div> | |
| <div class="kpi-value" id="naive-fp">—</div> | |
| </div> | |
| <div class="kpi-tile highlight-good"> | |
| <div class="kpi-label">Sequential Testing (OBF)<br/>False Positive Rate</div> | |
| <div class="kpi-value" id="seq-fp">—</div> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="chart-header"> | |
| <span class="chart-header-title">Naive Repeated Testing — Running p-values Across Simulations</span> | |
| </div> | |
| <div class="chart-desc"> | |
| Each line is one simulated experiment run. | |
| <strong style="color:#f43f5e;">Red lines</strong> crossed α at least once during the run — | |
| a false positive when no true effect exists. | |
| </div> | |
| <div class="chart-body"> | |
| <div id="chart-naive" class="chart-placeholder"></div> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="chart-header"> | |
| <span class="chart-header-title">Sequential Testing with O'Brien-Fleming Boundary</span> | |
| </div> | |
| <div class="chart-desc"> | |
| The green dashed boundary demands a much more extreme result early in the experiment and relaxes | |
| as more data accumulates — enabling valid early stopping without inflating the type I error rate. | |
| </div> | |
| <div class="chart-body"> | |
| <div id="chart-seq" class="chart-placeholder"></div> | |
| </div> | |
| </div> | |
| <div class="chart-card"> | |
| <div class="chart-header"> | |
| <span class="chart-header-title">False Positive Rate — Naive vs Sequential</span> | |
| </div> | |
| <div class="chart-body"> | |
| <div id="chart-compare" class="chart-placeholder-md"></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-title">Interpretation</div> | |
| <div class="interpretation" id="interp-text"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| let simInFlight = false; | |
| window.runModule = async function simulate() { | |
| if (simInFlight) return; | |
| simInFlight = true; | |
| hideError(); | |
| showSpinner(); | |
| const btn = document.getElementById('sim-btn'); | |
| btn.classList.add('loading'); | |
| btn.disabled = true; | |
| const payload = { | |
| true_effect: parseFloat(document.getElementById('true_effect').value), | |
| obs_per_step: parseInt(document.getElementById('obs_per_step').value), | |
| total_steps: parseInt(document.getElementById('total_steps').value), | |
| alpha: parseFloat(document.getElementById('alpha_seq').value), | |
| num_sims: parseInt(document.getElementById('num_sims').value), | |
| }; | |
| try { | |
| const resp = await fetch('/sequential/simulate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const r = await resp.json(); | |
| if (r.error) { showError(r.error); return; } | |
| document.getElementById('naive-fp').textContent = r.naive_fp_rate + '%'; | |
| document.getElementById('seq-fp').textContent = r.seq_fp_rate + '%'; | |
| document.getElementById('interp-text').innerHTML = r.interpretation; | |
| document.getElementById('empty-state').style.display = 'none'; | |
| document.getElementById('results-section').style.display = 'block'; | |
| renderChart('chart-naive', r.chart_naive); | |
| renderChart('chart-seq', r.chart_seq); | |
| renderChart('chart-compare', r.chart_compare); | |
| } catch(e) { | |
| showError(e.message); | |
| } finally { | |
| hideSpinner(); | |
| btn.classList.remove('loading'); | |
| btn.disabled = false; | |
| simInFlight = false; | |
| } | |
| }; | |
| // Selective debounce: num_sims only via button click | |
| ['true_effect', 'obs_per_step', 'total_steps', 'alpha_seq'].forEach(id => { | |
| document.getElementById(id).addEventListener('input', debounce(runModule, 800)); | |
| }); | |
| registerDefaults({ | |
| true_effect: '0.0', | |
| obs_per_step: '50', | |
| total_steps: '20', | |
| alpha_seq: '0.05', | |
| num_sims: '30', | |
| }); | |
| window.addEventListener('load', runModule); | |
| </script> | |
| {% endblock %} | |