Samples, variables, cuts and styles¶
rf.plot accepts plain strings everywhere, which is all you need for a quick
look. Analysis scripts that make dozens of plots from the same inputs are
clearer when the pieces are named once. Five small frozen dataclasses do
that; none of them opens a file or holds data.
Sample¶
Where data comes from, how it is labelled, and how it is drawn.
sig = rf.Sample("sig_*.root", tree="events", label="Signal", weight="mc_weight", color="tab:red")
bkg = rf.Sample(
["bkg_a.root", "bkg_b.root"],
tree="events",
label="Background",
selection="passTrigger",
weight="mc_weight",
scale=0.98,
)
data = rf.Sample("data.root:events", label="Data", is_data=True)
mem = rf.Sample({"x": awkward_array, "w": weights}, label="in memory")
datamay be a path, glob,"path:tree", a list of those, a mapping of arrays, an Awkward record array, a NumPy structured array, or any object implementing theSourceprotocol.- A NumPy
.npzfile (np.savez) is read as one branch per array (NpzSource):rf.plot("events.npz", "Muon_pt", bins=50). A flat array is per-event, an(events, n)array per-object; arrays of no dimension (a scalar saved beside the events) are not branches, and object arrays (ragged lists) are refused, since NumPy loads them only by unpickling. selectionandweightbelong to the sample and combine with the ones given toplot()(&and*respectively).is_data=Truedraws points with error bars (in the style's text colour unlesscoloris set) and keeps the sample out of stacks. In a ratio, relative difference, difference or pull panel it is compared with the prediction by default;reference=can name it as the reference instead, and a significance never takes data as a signal (see the roles).xsecandngendescribe simulated processes: the cross section (pb, or a string with a unit such as"1.2 fb") and the number of generated events (a number, the name of an object in the file holding it, e.g."eventsProcessed"TParameteror a sum-of-weights histogram, orNonefor the number of entries). Withlumi=given toplot(),cutflow(),summarize(), ... every weight is multiplied byxsec × lumi / ngen:
zh = rf.Sample("p8_ee_ZH_ecm240.root", label="ZH", xsec="0.201 pb", ngen="eventsProcessed")
ww = rf.Sample("p8_ee_WW_ecm240.root", label="WW", xsec="16.4 pb", ngen="eventsProcessed")
rf.plot([ww, zh], "recoil_mass", lumi="10.8 ab^-1", stack=True) # expected yields
Without a luminosity such samples raise a LuminosityError.
The same samples can point at files of ready-made histograms instead of
trees; naming the histogram in place of the branch reads it (see
stored histograms).
- entry_start/entry_stop restrict reading for quick looks at large files
(a plot reads every file of the sample, a chunk of entries at a time). For a
ready-made FileSource, give the range to the source itself; passing it to
Sample afterwards raises a SourceError.
- systematics={name: variation} lists the sample's sources of systematic
uncertainty: weight expressions (("w_up", "w_down")), normalisation
uncertainties (0.05, (1.1, 0.95)), varied branches
({"Jet_pt": ("Jet_pt_up", "Jet_pt_down")}) and varied files
(Systematic.samples). Sources with
the same name are correlated across samples; see
Systematic uncertainties.
- sample.replace(label="...") returns a copy with the given fields changed; new values are
validated like constructor arguments.
Passing a list of files to plot() creates one sample per file. To merge
several files into one sample, use a glob or a Sample.
Group¶
Several samples drawn as one histogram.
ww = rf.Sample("ww.root", label="WW", xsec="16.4 pb", ngen="eventsProcessed")
zz = rf.Sample("zz.root", label="ZZ", xsec="1.4 pb", ngen="eventsProcessed")
vv = rf.Group([ww, zz], label="VV", color="C0")
rf.plot([vv, zh], "recoil_mass", lumi="10.8 ab^-1", stack=True) # two histograms: VV and ZH
- The components are filled independently, each with its own source, selection, weight, scale, cross section and systematics, and their histograms are summed afterwards. Every sample of a plot, grouped or not, takes part in the shared binning.
- Systematic sources with the same name in several components are correlated
and add linearly; a component without a source contributes its nominal
histogram; different names stay independent. These are the rules of
Systematic uncertainties, applied to
the sum.
normalize=also applies to the sum. label,colorandhisttypedescribe the summed histogram. The components' own are not used;Nonekeeps the style defaults.- A group is all observed data or all simulation; mixing the two raises a
ValueError. A data group is drawn like any data sample and may be given asobserved=. - Groups nest:
rf.Group([vv, other], label="Background").group.componentsare the direct members andgroup.samplesevery leaf sample, sorf.plot(group.components, ...)andrf.plot(group.samples, ...)draw the parts separately. - Only
rf.Group(...)makes a group. A list of paths given toplot()is still one sample per path, andSample([...])one sample over several files. - The summed histogram has no
sampleand no unbinned statistics:Histogram.entriesisNone, andstats=Trueleaves it out of the statistics box, or raises when no histogram of the plot has statistics. group.replace(label="...")returns a validated copy.
plot(), histograms() and histogram() accept groups; summarize(),
cutflow(), efficiency(), profile(), correlation(), plot2d() and
load() take samples and point you to group.samples.
Variable¶
What to histogram and how to present it.
pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
met = rf.Variable(
"MET / 1000", bins=40, range="auto", label=r"$E_T^{miss}$", unit="TeV", log=True, name="met"
)
bins: anint(range inferred from the data),(n, low, high), a sequence of edges (e.g.rf.log_bins(30, 1, 1000)), or ahist.axis.Regular/Variable. Applied to a histogram that already exists (stored in a file, or filled earlier and passed back with the variable), anintmerges its bins down to that count and explicit edges must coincide with its own. The histogram is merged and cropped to the requested edges, with the rest in flow bins; see Histograms that already exist.range:(low, high),"robust"(the default: ignores far outliers such as-999sentinels and cuts a thin tail, both of which then land in the under/overflow) or"auto"(the finite min/max over all samples). See Binning and range.labelandunitform the axis labellabel [unit]; the unit also appears in the automatic y label (Events / 4 GeV).nameis used for file names byPlot.save(directory)andPlotBook; it must be a file name component on every platform: no slash, control character or<>:"|?*, no trailing dot or space, and not a Windows device name such asCON(check_file_stem). Without it the expression is reduced to one withsafe_file_stem:Muon_pt / 1000names its filesMuon_pt_1000, andCONnames themCON_.
bins, range, xlabel and unit given to plot() override the variable.
Cut¶
base = rf.Cut("nMuon >= 1", label="1 muon")
sr = base & "abs(Muon_eta) < 2.5" & ~rf.Cut("isCosmic")
See Expressions and selections.
Style¶
Appearance, applied only while a figure is drawn (global matplotlib state is
untouched unless you call rf.use_style).
atlas = rf.Style(
experiment="ATLAS", status="Internal", lumi=140, com=13.6, text=[r"$Z \to \mu\mu$ selection"]
)
neutral = rf.Style(
figsize=(6, 5), legend="upper left", colors=["#1b9e77", "#d95f02"], rc={"font.size": 12}
)
experimentselects the matching mplhep style sheet and label helper (ATLAS, CMS, LHCb, ALICE, DUNE); any other name still gets a label with the neutral style. Nothing is drawn unless you ask for it.status,lumi,com,textfill the label;simulationcontrols the "Simulation" word (default: shown when there is no data sample; a status that already contains it is not doubled).lumiandcomare numbers inlumi_unit(default fb⁻¹) andcom_unit(default TeV), or strings with their own unit:Style(experiment="FCC-ee", com="240 GeV", lumi="10.8 ab^-1"). The energy is shown only whencomis given.basecan be any mplhep or matplotlib style name ("ATLAS","ggplot", ...) or a mapping of rcParams;rcadds overrides on top;colorsreplaces the colour cycle.label_locoverrides the experiment's convention: 0 puts the experiment and secondary text above the frame; 3 puts the experiment above and secondary text inside; 1, 2, and 4 put both inside. Luminosity stays above for locations 0–3 and inside for 4. 2D histograms and correlation matrices default to location 0; onlylabel_loc=1,2, or4moves the experiment inside the frame.- The centre-of-mass energy and luminosity appear only when
com/lumiare given; nothing is invented for you. legendisTrue,Falseor a location string;legend_kwargsare forwarded toAxes.legend.
A bare string is accepted too: style="CMS".
Putting it together¶
samples = [bkg, sig]
variables = [pt, met, rf.Variable("nMuon", bins=(8, -0.5, 7.5))]
for var in variables:
p = rf.plot(samples, var, observed=data, selection=sr, stack=True, panel="ratio", style=atlas)
p.save("plots/") # plots/Muon_pt.pdf, plots/met.pdf, plots/nMuon.pdf
p.close()
The same loop over several selections and drawing variants, with deterministic
file names, is what rf.PlotBook does:
rf.PlotBook(
samples,
variables,
selections={"baseline": base, "sr": sr},
variants={"lin": {}, "log": {"logy": True}},
plot_kwargs={"observed": data, "stack": True, "panel": "ratio", "style": atlas},
).save("plots/") # plots/Muon_pt__sr__log.pdf, ...