experiment-lab / templates /sequential.html
mnoorchenar's picture
Update 2026-03-20 21:52:49
b9370c4
{% 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 %}