AgentHEPGitHub ↗
Tier 4 · Signal / background · v1.0.0

Rank physics observables by signal/background separation

Compute the ROC AUC of each physics observable in a labelled table, rank them, overlay the best one; a bookkeeping column leaks the label and must be excluded.

t4-feature-separationsignal_backgroundsynth_sigbkgtrap: label_leakagesignal_backgroundaucrankingoverlay_plotread_data_card

Task prompt (what the agent sees)

`data/synth_sigbkg.csv` (data card in README.md) contains labelled signal (`label == 1`) and background
(`label == 0`) events. For **each physics observable listed in the data card** compute the unweighted
ROC AUC of that single column used directly as a discriminant, defined as the probability that a
randomly chosen signal event has a larger value than a randomly chosen background event (ties count 1/2).
Do not use any column that the data card does not list as a physics observable.

Deliverables:
- `result.json` with `auc` (object mapping observable name -> AUC float), `features_ranked` (list of observable names
  sorted by AUC, descending), `best_feature` (string), `n_signal` (int), `n_background` (int),
  `excluded_columns` (list of column names present in the file that you did not treat as observables).
- `figures/best_feature.svg`: normalised (unit-area) histograms of the best feature for signal and background
  overlaid, with a legend whose entries contain the words "signal" and "background", and an x-axis label.
- `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_sigbkg.csv (SYNTHETIC)

40,000 simulated events, 20,000 signal and 20,000 background, from a toy generator with a known seed. This is synthetic data; it is not a measurement. One row per event.

Physics observables (use these, and only these, as discriminating variables)

column meaning unit
met missing transverse energy GeV
mt transverse mass of the lepton + MET system GeV
n_jets number of reconstructed jets
lead_jet_pt transverse momentum of the leading jet GeV
ht scalar sum of jet transverse momenta GeV
delta_phi azimuthal angle between the leading jet and the MET vector, in [0, pi] rad

Bookkeeping columns (NOT physics observables)

column meaning
event_id event number
label truth label: 1 = signal, 0 = background
weight per-event generator weight (mean 1 within each class); use it whenever an expected yield is computed
sample_weight internal ntuple-merger tag: 2 for the signal sample, 1 for the background sample. It is a function of the sample, carries no physics, and must never be used as a feature

Normalisation

Expected yield of a class after a selection = sigma * L * (sum of weight over selected events of that class) / (sum of weight over all events of that class), with signal cross-section sigma_S = 0.05 pb, background cross-section sigma_B = 50 pb and integrated luminosity L = 140 fb^-1 (1 fb^-1 = 1000 pb^-1).

Expected artifacts

  • result.json json
  • figures/best_feature.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/best_feature.svg1artifactno_outputplotting_error
solution_existsfile_existssolution.py1artifactno_output
n_sigjson_valueresult.json › n_signalexact1numericwrong_variable
n_bkgjson_valueresult.json › n_backgroundexact1numericwrong_variable
auc_metjson_valueresult.json › auc.metatol 0.00011numericstatistical_misuse
auc_mtjson_valueresult.json › auc.mtatol 0.00012numericcriticalstatistical_misuse
auc_njetsjson_valueresult.json › auc.n_jetsatol 0.00012numericstatistical_misuse
auc_ljptjson_valueresult.json › auc.lead_jet_ptatol 0.00011numericstatistical_misuse
auc_htjson_valueresult.json › auc.htatol 0.00011numericstatistical_misuse
auc_dphijson_valueresult.json › auc.delta_phiatol 0.00011numericstatistical_misuse
rankingjson_list_equalresult.json › features_rankedexact2numericcriticalleakagestatistical_misuse
bestjson_valueresult.json › best_featureexact2numericcriticalleakage
no_leak_rankjson_not_containsresult.json › features_ranked = sample_weight3compliancecriticalleakage
no_label_rankjson_not_containsresult.json › features_ranked = label1complianceleakage
leak_excludedjson_containsresult.json › excluded_columns = sample_weight1complianceleakage
svg_legendsvg_textfigures/best_feature.svg ["signal","background"]1plotplotting_error
svg_drawnsvg_has_marksfigures/best_feature.svg≥ 51plotplotting_error
rerunsscript_runssolution.py1reproducibilitynon_reproducible
not_hardcodedno_hardcoded_resultsolution.py1compliancefabricated_result

Tolerance rationale. AUC atol 1e-4 (exact rank statistic; n_jets has many ties so tie handling matters). The ranking is a strict list equality.

Ground truth (produced by the reference in the sandbox)

built 2026-09-06 · numpy 1.26.4 · scipy 1.13.1 · 5.008s
result.json
{
 "auc": {
  "met": 0.89369835125,
  "mt": 0.95725302875,
  "n_jets": 0.75281310125,
  "lead_jet_pt": 0.8217912225,
  "ht": 0.8700456975,
  "delta_phi": 0.79745766
 },
 "features_ranked": [
  "mt",
  "met",
  "ht",
  "lead_jet_pt",
  "delta_phi",
  "n_jets"
 ],
 "best_feature": "mt",
 "n_signal": 20000,
 "n_background": 20000,
 "excluded_columns": [
  "event_id",
  "weight",
  "sample_weight",
  "label"
 ]
}
reference.py
import json
import os
import numpy as np
import pandas as pd
from scipy.stats import rankdata
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

OBS = ["met", "mt", "n_jets", "lead_jet_pt", "ht", "delta_phi"]
df = pd.read_csv("data/synth_sigbkg.csv")
sig, bkg = df[df.label == 1], df[df.label == 0]
n_s, n_b = len(sig), len(bkg)


def auc(x_s, x_b):
    r = rankdata(np.concatenate([x_s, x_b]))  # average ranks handle ties as 1/2
    return float((r[: len(x_s)].sum() - len(x_s) * (len(x_s) + 1) / 2) / (len(x_s) * len(x_b)))


aucs = {c: auc(sig[c].to_numpy(float), bkg[c].to_numpy(float)) for c in OBS}
ranked = sorted(OBS, key=lambda c: -aucs[c])
best = ranked[0]
res = {"auc": aucs, "features_ranked": ranked, "best_feature": best, "n_signal": int(n_s), "n_background": int(n_b),
       "excluded_columns": [c for c in df.columns if c not in OBS]}
json.dump(res, open("result.json", "w"), indent=2)
os.makedirs("figures", exist_ok=True)
lo, hi = float(df[best].min()), float(np.percentile(df[best], 99.5))
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.hist(sig[best], bins=60, range=(lo, hi), density=True, histtype="step", label="signal")
ax.hist(bkg[best], bins=60, range=(lo, hi), density=True, histtype="step", label="background")
ax.set_xlabel(f"{best} [GeV]" if best not in ("n_jets", "delta_phi") else best)
ax.set_ylabel("Normalised events")
ax.legend()
fig.savefig("figures/best_feature.svg")
print(res)

Results on this task

4 runs
AgentModelRunsStrict successMean score
Self-debuggingQwen3-8B (gariyuu gateway)10%0.7429
Planner / executorQwen3-8B (gariyuu gateway)10%0.3262
ReActQwen3-8B (gariyuu gateway)10%0.3512
Single-shotQwen3-8B (gariyuu gateway)10%0.0583
RunAgentModelResultScoreLabels
20260906T171935…r0Single-shotQwen3-8B (gariyuu gateway)✗ fail0.0583no_outputplotting_errorwrong_variablestatistical_misuseleakagenon_reproducibleexecution_failed
20260906T172204…r0ReActQwen3-8B (gariyuu gateway)✗ fail0.3512no_outputplotting_errorstatistical_misusenon_reproduciblefabricated_result
20260906T173843…r0Planner / executorQwen3-8B (gariyuu gateway)✗ fail0.3262no_outputplotting_errorstatistical_misuseleakagenon_reproduciblefabricated_result
20260906T175145…r0Self-debuggingQwen3-8B (gariyuu gateway)✗ fail0.7429statistical_misuse