Skip to content

Quick start

Install

pip install rootfig        # or: uv add rootfig

The one-liner

import rootfig as rf

rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=50)

rf.plot reads the branches referenced by the variable, the selection and the weight (nothing else), evaluates them, fills a hist.Hist and draws it. If the file contains exactly one tree you can leave tree out. The file can also be a glob ("run_*.root"), a list of files, or "file.root:tree".

A bare bins=50 infers the range from the data, ignoring far outliers so that sentinel values such as -999 do not set the axis; pass range=(low, high) to be explicit or range="auto" for the full extent (see Binning and range).

The return value is a Plot with fig, ax, hists and a save() method:

p = rf.plot("events.root", "MET", tree="events", bins=(40, 0, 400), unit="GeV")
p.ax.axvline(100, color="gray", linestyle="--")
p.save("met.pdf")

Selections and weights

rf.plot(
    "events.root",
    "Muon_pt",
    tree="events",
    selection="Muon_pt > 20 and abs(Muon_eta) < 2.5",
    weight="mc_weight * pileup_weight",
    bins=(50, 0, 200),
)

Muon_pt is jagged (a list of muons per event), so the selection masks individual muons and every muon inherits its event's weight. See Expressions and selections for the full rules.

Several samples

A list of files gives one histogram per file, with a binning shared by all:

rf.plot(
    ["signal.root", "background.root"],
    "Muon_pt",
    tree="events",
    bins=(50, 0, 200),
    normalize=True,
    ratio=True,
)

Use a mapping to name them, or Sample objects for full control:

rf.plot({"Signal": "sig.root", "Background": "bkg_*.root"}, "Muon_pt", tree="events")

Stacked MC with data and a ratio panel

mc = [
    rf.Sample("ttbar.root", tree="events", label=r"$t\bar{t}$", weight="mc_weight"),
    rf.Sample("wjets.root", tree="events", label="W + jets", weight="mc_weight"),
]
data = rf.Sample("data.root", tree="events", label="Data", is_data=True)

rf.plot(
    mc,
    "MET",
    observed=data,
    stack=True,
    ratio=True,
    logy=True,
    bins=(40, 0, 400),
    unit="GeV",
    style=rf.Style(experiment="CMS", status="Preliminary", lumi=138, com=13),
)

Histograms and arrays without plotting

h = rf.histogram("events.root", "MET", tree="events", selection="nJet >= 2", bins=(40, 0, 400))
h.values(), h.variances()  # a hist.Hist with Weight storage

arrays = rf.load(
    "events.root", ["MET", "Jet_pt", "count(Jet_pt)"], tree="events", selection="nJet >= 2"
)  # an Awkward record array

Statistics, 2D histograms, correlations

print(rf.summarize("events.root", ["MET", "Muon_pt"], tree="events", selection="nMuon > 0"))

rf.plot2d("events.root", "Muon_pt", "Muon_eta", tree="events", bins=(40, 20), logz=True)

rf.correlation("events.root", ["MET", "nJet", "HT"], tree="events")