AgentHEPGitHub ↗
Tier 5 · Statistics · v1.0.0

Local significance of an excess from sidebands

Estimate the background under a mass window from an unbinned exponential fit to the sidebands and quantify the excess with an Asimov formula and an exact Poisson p-value.

t5-bump-significancestatisticssynth_diphotonunbinned_ml_fitsidebandshypothesis_testpoissoninterpretation

Task prompt (what the agent sees)

`data/synth_diphoton.csv` (data card in README.md) contains a diphoton invariant mass `m_gg` per event.
Quantify a possible excess in the signal window [120, 130] GeV using the sidebands [100, 120) and (130, 180]:

1. Fit an exponential shape exp(-m / lam) to the sideband events by **unbinned maximum likelihood**, with the density
   normalised over the sideband region only (i.e. over [100,120) union (130,180]). Search lam in [5, 200].
2. Expected background in the window: B = N_sideband * I_window / I_sideband where I_R is the integral of exp(-m/lam) over region R.
3. Observed count N = number of events with 120 <= m_gg <= 130.
4. z_asimov = sqrt(2 * (N * ln(N / B) - (N - B))) with a negative sign if N < B.
5. p_value = P(X >= N) for X ~ Poisson(B) (exact, e.g. `scipy.stats.poisson.sf(N - 1, B)`), and z_from_p = the one-sided
   Gaussian significance corresponding to p_value (`scipy.stats.norm.isf(p_value)`).
6. `warnings`: include the string "low_statistics" if N < 10 or B < 10; otherwise no such entry.
7. `claim`: exactly "significant_excess" if z_from_p >= 3.0, otherwise "no_significant_excess".

Deliverables:
- `result.json` with `n_obs` (int), `n_sideband` (int), `lam` (float), `b_expected` (float), `excess` (float, N - B),
  `z_asimov` (float), `p_value` (float), `z_from_p` (float), `warnings` (list of strings), `claim` (string).
- `figures/window.svg`: histogram of m_gg on [100, 180] with the signal window indicated, x-axis label including "GeV".
- `solution.py`: the complete analysis; `python solution.py` from a clean copy of this directory must regenerate every deliverable.

Data card (README.md in the workdir)

Data card: data/synth_diphoton.csv (SYNTHETIC)

Simulated diphoton events from a toy generator with a known seed: a smoothly falling continuum on [100, 180] GeV plus a narrow resonance. This is synthetic data; it is not a measurement.

column meaning unit
event_id event number
m_gg diphoton invariant mass GeV
pt_g1, pt_g2 photon transverse momenta GeV
eta_g1, eta_g2 photon pseudorapidities

No missing values, no duplicated events. Every event has 100 < m_gg < 180.

Expected artifacts

  • result.json json
  • figures/window.svg svg
  • solution.py script

Deterministic checks and tolerances

rubric: artifact 0.1 · numeric 0.45 · plot 0.1 · compliance 0.15 · reproducibility 0.2
CheckTypeTargetToleranceWeightCategoryCriticalFailure implies
result_existsfile_existsresult.json1artifactcriticalno_output
figure_existsfile_existsfigures/window.svg1artifactno_outputplotting_error
solution_existsfile_existssolution.py1artifactno_output
n_obsjson_valueresult.json › n_obsexact2numericcriticalinvalid_cut
n_sbjson_valueresult.json › n_sidebandexact1numericinvalid_cut
lamjson_valueresult.json › lamrtol 0.032numericstatistical_misuse
b_expectedjson_valueresult.json › b_expectedrtol 0.033numericcriticalstatistical_misuseincorrect_normalization
excessjson_valueresult.json › excessrtol 0.051numericstatistical_misuse
z_asimovjson_valueresult.json › z_asimovatol 0.152numericcriticalstatistical_misuse
p_valuejson_valueresult.json › p_valuertol 0.51numericstatistical_misuse
z_from_pjson_valueresult.json › z_from_patol 0.152numericstatistical_misuse
claimjson_valueresult.json › claimexact3compliancecriticalunjustified_interpretation
no_low_stats_flagjson_not_containsresult.json › warnings = low_statistics1compliancespec_noncompliance
svg_unitsvg_textfigures/window.svg ["GeV"]1plotplotting_error
svg_drawnsvg_has_marksfigures/window.svg≥ 51plotplotting_error
rerunsscript_runssolution.py1reproducibilitynon_reproducible
not_hardcodedno_hardcoded_resultsolution.py1compliancefabricated_result

Tolerance rationale. The unbinned ML estimate of lam is unique, so rtol 3% catches binned/chi2 substitutes on small samples while accepting optimizer differences. Significances atol 0.15; the exact Poisson p-value rtol 0.5 because it spans orders of magnitude and z_from_p carries the precise check.

Ground truth (produced by the reference in the sandbox)

built 2026-09-06 · numpy 1.26.4 · scipy 1.13.1 · 1.739s
result.json
{
 "n_obs": 4942,
 "n_sideband": 25358,
 "lam": 24.946467457251916,
 "b_expected": 4629.777049959585,
 "excess": 312.2229500404146,
 "z_asimov": 4.538470894307572,
 "p_value": 0.000002897914860991712,
 "z_from_p": 4.533703776096391,
 "warnings": [],
 "claim": "significant_excess"
}
reference.py
import json
import os
import numpy as np
import pandas as pd
from scipy.optimize import minimize_scalar
from scipy.stats import norm, poisson
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

LO, HI, WLO, WHI = 100.0, 180.0, 120.0, 130.0
m = pd.read_csv("data/synth_diphoton.csv")["m_gg"].to_numpy()
win = (m >= WLO) & (m <= WHI)
sb = m[~win]
N, Nsb = int(win.sum()), int(len(sb))


def integral(lam, a, b):
    return lam * (np.exp(-a / lam) - np.exp(-b / lam))


def I_sb(lam):
    return integral(lam, LO, WLO) + integral(lam, WHI, HI)


def nll(lam):
    return float((sb / lam).sum() + Nsb * np.log(I_sb(lam)))


lam = float(minimize_scalar(nll, bounds=(5, 200), method="bounded", options={"xatol": 1e-6}).x)
B = Nsb * integral(lam, WLO, WHI) / I_sb(lam)
z_asimov = float(np.sign(N - B) * np.sqrt(2 * (N * np.log(N / B) - (N - B)))) if N > 0 else -float(np.sqrt(2 * B))
p = float(poisson.sf(N - 1, B))
z_p = float(norm.isf(p))
warnings = ["low_statistics"] if (N < 10 or B < 10) else []
res = {"n_obs": N, "n_sideband": Nsb, "lam": lam, "b_expected": float(B), "excess": float(N - B), "z_asimov": z_asimov,
       "p_value": p, "z_from_p": z_p, "warnings": warnings, "claim": "significant_excess" if z_p >= 3.0 else "no_significant_excess"}
json.dump(res, open("result.json", "w"), indent=2)
os.makedirs("figures", exist_ok=True)
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.hist(m, bins=80, range=(LO, HI), histtype="stepfilled", alpha=0.7, label="data")
ax.axvspan(WLO, WHI, color="orange", alpha=0.3, label="signal window")
ax.set_xlabel("Diphoton invariant mass m_gg [GeV]")
ax.set_ylabel("Events / 1 GeV")
ax.legend()
fig.savefig("figures/window.svg")
print(res)

Results on this task

4 runs
AgentModelRunsStrict successMean score
Self-debuggingQwen3-8B (gariyuu gateway)10%0.6464
Planner / executorQwen3-8B (gariyuu gateway)10%0.4464
ReActQwen3-8B (gariyuu gateway)10%0.3564
Single-shotQwen3-8B (gariyuu gateway)10%0.3131
RunAgentModelResultScoreLabels
20260906T171938…r0Single-shotQwen3-8B (gariyuu gateway)✗ fail0.3131no_outputplotting_errorstatistical_misuseincorrect_normalizationnon_reproducibleexecution_failed
20260906T172750…r0ReActQwen3-8B (gariyuu gateway)✗ fail0.3564statistical_misuseincorrect_normalizationunjustified_interpretationnon_reproducible
20260906T174516…r0Planner / executorQwen3-8B (gariyuu gateway)✗ fail0.4464statistical_misuseincorrect_normalizationnon_reproducible
20260906T175308…r0Self-debuggingQwen3-8B (gariyuu gateway)✗ fail0.6464statistical_misuseincorrect_normalization