artatopics / ensemble.html
artaquest's picture
Publish the artatopics results + the ensemble stack page
28f5468 verified
Raw
History Blame Contribute Delete
17.8 kB
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>artatopics — the ensemble stack</title>
<style>
:root { --gold:#8A6D0B; --blue:#1746DC; --bg:#ffffff; --card:#f6f7f9; --ink:#1a2330; --ink2:#4a5568; --ink3:#6b7686; --line:#e3e6ea; }
@media (prefers-color-scheme: dark) {
:root { --gold:#E8B923; --blue:#6f8dff; --bg:#010C17; --card:#06121E; --ink:#eef2f7; --ink2:#b8c2cf; --ink3:#8b98a8; --line:#12283c; } }
* { box-sizing:border-box } body { margin:0; background:var(--bg); color:var(--ink);
font:15px/1.55 -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif }
main { max-width:760px; margin:0 auto; padding:36px 20px 70px }
h1 { font-size:26px; margin:0 } h1 b { color:var(--gold) }
h2 { font-size:16px; margin:34px 0 8px }
p { color:var(--ink2); margin:8px 0 }
table { border-collapse:collapse; width:100%; font-size:14px }
th,td { text-align:left; padding:6px 10px; border-bottom:1px solid var(--line) }
th { color:var(--ink3); font-size:11px; text-transform:uppercase; letter-spacing:.08em }
td.n { font-variant-numeric:tabular-nums; text-align:right }
select,button { padding:8px 12px; border-radius:10px; border:1px solid var(--line);
background:var(--card); color:var(--ink); font-size:14px }
button { cursor:pointer } button:hover { border-color:var(--blue) }
svg { width:100%; background:var(--card); border:1px solid var(--line); border-radius:10px }
.cap { font-size:12.5px; color:var(--ink3); margin:4px 0 0 }
.mono { font-family:ui-monospace,Menlo,monospace; background:var(--card); border:1px solid var(--line);
border-radius:10px; padding:10px 14px; overflow-x:auto }
.wrap { overflow-x:auto }
#pyout { white-space:pre-wrap; font:12.5px/1.5 ui-monospace,Menlo,monospace; background:var(--card);
border:1px solid var(--line); border-radius:10px; padding:10px 14px; min-height:40px }
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:8px 0 }
.row label { font-size:13px; color:var(--ink2); min-width:120px }
.row input[type=range] { flex:1; accent-color:var(--gold) }
.row output { font-variant-numeric:tabular-nums; font-size:13px; width:44px; text-align:right }
a { color:var(--blue) }
footer { margin-top:44px; font-size:12.5px; color:var(--ink3) }
</style></head><body><main>
<h1>arta<b>topics</b> · the ensemble stack</h1>
<p>Four Kaggle accounts, four GPU model families, one benchmark: forecast each of 251 research
fields' share of the world's citations for 1996–2025, fitting only on 1700–1995. Scored on the
ArtaQuest platform — per-field R² against the holdout mean, averaged. Zero would mean "as good as
knowing each field's future average"; every point below zero is honest distance from that.
Dataset: <a href="https://www.kaggle.com/datasets/artafather/astro-ensemble-251">astro-ensemble-251</a>.</p>
<h2>1 · The board</h2>
<div class="wrap"><table><tr><th>model</th><th>entrant</th><th>score</th></tr>
<tr><td>damped linear trend (reference baseline)</td><td>baseline</td><td class='n'><b>-2.0400</b></td></tr>
<tr><td>THE STACK v3.1 — recent-regime selection · <a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/the_stack_v31.py">code</a></td><td>the stack (this page)</td><td class='n'><b>-2.0927</b></td></tr>
<tr><td>stack v5 — kernel wall members offered; shared-basis took a slice, transferred worse · <a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/the_stack_v5.py">code</a></td><td>the stack</td><td class='n'><b>-2.1211</b></td></tr>
<tr><td>the stack v3 — six-wall selection · <a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/the_stack.py">code</a></td><td>the stack</td><td class='n'><b>-2.2897</b></td></tr>
<tr><td>carry today forward (= every family's round-3 shrink verdict: lam 0)</td><td>baseline</td><td class='n'><b>-2.5614</b></td></tr>
<tr><td>random sky-feature ridge swarm · <a href="https://www.kaggle.com/code/ashranet/astro-ensemble-entry-sky-swarm">code</a></td><td>ashranet · GPU</td><td class='n'><b>-3.5923</b></td></tr>
<tr><td>neural shared-basis receiver · <a href="https://www.kaggle.com/code/ashraasn/astro-ensemble-entry-shared-basis">code</a></td><td>ashraasn · GPU</td><td class='n'><b>-5.1739</b></td></tr>
<tr><td>deep per-field phasor · <a href="https://www.kaggle.com/code/arash0ash/astro-ensemble-entry-deep-phasor">code</a></td><td>arash0ash · GPU</td><td class='n'><b>-5.6035</b></td></tr>
<tr><td>pooled gradient boosting (round 1) · <a href="https://www.kaggle.com/code/artafather/astro-ensemble-entry-boosted-sky">code</a></td><td>artafather · GPU</td><td class='n'><b>-62.3708</b></td></tr></table></div>
<p class="cap">Every single-family GPU model loses to the do-nothing baselines. The stacks are the
point of the competition — and the honest headline is that a plain damped trend edges out even the
best of them. The 0.05 between the trend baseline and stack v3.1 is the price of committing to a
model before the answer was visible.</p>
<h2>2 · The deployed model, in one line</h2>
<p class="mono">forecast(field, h) = a(h) · yesterday + (1−a(h)) · [ 0.875 · trend + 0.125 · receiver ]</p>
<p><b>yesterday</b> — the field's 1995 share, held flat. <b>trend</b> — a straight line through its
last 15 years, with the slope damped away (φ=0.85). <b>receiver</b> — the repository's 9-parameter
per-field sky receiver: level + amplitude + seven phases read against the slow planets, the best
purely astrological model this campaign produced. <b>a(h)</b> — how much "yesterday" matters at
horizon h: 0.14 next year, rising to about 0.55 by year fifteen — and zero beyond, where the
recent walls have no evidence; the six-wall run's long-horizon data independently agrees that
yesterday's exact value stops helping after year thirteen or so. Selection used only data from before 1996,
on the three most recent walls (1981/86/91) — the disclosure of how that window was chosen is
written into <a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/the_stack_v31.py">the code's header</a>.</p>
<h2>3 · Every field, forecast against what happened</h2>
<div class="row"><select id="pick" style="flex:1"></select></div>
<svg id="chart" viewBox="0 0 760 280"></svg>
<p class="cap">Blue — the field's real share of each year's citations, 1700–2025. Gold dashes — the
deployed stack's 30-year forecast from the 1995 wall. Thin lines — its members
(<span style="color:#c084fc">damped trend</span>, <span style="color:#f87171">sky receiver</span>,
<span style="color:#34d399">yesterday</span>).</p>
<h2>4 · What each part is worth on the judged years</h2>
<table><tr><th>variant</th><th>score</th></tr>
<tr><td>without carry (a=0)</td><td class='n'>-2.0350</td></tr>
<tr><td>damped trend baseline</td><td class='n'>-2.0400</td></tr>
<tr><td>without the record receiver</td><td class='n'>-2.0820</td></tr>
<tr><td>stack v3.1 (the deployed model)</td><td class='n'>-2.0927</td></tr>
<tr><td>carry-forward baseline</td><td class='n'>-2.5614</td></tr>
<tr><td>record receiver alone</td><td class='n'>-3.5767</td></tr></table>
<p class="cap">The sky receiver keeps a 12.5% slice because the pre-1996 evidence earned it one.
Dropping it would have scored -2.0820 — its slice cost 0.011 points on the judged years. Both facts are on this page because both are true.</p>
<h2>5 · The finding: you learn the era you select in</h2>
<div class="wrap"><table><tr><th>fit ≤ wall, judged after</th><th>yesterday</th><th>trend</th><th>5-yr level</th><th>sky swarm</th><th>receiver</th></tr>
<tr><td>1966 → 1996</td><td class='n'>-12.80</td><td class='n'>-14.98</td><td class='n'>-4.78</td><td class='n'>-6.64</td><td class='n'>-6.08</td></tr>
<tr><td>1971 → 2001</td><td class='n'>-5.99</td><td class='n'>-7.38</td><td class='n'>-4.00</td><td class='n'>-4.39</td><td class='n'>-4.07</td></tr>
<tr><td>1976 → 2006</td><td class='n'>-3.83</td><td class='n'>-6.00</td><td class='n'>-4.40</td><td class='n'>-4.67</td><td class='n'>-5.59</td></tr>
<tr><td>1981 → 2011</td><td class='n'>-2.48</td><td class='n'>-2.61</td><td class='n'>-3.86</td><td class='n'>-4.35</td><td class='n'>-6.78</td></tr>
<tr><td>1986 → 2016</td><td class='n'>-3.00</td><td class='n'>-3.47</td><td class='n'>-4.15</td><td class='n'>-4.88</td><td class='n'>-5.46</td></tr>
<tr><td>1991 → 2021</td><td class='n'>-3.92</td><td class='n'>-3.30</td><td class='n'>-7.98</td><td class='n'>-6.87</td><td class='n'>-8.68</td></tr>
<tr><td><b>1996 → 2025 (judged)</b></td><td class='n'>-2.56</td><td class='n'>-2.04</td><td class='n'>-3.32</td><td class='n'>-3.24</td><td class='n'></td></tr></table></div>
<p>Read down any column. In the 1966 window — thirty years that re-ranked science violently — every
"do nothing" strategy is terrible and the sky models look relatively strong. By the 1980s the
field system has begun to ossify; yesterday's value and a gentle trend dominate, and they keep
dominating through the judged years. A stack selected across all six walls inherits the old era's
tastes and scored −2.29; selected on the recent regime only, −2.09. No planetary configuration
explains this — the calendar does. That is the competition's deepest result, and it is the same
one the <a href="index.html">main page</a> reports for the trending classifier: the sky's
predictive power here is mostly a slow clock.</p>
<h2>6 · Rebuild it in your browser</h2>
<p>This page ships the raw share matrix, the stack's parameters and the receiver's forecast. The
button loads Python (Pyodide + numpy, ~10&nbsp;MB, from a CDN), rebuilds the trend and carry
members from the raw shares, reassembles the stack, checks it against the exact forecast on the
board, and re-scores it on the held-out truth. Then the sliders re-mix the ensemble live. Note
that the two weight sliders only re-mix the bracketed part: yesterday is still folded in by a(h),
so receiver&nbsp;=&nbsp;1 scores about &minus;3.21 rather than the receiver's own &minus;3.58. Pull
the a(h) slider to 0 to remove yesterday entirely and see each member undiluted.</p>
<div class="row"><button id="run">Run the verification</button><span id="pystat" class="cap"></span></div>
<div id="pyout">(not run yet)</div>
<div id="mixer" style="display:none">
<div class="row"><label>damped trend</label><input type="range" id="w_trend" min="0" max="100" value="88"><output id="o_trend">.88</output></div>
<div class="row"><label>sky receiver</label><input type="range" id="w_record" min="0" max="100" value="12"><output id="o_record">.12</output></div>
<div class="row"><label>a(h) scale</label><input type="range" id="w_alpha" min="0" max="200" value="100"><output id="o_alpha">1.0</output></div>
<div class="row"><button id="rescore">Re-score this mix</button><b id="mixscore" style="font-variant-numeric:tabular-nums"></b></div>
<p class="cap">Weights renormalise to sum to one. The re-score runs the same numpy code on the same
data — nothing on this page is taken on faith.</p>
</div>
<footer>No causal claims. Scored by the ArtaQuest platform · code and full history:
<a href="https://github.com/ArtaQuest/artatopics">github.com/ArtaQuest/artatopics</a> ·
<a href="index.html">main results</a> ·
<a href="https://huggingface.co/spaces/artaquest/artatopics">also on Hugging Face</a></footer>
</main>
<script>
(async () => {
const D = await (await fetch("data/ensemble.json")).json();
const J = D.names.length, W = D.wall_year - D.y0, H = 30;
const pick = document.getElementById("pick"), svg = document.getElementById("chart");
D.names.map((n, j) => [n, j]).sort((a, b) => a[0].localeCompare(b[0]))
.forEach(([n, j]) => { const o = document.createElement("option"); o.textContent = n; o.value = j; pick.appendChild(o); });
const css = v => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
function members(j) {
const s = D.shares[j], L = s[W - 1], K = D.stack.trend_window, phi = D.stack.trend_phi;
const xs = [], ys = [];
for (let t = W - K; t < W; t++) if (s[t] > 0) { xs.push(t); ys.push(s[t]); }
let m = 0;
if (xs.length >= 4) {
const mx = xs.reduce((a, b) => a + b, 0) / xs.length, my = ys.reduce((a, b) => a + b, 0) / ys.length;
let num = 0, den = 0;
xs.forEach((x, i) => { num += (x - mx) * (ys[i] - my); den += (x - mx) * (x - mx); });
m = den ? num / den : 0;
}
const tr = [], cy = [];
for (let h = 1; h <= H; h++) {
tr.push(Math.max(L + m * phi * (1 - Math.pow(phi, h)) / (1 - phi), 0));
cy.push(L);
}
return { trend: tr, record: D.record_pred[j], carry: cy };
}
function stackOf(j) {
const M = members(j), g = D.stack.mix, a = D.stack.alpha, gt = g.trend + g.record;
return a.map((ah, k) => Math.max(ah * M.carry[k] + (1 - ah) *
(g.trend * M.trend[k] + g.record * M.record[k]) / gt, 0));
}
function draw(j) {
const s = D.shares[j].map(x => x * 100), F = stackOf(j).map(x => x * 100), M = members(j);
const Wd = 760, Hd = 280, Lp = 46, Rp = 8, Tp = 10, Bp = 24, N = s.length;
const ymax = Math.max(...s, ...F) * 1.08 || 1;
const x = i => Lp + (Wd - Lp - Rp) * i / (N - 1), y = v => Tp + (Hd - Tp - Bp) * (1 - v / ymax);
const path = (arr, i0) => arr.map((v, k) => (k ? "L" : "M") + x(i0 + k).toFixed(1) + " " + y(v).toFixed(1)).join("");
let g = "";
for (let f = 0; f <= 4; f++) { const yy = Tp + (Hd - Tp - Bp) * f / 4;
g += `<line x1="${Lp}" y1="${yy}" x2="${Wd - Rp}" y2="${yy}" stroke="${css('--line')}"/>`;
g += `<text x="${Lp - 6}" y="${yy + 4}" fill="${css('--ink3')}" font-size="10" text-anchor="end">${(ymax * (1 - f / 4)).toFixed(1)}</text>`; }
for (let yr = 1725; yr <= D.y0 + N; yr += 50) { g += `<text x="${x(yr - D.y0)}" y="${Hd - 8}" fill="${css('--ink3')}" font-size="10" text-anchor="middle">${yr}</text>`; }
const xw = x(W - 1);
g += `<line x1="${xw}" y1="${Tp}" x2="${xw}" y2="${Hd - Bp}" stroke="${css('--ink3')}" stroke-dasharray="2 3"/>`;
const th = (arr, col) => `<path d="${path(arr.map(v => v * 100), W)}" fill="none" stroke="${col}" stroke-width="1" opacity=".65"/>`;
g += th(M.trend, "#c084fc") + th(M.record, "#f87171") + th(M.carry, "#34d399");
g += `<path d="${path(s, 0)}" fill="none" stroke="${css('--blue')}" stroke-width="1.6"/>`;
g += `<path d="${path(F, W)}" fill="none" stroke="${css('--gold')}" stroke-width="2.2" stroke-dasharray="6 4"/>`;
svg.innerHTML = g;
}
pick.onchange = () => draw(+pick.value);
pick.value = "" + D.names.indexOf("Artificial Intelligence"); draw(+pick.value);
const stat = document.getElementById("pystat"), out = document.getElementById("pyout");
let py = null;
const PYCODE = `
import numpy as np, json
D = json.loads(DATA_JSON)
Y = np.array(D["shares"]); J, N = Y.shape
W = D["wall_year"] - D["y0"]; H = 30
mix, alpha = D["stack"]["mix"], np.array(D["stack"]["alpha"])
phi, K = D["stack"]["trend_phi"], D["stack"]["trend_window"]
carry = np.repeat(Y[:, W-1:W], H, 1)
tr = np.zeros((J, H)); h = np.arange(1, H+1)
for j in range(J):
win = Y[j, W-K:W]; idx = np.where(win > 0)[0] + (W-K); L = Y[j, W-1]
if len(idx) < 4: tr[j] = L; continue
m = np.polyfit(idx.astype(float), Y[j, idx], 1)[0]
tr[j] = np.clip(L + m*phi*(1-phi**h)/(1-phi), 0, None)
rec = np.array(D["record_pred"])
def assemble(gt, gr, ascale):
tot = gt + gr
sky = (gt*tr + gr*rec)/tot if tot > 0 else np.zeros((J, H))
a = np.clip(alpha*ascale, 0, 1)
return np.clip(a[None]*carry + (1-a[None])*sky, 0, None)
def score(P):
sc = []
for j in range(J):
t = Y[j, W:W+H]; mu = t.mean(); ss = ((t-mu)**2).sum()
if ss < 1e-12: continue
sc.append(1 - ((t-P[j])**2).sum()/ss)
return float(np.mean(sc))
P = assemble(mix["trend"], mix["record"], 1.0)
print(f"members rebuilt from the raw shares ({J} fields x {N} years)")
print(f"stack reassembled; score on held-out 1996-2025 truth: {score(P):+.4f}")
print("board says -2.0927 -- reproduced" if abs(score(P) - (-2.09273)) < 0.002 else "MISMATCH vs the board")
`;
document.getElementById("run").onclick = async () => {
try {
stat.textContent = "loading Pyodide…";
if (!py) {
const mod = await import("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs");
py = await mod.loadPyodide(); await py.loadPackage("numpy");
}
stat.textContent = "running…";
py.globals.set("DATA_JSON", JSON.stringify(D));
let buf = "";
py.setStdout({ batched: s => { buf += s + "\n"; out.textContent = buf; } });
await py.runPythonAsync(PYCODE);
stat.textContent = "done — live re-mixing enabled";
document.getElementById("mixer").style.display = "block";
} catch (e) { stat.textContent = ""; out.textContent = "failed to load/run: " + e; }
};
const upd = () => {
["trend", "record"].forEach(k =>
document.getElementById("o_" + k).textContent = (document.getElementById("w_" + k).value / 100).toFixed(2).slice(1));
document.getElementById("o_alpha").textContent = (document.getElementById("w_alpha").value / 100).toFixed(1);
};
["w_trend", "w_record", "w_alpha"].forEach(id => document.getElementById(id).oninput = upd);
document.getElementById("rescore").onclick = async () => {
const v = id => document.getElementById(id).value / 100;
py.globals.set("GT", v("w_trend")); py.globals.set("GR", v("w_record")); py.globals.set("AS", v("w_alpha"));
const s = await py.runPythonAsync("score(assemble(GT, GR, AS))");
document.getElementById("mixscore").textContent = s.toFixed(4) + (s > -2.0398 ? " — beats every entry on the board" : "");
};
})();
</script>
</body></html>