Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 393 additions & 0 deletions playground/acf_pacf.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>tsb — ACF / PACF / Portmanteau Tests</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #e6edf3;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
padding: 2rem;
max-width: 900px;
margin: 0 auto;
}
a { color: var(--accent); }
h1 { color: var(--accent); margin-bottom: 0.5rem; }
h2 { margin-top: 0; margin-bottom: 0.5rem; font-size: 1.25rem; }
p { color: #8b949e; margin-bottom: 1rem; }
code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.3rem;
padding: 0.1rem 0.4rem;
}
.back { margin-bottom: 2rem; display: inline-block; }
.subtitle { margin-bottom: 1.5rem; }

#playground-loading {
position: fixed; inset: 0;
background: rgba(13, 17, 23, 0.92);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
z-index: 1000; gap: 1rem;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#playground-status { color: #8b949e; font-size: 0.95rem; }

.section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.section p { margin-bottom: 0.75rem; }

.playground-block { margin-top: 0.75rem; }
.playground-header {
display: flex; align-items: center; justify-content: space-between;
background: #1c2128;
border: 1px solid var(--border);
border-bottom: none;
border-radius: 0.5rem 0.5rem 0 0;
padding: 0.4rem 0.75rem;
}
.playground-label {
font-size: 0.75rem; color: #8b949e;
text-transform: uppercase; letter-spacing: 0.05em;
}
.playground-actions { display: flex; gap: 0.5rem; }
.playground-actions button {
background: transparent; color: var(--accent);
border: 1px solid var(--border);
border-radius: 0.35rem;
padding: 0.25rem 0.7rem;
font-size: 0.8rem; cursor: pointer;
font-family: system-ui, sans-serif;
transition: background 0.15s, border-color 0.15s;
}
.playground-actions button:hover:not(:disabled) {
background: rgba(88, 166, 255, 0.1);
border-color: var(--accent);
}
.playground-actions button:disabled { opacity: 0.4; cursor: not-allowed; }
.playground-run { font-weight: 600; }

.playground-editor {
display: block; width: 100%; min-height: 80px;
background: #0d1117; color: var(--text);
border: 1px solid var(--border);
border-top: none; border-bottom: none;
padding: 1rem;
font-family: var(--font-mono);
font-size: 0.875rem; line-height: 1.55;
resize: vertical; outline: none;
tab-size: 2; white-space: pre; overflow-x: auto;
}
.playground-editor:focus {
border-color: var(--accent);
box-shadow: inset 0 0 0 1px var(--accent);
}

.playground-output {
background: #1c2333;
border: 1px solid var(--border);
border-radius: 0 0 0.5rem 0.5rem;
padding: 0.75rem 1rem;
font-family: var(--font-mono);
font-size: 0.85rem; color: #8b949e;
white-space: pre-wrap; min-height: 2rem;
word-break: break-word;
}
.playground-output.active { color: var(--green); border-color: var(--green); }
.playground-output.error { color: var(--red); border-color: var(--red); }
.playground-hint {
font-size: 0.75rem; color: #484f58;
margin-top: 0.35rem; text-align: right;
}

footer {
text-align: center;
padding: 2rem 0;
color: #8b949e;
font-size: 0.85rem;
border-top: 1px solid var(--border);
margin-top: 2rem;
}
</style>
</head>
<body>

<div id="playground-loading">
<div class="spinner"></div>
<div id="playground-status">Initializing playground…</div>
</div>

<a class="back" href="index.html">← Back to roadmap</a>
<h1>ACF / PACF &amp; Portmanteau Tests</h1>
<p class="subtitle">
Autocorrelation (<code>acf</code>), partial autocorrelation (<code>pacf</code>),
cross-correlation (<code>ccf</code>), Durbin-Watson, Ljung-Box, and Box-Pierce tests —
mirrors <code>statsmodels.tsa.stattools</code> and <code>pd.Series.autocorr</code>.
</p>

<div class="section">
<h2>1 — Single-lag autocorrelation (pandas-style)</h2>
<p>
<code>autocorr(x, lag)</code> computes the Pearson correlation between
<code>x[0..n−lag−1]</code> and <code>x[lag..n−1]</code>, exactly like
<code>pd.Series.autocorr(lag)</code>.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { autocorr } from "tsb";

// Linear trend — strong positive autocorrelation at all lags
const trend = Array.from({ length: 20 }, (_, i) => i + 1);
console.log("Trend autocorr lag=1:", autocorr(trend, 1).toFixed(4));
console.log("Trend autocorr lag=5:", autocorr(trend, 5).toFixed(4));

// Alternating ±1 — perfect negative autocorrelation at lag 1
const alt = Array.from({ length: 10 }, (_, i) => i % 2 === 0 ? 1 : -1);
console.log("Alternating autocorr lag=1:", autocorr(alt, 1).toFixed(4));

// Seasonal: period 4
const seasonal = [1, 2, 3, 2, 1, 2, 3, 2, 1, 2, 3, 2];
for (let k = 0; k <= 4; k++) {
console.log(`Seasonal autocorr lag=${k}:`, autocorr(seasonal, k).toFixed(4));
}</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<div class="section">
<h2>2 — Full ACF with Bartlett confidence intervals</h2>
<p>
<code>acf(x, { nlags, alpha })</code> returns all autocorrelations at lags 0…nlags.
With <code>alpha=0.05</code>, Bartlett confidence intervals are returned: lags whose
CI excludes zero are statistically significant.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { acf } from "tsb";

// AR(1)-like series with φ ≈ 0.8
const x = [1];
for (let i = 1; i < 60; i++) x.push(0.8 * x[i-1] + (Math.random() - 0.5) * 0.3);

const result = acf(x, { nlags: 8, alpha: 0.05 });
console.log("Lag | ACF | 95% CI");
console.log("----+---------+------------------");
for (let k = 0; k <= 8; k++) {
const r = result.acf[k].toFixed(4);
const ci = result.confint[k];
const lo = ci[0].toFixed(4);
const hi = ci[1].toFixed(4);
const sig = (result.acf[k] < ci[0] || result.acf[k] > ci[1]) ? " *" : "";
console.log(` ${k} | ${r} | [${lo}, ${hi}]${sig}`);
}</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<div class="section">
<h2>3 — Partial ACF (Levinson-Durbin)</h2>
<p>
<code>pacf(x, { nlags, alpha })</code> uses the Levinson-Durbin recursion to compute
partial autocorrelations. For a true AR(p) process, only the first p PACF values are
significantly non-zero — this is how you identify the AR order.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { pacf } from "tsb";

// AR(2): x[t] = 0.6*x[t-1] - 0.3*x[t-2] + ε
// → PACF should show non-zero values at lags 1 and 2, near-zero beyond
const x = [0, 0];
for (let i = 2; i < 100; i++) {
x.push(0.6 * x[i-1] - 0.3 * x[i-2] + (Math.random() - 0.5) * 0.2);
}

const result = pacf(x, { nlags: 6, alpha: 0.05 });
console.log("PACF for AR(2) process");
console.log("Lag | PACF | 95% CI");
console.log("----+---------+--------------------------");
for (let k = 0; k <= 6; k++) {
const r = result.pacf[k].toFixed(4);
const ci = result.confint[k];
const lo = ci[0].toFixed(4);
const hi = ci[1].toFixed(4);
console.log(` ${k} | ${r} | [${lo}, ${hi}]`);
}
console.log("\nLags 1-2 should be significant; higher lags should be near zero.");</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<div class="section">
<h2>4 — Cross-Correlation Function (CCF)</h2>
<p>
<code>ccf(x, y, { nlags, alpha })</code> measures the linear relationship between
<code>x[t]</code> and <code>y[t+k]</code> at each lag k. Peaks in the CCF reveal
lead/lag relationships between two series.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { ccf } from "tsb";

// Create x = sine wave; y = x shifted by 3 steps
const n = 40;
const x = Array.from({ length: n }, (_, i) => Math.sin(2 * Math.PI * i / 8));
const y = Array.from({ length: n }, (_, i) => Math.sin(2 * Math.PI * (i - 3) / 8));

const result = ccf(x, y, { nlags: 6, positiveOnly: false });
console.log("Lag | CCF");
console.log("-----+--------");
for (let i = 0; i < result.lags.length; i++) {
const bar = "#".repeat(Math.round(Math.abs(result.acf[i]) * 20));
console.log(`${String(result.lags[i]).padStart(4)} | ${result.acf[i].toFixed(4)} ${bar}`);
}
console.log("\nPeak at lag 3 indicates y leads x by 3 steps.");</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<div class="section">
<h2>5 — Durbin-Watson statistic</h2>
<p>
<code>durbinWatson(residuals)</code> tests for first-order autocorrelation in OLS residuals.
Values near 2 indicate no autocorrelation; values near 0 indicate positive autocorrelation;
values near 4 indicate negative autocorrelation.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { durbinWatson } from "tsb";

// White noise residuals — DW ≈ 2
const noise = Array.from({ length: 50 }, () => Math.random() - 0.5);
console.log("White noise DW:", durbinWatson(noise).toFixed(4), "(should be ≈ 2)");

// Positively autocorrelated — DW < 2 (near 0)
const posAC = noise.map((_, i) => 1 + i * 0.01); // slow upward drift
console.log("Positive AC DW:", durbinWatson(posAC).toFixed(4), "(should be < 1)");

// Alternating residuals — DW ≈ 4 (negative autocorrelation)
const altResid = Array.from({ length: 20 }, (_, i) => i % 2 === 0 ? 1 : -1);
console.log("Alternating DW:", durbinWatson(altResid).toFixed(4), "(should be ≈ 4)");

// Rule of thumb: 1.5 < DW < 2.5 → no autocorrelation concern
const isOk = (dw) => dw > 1.5 && dw < 2.5;
console.log("\nWhite noise is OK:", isOk(durbinWatson(noise)));
console.log("Positive AC is OK:", isOk(durbinWatson(posAC)));</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<div class="section">
<h2>6 — Ljung-Box &amp; Box-Pierce portmanteau tests</h2>
<p>
<code>ljungBox(x, { lags })</code> and <code>boxPierce(x, { lags })</code> test the null
hypothesis that no autocorrelation exists up to a given lag. Small p-values reject the
white-noise hypothesis. Ljung-Box has better finite-sample properties.
</p>
<div class="playground-block">
<div class="playground-header">
<span class="playground-label">JavaScript</span>
<div class="playground-actions">
<button class="playground-run" disabled>▶ Run</button>
<button class="playground-reset">↺ Reset</button>
</div>
</div>
<textarea class="playground-editor" spellcheck="false">import { ljungBox, boxPierce } from "tsb";

// White noise: p-values should be large (fail to reject H0)
const wn = Array.from({ length: 100 }, () => Math.random() - 0.5);
const lb_wn = ljungBox(wn, { lags: [5, 10, 15] });
console.log("=== White Noise ===");
lb_wn.lags.forEach((h, i) => {
console.log(`Lag ${h}: Q=${lb_wn.statistic[i].toFixed(3)}, p=${lb_wn.pvalue[i].toFixed(3)}`);
});

// AR(1) process: p-values should be very small (reject H0)
const ar1 = [1.0];
for (let i = 1; i < 100; i++) ar1.push(0.85 * ar1[i-1] + (Math.random() - 0.5) * 0.2);
const lb_ar = ljungBox(ar1, { lags: [5, 10] });
const bp_ar = boxPierce(ar1, { lags: [5, 10] });
console.log("\n=== AR(1) φ=0.85 ===");
lb_ar.lags.forEach((h, i) => {
console.log(`Lag ${h}: LB Q=${lb_ar.statistic[i].toFixed(2)}, p=${lb_ar.pvalue[i].toExponential(2)}; BP Q=${bp_ar.statistic[i].toFixed(2)}, p=${bp_ar.pvalue[i].toExponential(2)}`);
});</textarea>
<div class="playground-output">Click ▶ Run to execute</div>
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
</div>
</div>

<footer>
<p>
<a href="index.html">tsb playground</a> ·
Built by <a href="https://github.com/githubnext/autoloop">Autoloop</a>
</p>
</footer>

<script type="module" src="playground-runtime.js"></script>
</body>
</html>
Loading
Loading