Skip to content

Plotting options

All options below are keyword arguments of rf.plot, whether it fills from a tree, reads a histogram stored in the file or draws histogram objects you already have (see the end of this page).

Overlays, stacks and data

  • Overlay (default): every sample is drawn as an outline (histtype="step"). Use histtype="fill" for translucent filled areas, "errorbar" for points, "band" for uncertainty bands; a Sample(histtype=...) overrides per sample.
  • Stack: stack=True stacks every non-data histogram; a label or a sequence of legend labels selects what is stacked, for example rf.plot([ww, zz, zh], "mass", stack=["WW", "ZZ"]). Every histogram carrying a selected label is stacked, in input order with the first at the bottom; the order of the selectors does not matter. The stack is drawn first, then its hatched uncertainty band, then the remaining histograms as overlays in input order, then data. A histogram keeps its colour whether it is stacked or overlaid; explicit colours do not use up entries in the colour cycle. Stacked histograms are always filled: histtype=, a sample's or group's own histtype, and errorbars= apply only to overlays. False or [] overlays everything. Unknown labels and labels belonging only to observed data raise ValueError; anything other than a bool, string or sequence of strings raises TypeError. A group is stacked by its own label, not its components' labels.
  • Data: samples with is_data=True (or passed as observed=...) are points with error bars, drawn on top and never stacked, in the style's text colour (black by default) unless the sample sets color. Their error bars are √N, as ROOT draws a TH1, or Poisson intervals where data holds unit-weight counts with data_errors="auto" (Uncertainties of data).
  • Groups: a Group of samples is one histogram of the overlay or stack, the sum of its components filled apart; see Group.
  • errorbars=True adds statistical error bars to overlaid non-data histograms.

The selective-stacking example shows stacked backgrounds with a signal drawn over them and a significance panel.

Uncertainties of data

By default every histogram, data included, has ROOT's TH1 errors: √(Σw²) on both sides, √N for counts, so a plot shows the numbers ROOT shows for the same histogram. For observed counts the asymmetric Poisson (Garwood) interval is the usual convention of published data points, and one keyword gives it:

rf.plot(mc, "m_ll", observed=data, data_errors="auto")  # Poisson where data is counts
rf.plot(mc, "m_ll", observed=data, data_errors="poisson")  # Poisson, or an error if not counts

data_errors= applies to observed data in the main panel, in the lower panel and in p.uncertainty("Data"):

data_errors= Error bars
None (default) each histogram's own: √(Σw²), so an empty bin is 0 ± 0, unless it carries a model of its own (a stored TH1 saved with kPoisson or kPoisson2, or a histogram object with poisson= or stat_errors=)
"auto" a model of its own is kept; otherwise the Poisson interval for unit-weight counts and √(Σw²) for weighted or scaled data: the usual convention for data points
"sumw2" √(Σw²) for every data histogram, replacing any model it carries
"poisson" ROOT's TH1::kPoisson for every data histogram, replacing any model it carries: the Garwood 68 % interval of the counts, 0 +1.84 for an empty bin
a confidence level, 0.95 the Garwood interval at that level, replacing any model; 0.95 is ROOT's TH1::kPoisson2 (0 +3.69 for an empty bin)
  • The model is decided on each data histogram as filled or read, before normalize= and flow= change it, so normalising data keeps its error model.
  • "poisson" and a confidence level need data known to hold counts, and raise ValueError otherwise. Unweighted data filled from a tree holds them, and so does a stored TH1 without Sumw2 holding whole numbers, which ROOT itself treats as unweighted: every bin a non-negative whole number equal to its variance. Weighted data does not, nor does data scaled by a common weight: the sums of weights (Σw, Σw²) cannot tell counts scaled by one factor from unequal weights ([1, 1, 4] sums like two entries of weight 3), so no interval of counts describes them. "auto" keeps √(Σw²) for them instead.
  • Data filled from a tree is counts when every weight is 0 or 1 (no weight=, weight="1", a flag) and there is no scale, as ROOT treats a TH1 whose sum of weights equals the sum of their squares. Other weights or a scale (also for a histogram read from a file) make it weighted, judged on the weights before the selection, so an empty selection of weighted data keeps 0 ± 0 under "auto". Histograms from elsewhere (stored TH1s, histogram objects) are judged by their contents, as ROOT judges a TH1 by its sums: an empty one holds counts.
  • A stored TH1 brings the statistical error option it was saved with (TH1::SetBinErrorOption, the first file's for a sample of several, as hadd keeps it): kPoisson and kPoisson2 give it the Poisson interval at 68.27 % or 95 %, which data_errors=None shows as ROOT would. Weighted contents keep √(Σw²), as ROOT falls back to it for a weighted histogram, and so does a histogram read with a sample scale or luminosity factor, as after TH1::Scale. A histogram read with rf.io.FileSource(...).read_histogram(...) and given as an object keeps it.
  • The interval of n counts runs from L to U with P(N ≥ n | L) = P(N ≤ n | U) = 15.87 % (L = 0 for n = 0), as in ROOT. The bounds are gamma quantiles from SciPy, exact at any count, as ROOT's; poisson_interval gives them for any counts.
  • Normalising, rescaling and rebinning keep the interval, scaled like the contents: rootfig knows the counts behind every bin and their factor, so an empty bin keeps an upper error. After normalize="width" an empty bin gets 0 +1.84 divided by its own width, and an empty histogram scaled by 3 gets 0 +5.52; summed flow bins (flow="sum") get the interval of the summed count. ROOT instead falls back to √(Σw²) once a histogram is scaled. normalize=True, "density" and numeric targets divide by the histogram's own total, which fluctuates too: the bars are then the counts' interval scaled by the observed total, not an interval of the normalised shape (see normalize_uncertainty="shape" under Normalisation).
  • The lower panel propagates the two sides separately: a data/MC ratio runs from L / d to U / d, and a pull divides by the data error facing the prediction (the upper one where data lie below it).
  • rf.Histogram(h, label="Data", is_data=True, poisson=True) carries the model on a histogram of counts you pass yourself (poisson=0.95 for 95 %), so rf.compare uses it too; counts scaled by c are rf.Histogram(counts, ..., poisson=True).scaled(c). Whether a histogram holds counts is kept apart from its error model: replace(poisson=False) draws √(Σw²) and data_errors="poisson" brings the interval back, and Histogram.counts() returns the counts and their factor per bin. sum_histograms keeps the interval when every input carries the same factor per count at one level (two data periods), as TH1::Add keeps kPoisson for unweighted histograms, and the sum of data histograms is data.
  • Errors of any other origin, a fit or a bootstrap, are given as rf.Histogram(h, label="Fit", stat_errors=(down, up)), one per bin (or per cell with the flow bins). They are what errors(), the drawing and the lower panel use; scaling and normalising scale them (a negative factor, or a negative total, turns the interval over and swaps the sides), and where bins are merged (rebinning, flow="sum", sum_histograms) they add in quadrature side by side, an approximation for asymmetric errors, whose statistical meaning rootfig does not know. normalize_uncertainty="shape" refuses them (see Normalisation).

Systematic uncertainties

Samples carry their sources of systematic uncertainty, {name: variation}; systematics= on plot() and histograms() adds sources to every simulated sample (a sample's own source of the same name wins):

bkg = rf.Sample(
    "mc.root",
    tree="events",
    label="Background",
    weight="weight",
    systematics={
        "pileup": ("weight_pu_up", "weight_pu_down"),  # weights replacing Sample.weight
        "trigger": "weight_trig_up",  # one-sided: down mirrors up
        "xsec": 0.05,  # ±5 % normalisation
        "shower": (1.10, 0.97),  # (up, down) normalisation factors
        "jes": {"Jet_pt": ("Jet_pt_jesUp", "Jet_pt_jesDown")},  # branches
        "generator": rf.Systematic.samples("mc_herwig.root"),
    },
)
p = rf.plot(
    [bkg, sig], "Jet_pt", observed=data, stack=True, panel="ratio", systematics={"lumi": 0.017}
)
Form Variation
"w_up", ("w_up", "w_down") weight expression(s) replacing Sample.weight; the plot weight=, scale and luminosity scaling still multiply
0.05, (1.10, 0.97) the nominal histogram scaled by 1 ± 0.05 (a magnitude, at least 0 and below 1), or by the two positive factors, which may point either way
{"Jet_pt": ("Jet_pt_up", "Jet_pt_down")} branch names replaced by other branches in the variable, the selection and the weight, so a cut on Jet_pt moves with it; replacements are branch names, not expressions
Systematic.samples(up, down) other files or arrays with the sample's selection, weight, cross section, tree name and entry range; files look up a string ngen themselves, arrays take the nominal sample's; a Sample is used as given

Rules:

  • Every tuple is (up, down). Without a down variation, the up shift is mirrored: down = 2 × nominal − up.
  • The binning comes from the nominal values; variations fill the same axis. Weight and branch variations are evaluated on the branches read once for the nominal histogram.
  • Per bin, each source shifts the contents by up − nominal and down − nominal. The larger positive shift enters the upper uncertainty, the larger negative one the lower (so two variations moving the same way widen one side only). Different sources are independent and added in quadrature; the total is statistical ⊕ systematic, per side. That is the usual convention for an uncertainty band, not a confidence interval: a Poisson interval is no Gaussian standard deviation, so its sum in quadrature with the systematics has no exact coverage.
  • A source's name is its identity (its nuisance parameter). Sources with the same name are one source, fully correlated across samples: the stack total adds their variations linearly (a sample without the source contributes its nominal contents), and a comparison in the lower panel varies numerator and reference together, so a shared luminosity uncertainty cancels in an MC/MC ratio. Sources with different names are independent. To correlate two sources, give them one name; to keep them apart, two names. A plot-level source (systematics=) is one source shared by every simulated sample, which suits a luminosity; uncertainties independent between processes, such as their cross sections, take a name per sample: Sample(..., systematics={"xsec_zh": 0.05}).
  • A Group sums its components' variations by the same rule before it is drawn or normalised.
  • normalize=True, "unity", "density" or a numeric target normalises every variation by its own total, so the plot shows shape uncertainties; a pure normalisation uncertainty drops out. normalize="width" only divides by bin width and retains normalisation uncertainties. If the nominal cannot be normalised, its variations also stay raw. If only a variation has a zero or non-finite total, normalization raises SystematicError. flow="sum"/"show" treat variations like the nominal histogram.
  • Non-finite values are reported for each affected variation, with the source name and direction in the warning. nonfinite="error" rejects them.
  • Data samples cannot carry systematics (SystematicError), and neither can pre-filled Histogram(is_data=True); plot-level sources skip them.
  • plot2d, correlation, summarize, cutflow, efficiency, profile and histogram() (a plain hist.Hist) ignore systematics. Significance panels also use only statistical uncertainties.

Drawing follows mplhep's conventions: a stack's hatched band shows the statistical and systematic uncertainty of the total (legend Stat. + syst. unc.), overlaid samples with variations get a light band in their own colour, and the lower panel includes the systematics in its band around the baseline (split_ratio, data/MC) or in the error bars of the points (propagate: statistical uncertainties uncorrelated, systematic ones propagated source by source through the varied comparison; in a ratio or a relative difference, a variation that empties a reference bin leaves that bin's systematic uncertainty undefined, with a warning), and a pull divides by them. The automatic range covers the bulk of the band and of the systematic error bars (robust percentiles, like the points, so a single bin with a huge uncertainty runs off the panel instead of squashing it; pass panel_ylim to show it in full). A ratio stays at or above zero unless a central ratio is negative (signed weights).

The numbers are part of the result. Plot.stack holds the sum of the stacked histograms, labelled "Total" and including variations, or None without a stack. p.uncertainty() uses that total, or the sole non-data histogram if there is no stack; several overlays require a label, such as p.uncertainty("ZH"):

u = p.uncertainty()  # the stack total (p.stack), or one histogram by label
u.stat_down, u.stat_up  # statistical, per bin, visible bins
u.syst_down, u.syst_up  # systematic
u.total_down, u.total_up  # statistical ⊕ systematic
u.components["jes"]  # signed (up − nominal, down − nominal) shifts
p.histograms[0].variations  # {"jes": (hist_up, hist_down), ...}
p.comparisons[0].syst_band  # (down, up) band of the reference, relative for a ratio

Pre-filled histograms take variations directly and are drawn the same way: rf.Histogram(h, label="MC", variations={"jes": (h_up, h_down)}) passed to plot; uncertainty and sum_histograms work on them too.

Use (h_up, None) for a mirrored variation. Sample.systematics and Histogram.variations are read-only; sample.replace(systematics=...) and histogram.replace(variations=...) return copies with other ones.

Luminosity

lumi= scales simulated samples that carry a cross section (Sample(xsec=..., ngen=...)) to expected yields, xsec × lumi / ngen, and writes the luminosity into the label. Numbers are in fb⁻¹; strings carry a unit: lumi="10.8 ab^-1". Data samples and samples without a cross section are left alone. The same keyword exists on histogram(s), plot2d, summarize, correlation, cutflow, efficiency and profile. A luminosity already set on the Style wins in the label: Style(lumi=...) describes the figure, lumi= the scaling, and they need not agree (a partial dataset scaled to the full one, for instance).

Normalisation

normalize=

Value Meaning y label
False/None raw sums of weights Events or Entries / 5 GeV
True, "unity" visible bins sum to one Normalised to unity
"density" integral over the visible range is one Density
"width" divide by bin width, no rescaling Entries / GeV
a number visible bins sum to that number Normalised to 100

With any histograms stacked, normalize=True, "unity", "density" and numeric targets raise: normalising each component separately would not yield a normalised total. Use stack=False to compare shapes, rf.Group to draw a combination as one histogram, or normalize="width"; None and False also pass.

A Group is summed first and normalised as one histogram. Variances are scaled consistently. Flow bins scale with the same factor; for "width" and "density" they are divided by the width of the neighbouring visible bin. Plain hist.Hist objects with a count storage passed to plot are converted to Weight storage first. If such a histogram was filled with weights (or rescaled) its sum of squared weights is lost, and rootfig refuses it with a ValueError rather than invent uncertainties; pass variances_from_contents=True to use the absolute bin contents as variances (with a warning), or fill with hist.storage.Weight() in the first place.

Uncertainties. By default (normalize_uncertainty="scale") normalising multiplies each bin's contents by a factor and its variance by the factor squared, taking the factor as a constant, as TH1::Scale does: a normalised histogram keeps the relative uncertainties of its bins, and the Poisson intervals of data scale with the same factor as its contents. The rescaling modes take the factor from the histogram's own total, though, which fluctuates with the bins: the bins of a normalised shape are anti-correlated, and for N counts normalised to unity a bin with fraction p has variance p (1 − p) / N as a multinomial shape, not the p / N drawn. normalize_uncertainty="shape" (on plot, histograms and normalize) takes that into account:

normalize_uncertainty= A bin's uncertainty after normalize=True, "density" or a number
"scale" (default) its own, scaled with the contents
"shape" propagated to first order through the division by the fluctuating total: (g/S)² (v (1 − 2p) + p² Σv) for contents x of variance v, p = x / S, S the visible total and g the target (or one over the bin size); counts with a Poisson interval get the Clopper–Pearson interval of their fraction of the total count, at the same level, when every visible bin holds counts of one factor (counts of different factors, e.g. divided by unequal bin widths, keep the first-order variance)

A flow bin is divided by the total without entering it, which is why "shape" refuses flow="sum": added to an edge bin afterwards, the two cells would be treated as independent although they share the fluctuating total (flow="show", "hint" and "none" work). The histogram's variances are the first-order ones in every case, so a plain hist.Hist (rf.histogram) carries them; the Clopper–Pearson interval of counts is what errors(), the drawing and the lower panel use. Each bin's interval is a marginal one, bin by bin, not a simultaneous confidence region for the whole shape. The correlations between bins are in rootfig.histograms.shape_covariance(histogram, spec), the first-order covariance matrix of the normalised visible bins from the sums of squared weights (its rows sum to zero for True: the normalised bins always add up to one; for counts its diagonal is the binomial p (1 − p) / N, not the exact interval). It raises for a histogram without a shape (empty, or with weights that cancel) and for one with stat_errors, whose asymmetric errors no covariance matrix describes. "shape" refuses stat_errors too: the total enters every bin with the opposite sign, so a bin's lower error would take the other bins' upper errors, and rootfig does not know what those errors mean. "shape" needs a normalisation to the histogram's own total and raises with "width" or none. Systematic variations are normalised by their own totals either way, so a pure normalisation uncertainty drops out of the rescaling modes (Systematic uncertainties).

The rescaling modes divide by the signed sum of the visible bins: a histogram dominated by negative weights still sums to the target, its shape flips sign, and a warning says so. An empty histogram, or one whose positive and negative weights cancel exactly, is left unchanged with a warning and keeps the plain Events label (Histogram.normalization stays None).

Lower panel

panel= adds a lower panel sharing the x axis and says what it shows; reference= names the histogram it compares with. With n and d the contents of a numerator and of the reference, and vn and vd their variances (the formulas for symmetric errors; data with Poisson intervals enters each side with the error that moves the result that way):

panel= Values Error bars: propagate / numerator Band (numerator) Automatic range
"ratio" n / d, around 1 √(vn/d² + n²·vd/d⁴) / √vn / abs(d) √vd / abs(d) around 1 at least 0.5 to 1.5, widened to the bulk of the points, their systematic error bars and the band; within 0 to 3, or −3 to 3 when a ratio is negative
"relative_difference" n / d − 1, around 0 the ratio's the ratio's, around 0 the ratio's range moved down by one: at least −0.5 to 0.5, within −1 to 2 (−4 to 2)
"difference" n − d, around 0 √(vn + vd) / √vn √vd around 0 symmetric: ±1.1 times the largest magnitude of the 5th and 95th percentiles of the points, their systematic extent and the band; ±1 when all are zero
"pull" (n − d) / σ, σ² = vn + vd + σ_syst² none: 1 by construction none symmetric: ±1.1 times the 95th percentile of the magnitudes, at least ±3 and at most ±5
"asymmetry" (n − d) / (n + d), around 0 2·√(d²·vn + n²·vd) / (n + d)²; propagate only none the difference's, at most ±1.1 so that ±1 (an empty side) stays in the frame
"s/sqrt(b)", "s/sqrt(s+b)" S/√B, S/√(S+B) per bin, the reference as background statistical, propagated none 0 to 1.25 times the highest point plus its error

Ratios, relative and absolute differences and asymmetries are points with error bars and a dashed line at their baseline, over the grey reference band where one is drawn (the "numerator" mode below); pulls are filled bars from 0 in the numerator's colour; significances are points without a baseline. Every numerator gets its own series, in its histogram's colour. A bin is left empty where the value is undefined: an empty reference for a ratio or a relative difference, σ = 0 for a pull, n + d = 0 for an asymmetry, and no background (or no signal plus background) for a significance. Only the bins inside the visible x range (xlim, both segments of xbreak) set the automatic range. Statistical error bars do not widen it, so a few low-statistics bins cannot squash the panel; a significance is the exception, its upper limit following the highest point plus its error. A point beyond the range is marked by a triangle at the edge it left through, in its colour, so the robust range never hides a bin; the markers follow a later p.panel_ax.set_ylim(...).

σ_syst of a pull is the combined systematic uncertainty of n − d: a source carried by both histograms varies them together, and one carried by a single histogram varies it against the other's nominal contents. The combination is taken on the side facing the other histogram: the lower one where n > d, the upper one elsewhere (mplhep's rule for Poisson pulls). A pull is a Gaussian diagnostic of the error bars, not a likelihood (deviance) residual, so read it with care where counts are small. Significance panels use statistical uncertainties only.

Roles. Without reference=:

Drawn Ratio, differences, asymmetry, pull: numerators / reference Significance: signals / background
stack and data data / stack total; overlays are not part of the prediction overlaid non-data histograms / stack total, or, with everything stacked, the last non-data histogram / the sum of the others
stack, no data every overlaid histogram / stack total; a full stack raises as above
no stack, data data / the first non-data histogram the last non-data histogram / the sum of the others
no stack, no data every histogram after the first / the first the last non-data histogram / the sum of the others
observed data alone every data histogram after the first / the first raises: no non-data histograms

With the signal last, drawing it inside the stack or over it shows the same significance panel, and stacking the backgrounds (stack=["WW", "ZZ"]) compares every overlaid signal with them. reference="Background" names one histogram (a group's label counts): the denominator of a ratio or a relative difference, the d of n − d for a difference, an asymmetry and a pull, and the background of a significance. Every other histogram, data included, is compared with it; for a significance every other non-data histogram is a signal over it. A label that no drawn histogram or several carry raises ValueError, as does observed data as a background, or reference= without panel= or goodness_of_fit=.

Uncertainties. A ratio, relative difference or difference draws its error bars in one of two modes, chosen per numerator: data over simulation keeps the reference uncertainty as the grey band ("numerator", mplhep's split_ratio), and everything else propagates both sides ("propagate"), so sources shared by numerator and reference cancel. panel_uncertainty= applies one mode to all numerators; it raises for a pull, an asymmetry or a significance, which have no band. For a ratio (or relative difference) of two histograms of counts, panel_uncertainty="poisson-ratio" draws the exact interval of the ratio of two independent Poisson means instead of the propagated one, as ROOT's TGraphAsymmErrors::Divide(..., "pois"): the one-sigma Clopper–Pearson interval of n / (n + d) turned into one of n / d, which stays sensible for few counts and never goes below zero. Its confidence level is ROOT's default and belongs to the ratio: drawing data with data_errors=0.95 does not change it. Every compared histogram must hold known counts (unit-weight counts, or counts rootfig has scaled since; Histogram.counts()), checked before a figure is made: weighted histograms are refused rather than approximated, and keep the propagated modes. A bin with an empty reference has none. rf.compare(a, b, uncertainty="poisson-ratio") computes one.

Range and label. panel_ylim and panel_label override the automatic range and label. The label names the reference, MC for the stack total, and is worded for data only when every numerator is observed data and the reference is simulated; a named reference with simulation and data compared with it keeps the general label:

panel= Data alone over simulation Otherwise
"ratio" Data / MC Ratio to X
"relative_difference" (Data − MC) / MC Rel. difference to X
"difference" Data − MC Difference to X
"pull" Pull Pull
"asymmetry" (Data − MC) / (Data + MC) Asymmetry to X
"s/sqrt(b)", "s/sqrt(s+b)" S/√B, S/√(S+B) the same

A rotated y label is bounded by the height of the short panel, so a long one is shrunk and, if that is not enough, wrapped onto two lines; pass a shorter panel_label such as "Ratio" to keep it at full size.

Result. Plot.comparisons holds one Comparison per numerator (kind, label, reference, values, errors, edges, band, and syst_errors/syst_band with systematic uncertainties), in the order drawn; rf.compare computes one from any two histograms. Every uncertainty in it is a (down, up) pair of arrays, matplotlib's yerr order, so asymmetric errors stay asymmetric: each side of a numerator and of the reference enters with the error that moves the result the same way, which with symmetric errors is the formula in the table.

A detector or software comparison, a fast simulation against the full one, normalised to compare shapes and with the full simulation as the reference because it comes first:

full = rf.Sample("full_sim.root", tree="events", label="Full simulation")
fast = rf.Sample("fast_sim.root", tree="events", label="Fast simulation")
rf.plot([full, fast], "Muon_pt", normalize=True, panel="relative_difference")

The gallery shows a ratio to a chosen sample, a pull, a significance panel and a ratio of efficiencies.

Efficiencies and profiles. rf.efficiency and rf.profile take panel= (every kind but the significances, which count events), reference=, panel_ylim and panel_label. The roles are those without a stack: data over the first simulated sample, or every further sample over the first. Both sides are independent points, so their intervals are propagated to first order, each side entering with the error that moves the result the same way: a ratio of efficiency intervals keeps its asymmetry, and a pull divides by the errors facing the other side. There is no band and no panel_uncertainty.

Goodness of fit

goodness_of_fit=True tests how well each histogram a ratio panel would compare agrees with its reference, data with the stack total by default (the roles of a ratio, whatever panel= shows; reference= names another reference, with or without a panel), and writes the chi-square per degree of freedom and the p-value below the experiment label, after the text= lines (one line per histogram tested, headed by its label when there are several), as χ²/ndf = 1.15, p = 0.258; goodness_of_fit_format="fraction" writes χ²/ndf = 33.5/29 instead:

rf.plot(mc, "Muon_pt", observed=data, stack=True, panel="ratio", goodness_of_fit=True)
goodness_of_fit= Statistic ndf Tests Uncertainties
True, "chi2" ROOT's TH1::Chi2Test bins − 1 the shapes statistical
"chi2-absolute" rᵀ C⁻¹ r with r = n − d bins shapes and normalisation statistical and systematic
"ks" ROOT's TH1::KolmogorovTest — the shapes statistical

"chi2" is ROOT's Chi2Test, Gagunashvili's test of whether both histograms follow one shape, whatever their normalisations. How each side enters follows from what it holds, never from a guess at its sums: histograms known to hold counts (filled without weights, and scaled since, e.g. to a luminosity, as ROOT's "UU NORM"; see Histogram.counts()) are Poisson counts, every other one is weighted. Counts with counts is ROOT's "UU", counts with a weighted histogram "UW" (in either order) and two weighted histograms "WW". The edge cases are ROOT's: a bin empty on both sides is left out and lowers ndf, and an empty bin of the weighted histogram of "UW" takes the variance Σw² / Σw, which does not scale like a variance, so with such bins the result depends on how that histogram is normalised. A histogram with stat_errors is weighted and enters with their squares as its variances, and so does a stack total carrying them; asymmetric stat_errors are refused by "chi2" and "ks", as no variance describes them.

"chi2-absolute" also tests the normalisation. C is the statistical variance of n − d, each side's error taken towards the other histogram as in a pull (data drawn with Poisson intervals enters with them), and where the two agree the root mean square of its two errors, so the order of the histograms does not matter, plus δ δᵀ for each systematic source, δ the larger of its up and down shifts of n − d, signed as the up shift (as the down shift reversed where up does not move the bin): asymmetric shifts are symmetrised to the larger one, and two moving the same way do not cancel, as in the uncertainty. With mirrored shifts this is half their difference. A source is fully correlated across bins, and one carried by both histograms moves them together, so it cancels only as far as it shifts both alike: a shared luminosity uncertainty, scaling both by 1 + ε, still moves n − d by ε (n − d). Bins empty on both sides are left out, but not a bin whose weights cancel to zero, which keeps its sum of squared weights; without systematics the result is the sum of the squared pulls. It is a Gaussian approximation, poor for bins with few entries (an empty data bin has none).

"ks" is ROOT's KolmogorovTest with its default options: the largest distance between the cumulative shapes, its probability from the effective entries of both; a histogram without uncertainties is compared as a function. For binned data the p-value is biased high, the less so the finer the bins compared with the features of the distribution. Categories have no order to accumulate along, so "ks" refuses a category axis.

Inputs. The histograms are tested as filled or read, with the data uncertainty of data_errors=, before normalize=, which only changes the drawing, and in the bins drawn: the flow bins enter where flow="show" adds them or flow="sum" folds them, and xlim and xbreak restrict nothing, as they only change the view; bins= and range= choose the bins.

Result. Plot.goodness_of_fit holds one GoodnessOfFit per histogram tested, in the panel's order. It records the test and the method ("UU", "UW", "WW", "absolute", "ks"), the statistic, ndf, chi2_ndf and p_value (nan without degrees of freedom, where ROOT returns 0), which bins entered, the systematics in the covariance, and notes: ROOT's warnings that a bin holds less than one event or fewer than ten effective entries, where the approximation is poor. rf.goodness_of_fit(a, b, test=...) tests any two histograms. The gallery shows a goodness of fit of data and a stack.

Binning and range

bins takes an int, a (n, low, high) triple, a sequence of edges or a hist axis, and is shared by every sample of one plot (and by the numerator and denominator of an efficiency).

With an integer bins the range comes from range:

range Meaning
(low, high) explicit
"robust" default: the min/max of the data, ignoring values far from the bulk
"auto" the full finite minimum and maximum over all samples
rf.plot("events.root", "d0_significance", bins=50)  # robust
rf.plot("events.root", "d0_significance", bins=50, range="auto")  # full extent
rf.plot("events.root", "d0_significance", bins=50, range=(-5, 5))  # explicit

Rejected values are not discarded

A value outside the range is not removed from the data: it goes to the under/overflow, shown by the flow arrows (flow="show" turns them into visible bins, flow="sum" folds them into the edge bins). Statistics boxes and rf.summarize are computed before binning, so means and entry counts cover the full sample whichever range is used.

This happens in two steps. Outliers are rejected by their modified z-score (0.6745 * |x - median| / MAD), with a threshold of 30, which removes sentinels and anything else far from the bulk. That is done within each sample, and the ranges they keep are unioned, so a sample is judged against its own median and spread: a signal offset from a background is not an outlier merely because the background outnumbers it, and a sample keeps the same values whether it is plotted alone or in an overlay. The threshold is then tightened for as long as each step costs no more than an additional 1 percent of any one sample, by entries and by weight. Additional is meant literally: the budget is measured against what the first step already moved out of the view, which may be a good deal more than 1 percent. This second step cuts a tail that reaches far but thins out smoothly, the kind a distance threshold keeps and that leaves the interesting part of the distribution in a corner of the axis.

The budget is charged per sample rather than over the pooled entries, so a small signal sitting far from a large background keeps its own place on the axis instead of being cut as a rounding error, and it is charged against the weight a cut would remove as well as the entries, so a handful of high-weight entries is not treated as negligible. A sample with fewer than 20 distinct values is categorical — counts, flags, multiplicities — has no tail to cut and gets no budget at all, so the second step takes no value off its axis; this is decided per sample too, and holds when it is overlaid with a continuous one. If MAD is zero, the mean absolute deviation from the median is used instead. Every candidate is padded by 5 percent and clamped to the "auto" range (whose upper edge is nudged above the maximum to include it), so the range never reaches past the data. Degenerate ranges are widened symmetrically. The threshold cannot distinguish sentinels from valid data. Cases where you may want range="auto":

  • a distribution with a long tail (log-normal, Student-t, or an invariant mass with a continuum) has valid tail entries pushed into the flow bins — this is what the second step is for, so "auto" is the way to see the whole tail;
  • a sparse discrete distribution can lose rare valid values from the visible range, for example the ones in a binary sample with 999 zeros and one one;
  • a lone value far from a bulk of near-identical ones looks exactly like a sentinel and is rejected with them, however real it is;
  • xbreak=(a, b) is validated against the inferred axis, so a break meant to span a far tail needs range="auto" or an explicit range.

Inferring a robust range requires additional median and deviation calculations over the combined samples, with additional time and memory costs. An explicit range avoids range inference.

The plot is mostly empty space

The inferred range cuts a thin tail, but only as far as its coverage budget allows. A distribution whose tail carries more than that — a heavy Student-t, a steeply falling spectrum over several decades — still spreads the axis over bins holding a fraction of a percent of the peak. Three ways out, in order of how often they are what you want:

  • range=(a, b) around the core. Nothing is lost: entries outside go to the flow bins, where flow="hint" (the default) marks them with arrows and flow="sum" folds them into the edge bins.
  • logy=True, which makes the tail visible instead of hiding it.
  • xbreak=(a, b) to cut the empty middle out and keep both ends, with range="auto" or an explicit range so the break lies inside the axis.

Axes

Automatic y limits come from the bins overlapping the x range shown: xlim, or both segments of xbreak. This applies to the main panel and to the lower panel alike.

  • logx, logy: logarithmic scales. Log-spaced bins: bins=rf.log_bins(n, low, high).
  • xlim, ylim: limits; ylim=(None, 1e4) keeps the automatic lower value. Automatic y limits add a small margin above the tallest bin (a factor 1.2 in linear scale, 12 in log scale) and then raise it further as the drawn legend, label, statistics box and text lines need.
  • xbreak=(a, b): cut the range between a and b out of the x axis and draw the two remaining segments side by side with break marks, sharing the y axis (and the lower panel, if any). Useful for a peak plus a far tail or a sentinel region. The right segment is Plot.ax_right (Plot.panel_ax_right). Not available together with ax= or flow="show".

Broken x axis with a ratio panel Broken x axis with a ratio panel - flow: how under/overflow is shown (this is where entries outside an inferred range end up), "hint" (small arrows, default), "show" (extra bins labelled <low / >high, added on a side as soon as any sample has content there, identical for all samples and the lower panel), "sum" (added to the edge bins before anything is computed, so the lower panel, stack bands and y limits use the folded bins), "none". - logx, logy: logarithmic axes. logx=None (default) follows the Variable's log flag (plot2d, efficiency and profile do the same for their variables); True/False override it. - xlabel, ylabel, unit, title. The title sits above the axes, where the CMS-style label is also drawn; with such a style prefer text=. The x label ends at the right end of the axis, where matplotlib also puts the axis' offset text (×10⁻⁶ for small values); when both are shown the label moves left of it, or below it where the axis is too short for both side by side, also with axes.formatter.useoffset off, which drops an additive offset but still shows the order of magnitude. On a figure rootfig makes the place is chosen each time the figure is drawn, so a resized figure keeps the label clear for its new size; on axes passed with ax= the label goes below the offset text, which holds at any size. A labelpad or transform set on the label after plotting is kept, and the label is kept clear starting from it. - Automatic y limits leave room for the legend, the experiment label, the statistics box and text lines: a small fixed margin is added above the tallest bin, and the upper limit is then raised until none of them covers a histogram (the legend picks a free upper corner of its axes). Room is only made for what is actually drawn, so a plot without annotations keeps the margin. It is measured on the figure as laid out, as it is shown and saved. A ylim with an explicit upper value switches this off.

Legend, labels, text and statistics

  • legend=False or a location string such as "upper left".
  • style= an experiment name or a Style; see Samples, variables, cuts and styles.
  • with rf.dark_theme(): draws the figures made inside the block for a dark page — light ink on a transparent background, applied on top of any style (including experiment styles that fix a white background). Colours set in Style.rc, background and text included, are applied after it and win for the figures and axes rootfig creates, so such a style is not rendered dark. Data points and outlines follow the style's text.color. Plain matplotlib inside the block matches too: axes made with plt.subplots() for ax=, text added to Plot.ax, and fig.savefig() keeps the transparent background. Axes passed with ax= keep the properties they were created with (their background, frame and ticks), so a Style.rc passed to that plot colours only what rootfig draws into them.
  • text= extra line(s) drawn inside the frame: in the upper left corner for label_loc=0 (the CMS and DUNE default), below the secondary text for the split layout label_loc=3, and below the label for locations 1, 2, and 4.
  • A label line above the frame (experiment name, status, luminosity) that does not fit the width of the axes, next to a colour bar or over a broken x axis, is shrunk to fit, down to 60% of its original font size. If that is still too wide, the luminosity gets a separate line above the label. The luminosity of a broken x axis sits above the right end of the right segment. An explicit title is placed above these labels, preserving its font, alignment and padding.
  • The experiment name and status share a baseline, including after resizing the figure. label_loc=2 and 3 explicitly put the status on a separate line.
  • stats=True adds entries, mean and standard deviation per sample below the legend (a location string moves it).
  • goodness_of_fit=True adds ROOT's chi-square test of the compared histograms as a line below the experiment label (see Goodness of fit).

Figure handling

  • figsize=(w, h); ax=some_axes draws into your own axes (pass a pair (main, panel) with panel=), so several rootfig plots can share a figure.
  • save="file.pdf" saves immediately; Plot.save(path) accepts a directory (file named after the variable) and formats=["pdf", "png"].
  • Figures use matplotlib's constrained layout, so labels, legends and colour bars fit inside the canvas and a saved file has exactly the figsize dimensions: 1D, 2D and lower-panel plots of one size share one shape. Figures drawn into your own ax are saved with a tight bounding box instead.
  • Fonts are fixed on the figure when it is made, so saving or displaying it later renders exactly the layout that was computed, in the style's fonts.
  • Plot.fig, Plot.ax, Plot.panel_ax are plain matplotlib objects; Plot.histograms wrap the hist.Hist objects with labels and statistics.
  • In a notebook the figure is displayed automatically — it is a pyplot figure, flushed by the inline backend at the end of the cell, so %matplotlib inline is not needed. End the call with ; to hide the Plot repr, and use Plot.close() in loops that make many figures.

Large inputs

A plot reads only the branches its variable, selection, weight and systematics use, a chunk of entries (about 32 MB of arrays) at a time. The chunks are decompressed and prepared (expressions, selection, weights) in parallel threads, and only the values that fill the histograms are kept, so the peak memory follows those values rather than every branch read. The histograms and their statistics are the same as from reading everything at once.

  • Decompressing and preparing share up to eight worker threads, fewer on a machine with fewer cores. Set ROOTFIG_THREADS to choose the number, e.g. the cores a batch job was given; ROOTFIG_THREADS=1 decompresses and prepares in the calling thread. uproot fetches the file contents in a thread or two of its own either way, which do little but wait for the data.
  • Functions of your own (functions=) are called in these threads, a chunk of events at a time, so they must treat each event on its own.
  • An explicit range (bins=(50, 0, 200), or range=(low, high)) skips range inference, which takes two medians over all the values of every sample.
  • Many plots of the same files are fastest as a PlotBook, which reads each file once per batch of variables: a tree with thousands of branches costs a noticeable fraction of a second to open, every time.
  • LZMA-compressed files (the NanoAOD default) take several times longer to decompress than ZLIB, LZ4 or ZSTD ones; the threads help most there.
  • A sample whose files hold a branch in different types (float32 in one, float64 in another) is read whole once the difference shows, and evaluated in the wider type. The first file's chunks may already have been evaluated in their own type by then, so NumPy can warn about an overflow that the wider type avoids. The histograms are the same either way, also when NumPy is set to raise such errors (np.errstate(over="raise"), a warnings filter).

2D histograms and correlations

rf.plot2d(sample, x, y, bins=((40, 0, 200), (30, -3, 3)), logz=True, normalize="density")
rf.correlation(sample, ["MET", "nJet", "HT"], selection="nJet >= 2", percent=True)

rf.plot2d options: logz, logx, logy, cmap (any matplotlib colour map, default viridis), colorbar=False, zlabel (default Events or the normalisation), normalize, title, text, style, figsize, ax, save. The figure has the same size as a 1D plot; the colour bar takes its space from the axes. For 2D histograms and correlation matrices, label_loc defaults to 0, with the experiment and secondary text above the frame. Location 3 splits them: experiment above, secondary text inside. Only label_loc=1, 2, or 4 moves both inside. Luminosity remains above for locations 0–3 and inside for 4.

rf.correlation options: labels (tick labels, default the variable labels), percent=True (integer percentages instead of two-decimal coefficients), annotate=False (colours only), cmap (default RdBu_r), title, style, figsize, ax, save. The matrix is returned as Plot.matrix. Without a figsize the figure grows with the number of variables, in proportion to the style's figure width, so the larger fonts of experiment styles keep their cells readable. It is titled "<sample>: correlation"; with an experiment style the experiment label replaces that automatic title, using the placement described above. An explicit title is shown either way.

Both variables of a 2D histogram (and all variables of a correlation matrix) must share their structure: all per-event, or all per-object from the same collection.

Efficiencies

rf.efficiency([reco], "TrueMuon_pt", passed="TrueMuon_matched", bins=(20, 0, 100), unit="GeV")

For every sample the entries satisfying selection form the denominator and those also satisfying passed the numerator, with the same binning. The passing entries are a subset of all of them, so the uncertainty is that of a pass fraction, never that of two independent yields. By default the points get what ROOT's TEfficiency gives: the Clopper–Pearson interval for unweighted entries and the normal approximation for weighted ones, at one standard deviation (68.27 %, rf.ONE_SIGMA; cl=0.95 for 95 %). interval= chooses another of TEfficiency's methods, computed as ROOT computes it:

interval= Interval
"auto" (default) what ROOT's TEfficiency gives, and TGraphAsymmErrors::Divide by default: "clopper-pearson" for unweighted entries, "normal" for weighted ones
"clopper-pearson" the exact binomial interval of the counts, never covering less than 68 %; unweighted entries only
"normal" ε ± z·σ clipped to [0, 1], with σ² = (Σw²_pass (1 − 2ε) + Σw²_all ε²) / (Σw_all)² (ε(1 − ε) / n for counts); no width at 0 and 1
"wilson" the Wilson score interval of the counts; unweighted entries only. It keeps a width at 0 and 1
"jeffreys", "uniform" the Bayesian interval of the prior Beta(0.5, 0.5) or Beta(1, 1); weighted entries too
rf.Bayesian(alpha, beta, mode=False, shortest=None) any Beta prior; mode=True shows the posterior's mode rather than its mean, shortest=True the shortest interval rather than the central one (ROOT's kPosteriorMode and kShortestInterval). shortest follows mode unless given, as TEfficiency::SetPosteriorMode sets both
"agresti-coull" the normal approximation around the Wilson centre, (k + z²/2) / (n + z²); unweighted entries only
"wilson-effective" rootfig's extension of "wilson" to weighted entries: the Wilson interval of the effective entries n_eff = (Σw)² / Σw² of the denominator. Entries passing with probability ε give a weighted fraction of variance ε(1 − ε) / n_eff, and the interval inverts that. An approximation for non-negative weights: convenient, but with no guaranteed coverage for arbitrary weights. The same as "wilson" for counts

ROOT's Feldman–Cousins and mid-P options are left out: they need numerical machinery of their own for intervals that plots of efficiencies rarely use, and Clopper–Pearson (conservative) or Wilson (close to nominal coverage) serve the same purpose.

Entries count as unweighted by ROOT's aggregate test: when the sum of weights equals the sum of squared weights (to 10⁻¹², TEfficiency's tolerance for TH1D). Weights of 1, or of 0 and 1, pass it, and a weight other than 1 normally fails it; the test cannot prove unit weights (2 and eight 0.5s sum to 6 either way), and rootfig follows ROOT there too. The sample's scale and luminosity factor cancel in an efficiency and are left out, so they neither make a sample weighted nor, when zero or negative, change its efficiency. The normal approximation shrinks to nothing at 0 % and 100 %, which is why the two Z + jets bins at 100 % in the gallery have no error bar; "wilson-effective" is the alternative that keeps one for weighted samples. ROOT has no Clopper–Pearson or Wilson interval for weighted entries: asked for one, TEfficiency warns and uses the normal approximation, and so does TGraphAsymmErrors::Divide (ROOT 6.40). rootfig raises instead, so an explicit method never silently changes. The Clopper–Pearson bounds are beta quantiles from SciPy, as ROOT's, and agree with ROOT's to 10⁻¹¹.

A Bayesian interval reports the posterior as ROOT's TEfficiency does: its mean (or mode) is the efficiency, so 3 of 10 are drawn at 0.318 with "jeffreys". For weighted entries the sums are first scaled to the effective entries of the total, Σw / Σw², as ROOT does, which makes Bayesian intervals the ROOT-supported choice that keeps a width at 0 and 1 for weighted samples. With a posterior mode the point can lie outside a central interval, which then reaches one way only; the shortest interval always holds the mode. Empty bins have no efficiency and are left out; show_empty=True draws them as ROOT's "e0" option does: for unweighted entries at 0 with the interval [0, 1], or at the prior's mean with its interval for a Bayesian one; for weighted entries at 0 without a width for the normal approximation, and not at all for a Bayesian interval, whose effective entries of an empty bin are undefined ("wilson-effective", rootfig's own, spans [0, 1]). Only a bin without entries is empty: one whose signed weights cancel to a total of zero holds entries, and its efficiency stays undefined rather than drawn as 0 or as the prior. Options are the usual axis, legend, label and style ones (xlabel, ylabel, unit, title, logx, xlim, ylim, legend, text, style, figsize, ax, save); the Efficiency objects (values, lower, upper, edges) are in Plot.efficiencies. panel="ratio" adds the lower panel of a scale factor, data over simulation:

rf.efficiency([mc, data], "Muon_pt", passed="Muon_isTight", bins=(20, 0, 100), panel="ratio")

Profiles and resolutions

rf.profile(sample, "true_E", "(reco_E - true_E) / true_E", statistic="std", bins=(20, 0, 100))

statistic="mean" (default) draws the weighted mean of y per bin of x with its standard error (ROOT's TProfile); "std" draws the standard deviation with its error, the usual resolution-versus-variable plot. x and y must have the same structure; xlabel and unit describe the x axis. The Profile objects (values, errors, counts, edges) are in Plot.profiles. A lower panel compares configurations, here the resolution of a new reconstruction against the nominal one:

rf.profile(
    [nominal, new],
    "true_E",
    "(reco_E - true_E) / true_E",
    statistic="std",
    bins=(20, 0, 100),
    panel="difference",
)

Negative weights (NLO samples) can make a weighted variance negative or an efficiency leave [0, 1]. rootfig then reports nan for the standard deviation, the profile error or the confidence interval (with a warning for efficiencies) rather than a made-up uncertainty; means, yields and histogram contents are unaffected. Bins whose total weight is negative keep their mean or efficiency (the plain ratio) but get no uncertainty; bins whose weights cancel to exactly zero count as empty (nan). The normal approximation, the default for weighted entries, propagates the sums to first order, which holds for signed weights too, as in ROOT. A binomial interval (any other method) assumes non-negative weights, so with it an efficiency bin that any entry with a negative weight falls into has no interval even when its ratio lies in [0, 1]: rf.efficiency knows them from filling. The lower-level rootfig.histograms.efficiency sees only the sums of the two histograms, which reveal a negative weight when a part of the bin has a sum of squared weights above the square of its sum; its negative_weights= flags the bins the sums do not reveal. The variance of the normal approximation is never negative for a real pass/total pair (the failing entries only add squared weights to the total's). Where variances that belong to no such pair make it negative beyond round-off, the bin gets no interval, with a warning; ROOT's square root of it is nan there too. "wilson-effective" has no interval where a positive total has no variance, since no weights give that.

Cut flows

table = rf.cutflow(
    [zh, ww, zz],
    ["nMuon >= 2", rf.Cut("abs(m_ll - 91.2) < 10", label="Z window"), "recoil_mass > 120"],
    lumi="10.8 ab^-1",
)
print(table)  # yields ± error (raw events) and step efficiencies
table.get("ZH").efficiencies  # relative to the previous step
table.get("ZH").efficiency_errors  # (down, up)
table.get("ZH").absolute_efficiencies
table.get("ZH").absolute_efficiency_errors

Step efficiencies are ratios of weighted yields (nan after a zero yield). Every step keeps a subset of the events before it, so their uncertainty is that of a pass fraction, not that of two independent yields: efficiency_errors and absolute_efficiency_errors are the (down, up) distances to the confidence interval at the level cl= (one standard deviation by default) that interval= names, as for efficiencies, of the summed event weights. Every frequentist method can be named; Bayesian intervals are refused, since they would report the posterior's mean as the efficiency where a cut flow reports the ratio of its yields. The default is what TEfficiency gives for the histograms of the two steps of each efficiency: Clopper–Pearson when both steps' sums of weights equal their sums of squared weights (every weight 1, or 0 and 1: an event of weight 0 is no trial), the normal approximation otherwise. It is decided per efficiency, so after a cut that removes every weighted event the later relative efficiencies get Clopper–Pearson, while those measured against the first step keep the normal approximation. Efficiencies and their errors come from the event weights alone (CutflowStep.sum_w, sum_w2): the sample's scale and luminosity factor cancel, whatever their sign, and enter only the yields. With signed (NLO) weights a yield can be negative and a ratio can lie outside [0, 1]; the ratio is reported as is, without errors outside [0, 1], and with a binomial interval (any but the normal approximation) also without errors when measured against events that include a negative weight (CutflowStep.negative_weights), since no binomial interval describes them. Cut flows are statistical only; systematic variations are not propagated through them.

Cuts apply cumulatively; a sample's own selection is the first row. Per-object cuts pass an event when any object passes. weight, lumi and nonfinite work as in plot(): events with a missing or non-finite weight are excluded from every row (with a warning, or an error for nonfinite="error").

Statistics tables

table = rf.summarize([sig, bkg], ["MET", "Muon_pt"], selection="nMuon > 0", weight="mc_weight")
print(table)  # aligned text table
table.get("MET", "Signal").mean  # a Summary: entries, mean, std, sem, skewness, min, max

Histograms that already exist

rf.plot, rf.histogram(s) and rf.plot2d read histograms stored in ROOT files; rf.plot and rf.plot2d also draw histogram objects you already have. Each keeps its usual drawing options where they apply: the 1D options above for rf.plot (stacks, lower panels, flow, ...), those of the 2D section for rf.plot2d. Options that need information a ready-made histogram does not contain (a selection, a weight, stats on one without statistics) are refused.

Histograms already in ROOT files

Analysis frameworks often write their selections out as TH1/TH2 objects, one file per process. Name the histogram where a branch would go:

ww = rf.Sample("outputs/p8_ee_WW_ecm240.root", label="WW")
zz = rf.Sample("outputs/p8_ee_ZZ_ecm240.root", label="ZZ")
zh = rf.Sample("outputs/p8_ee_ZH_ecm240.root", label="ZH")
recoil = rf.Variable("zmumu_recoil_m", bins=(200, 120, 140), label="Recoil mass", unit="GeV")
rf.plot(
    [rf.Group([ww, zz], label="VV"), zh],
    recoil,
    stack=["VV"],
    logy=True,
    style=rf.Style(experiment="FCC-ee", com="240 GeV", lumi="5 ab^-1"),
)
h = rf.histogram("outputs/p8_ee_ZH_ecm240.root", "zmumu_recoil_m")  # a hist.Hist
rf.plot2d("outputs/p8_ee_ZH_ecm240.root", "zmumu_m_vs_recoil_m")  # a stored TH2

A Group of such samples sums their stored histograms into one, each scaled by its sample's scale and luminosity factor first. See the complete histogram-file example for an overview book and selected plots from histogram files or ntuples.

The decision is made per call and is deterministic. A variable that is a bare name is read as a stored histogram when every sample reads files without an explicit tree= or entry range, the first file of each sample holds a TH1 or TH2 of that name, and the file has no tree or its only tree has no branch of that name. Anything else fills from the tree as usual: an explicit tree= always means a branch, a branch of the same name wins over a histogram, a file with several trees next to the histogram raises (pass tree= for a branch, or read the histogram with FileSource.read_histogram), and samples that disagree raise with the reason per sample. A histogram inside a directory is named by its path in backticks, like a branch with odd characters: rf.plot("histo.root", "`selection/mz`"). rf.PlotBook(file, rf.ALL) discovers every stored TH1 of a file, and every branch of a tree whose values are numbers or booleans (lists and fixed-size arrays of them included; strings and records are not), from the same metadata; see Automatic variable discovery.

What a stored histogram supports:

  • The files of one Sample are summed (they must agree on the binning); scale, color, is_data, histtype and the luminosity scaling (Sample(xsec=..., ngen=...) with lumi=, ngen may name a TParameter or a sum-of-weights histogram in the same file) apply as for trees. A file without a tree has no entries to count, so it needs ngen.
  • The stored axis title is the x label unless xlabel=/a Variable label is given; a unit= is appended to it and, as for trees, feeds the bin-width y label (Events / 5 GeV). A title that ends in [unit] already supplies it. A placeholder title (none, ROOT's xaxis, uproot's Axis 0) gives way to the variable's name. The y axis of a TH2 named by a single variable gets no label from that name, which describes the histogram rather than the axis; as for any unlabelled axis, hist shows the axis name (mz_recoil_2D_y) instead. To label it, pass a Variable for y that names the same histogram, since both axes must resolve to one stored object: rf.plot2d(f, "my_hist", rf.Variable("my_hist", name="recoil", label="Recoil")). A Variable with another expression asks for a branch instead.
  • bins= and range= crop and merge the stored bins as described below. Every requested edge must coincide with an existing edge; the range can shrink but cannot grow. Content and variances outside the range join the flow bins. A crop requires the flow bin on each cropped side. Crop and rebin before normalising; asking for the bins already present is a no-op.
Specification Filling a tree Stored or ready-made histogram
bins=None, range unset, "auto" or "robust" DEFAULT_BINS over an inferred range Keeps its binning
bins=20, range unset, "auto" or "robust" 20 bins over an inferred range Merges the whole axis to 20 bins; the count must divide its size
bins=(20, 120, 140), bins=20, range=(120, 140), explicit edges, or a Regular/Variable axis Fills those bins, with entries outside in flow bins Crops and merges to those edges, with contents outside in flow bins
bins=None, range=(120, 140) DEFAULT_BINS between 120 and 140 Keeps its own bins between those edges

bins= accepts only Regular and Variable axes; integer, boolean and category axes must be expressed as a count/range or numeric edges instead. - Normalisation, stacks, lower panels, flow and the other drawing options work unchanged. Systematics of the normalisation kind ({"lumi": 0.02}) and Systematic.samples(other_files) (the same histogram read from other files) are supported by plot and histograms; plot2d ignores systematics, for stored histograms as for trees. - selection=, weight= (on the call or the Sample), nonfinite="error", weight and branch-replacement systematics and stats= need event data and raise with a message that says so. - A TH1 written with Sumw2 keeps its uncertainties; one without it arrives with its bin contents as variances (uproot cannot know the weights). As observed data, one without Sumw2 holding whole numbers is taken as unweighted counts, which data_errors="poisson" or "auto" give Poisson intervals. A TH1 saved with the error option kPoisson or kPoisson2 brings its Poisson interval, as ROOT draws it (data or simulation alike). Negative contents without Sumw2 leave no usable variances: rootfig refuses them unless variances_from_contents=True takes the absolute contents, as for histogram objects. The files of one sample may mix both kinds and may differ in their axis titles; the sum has Weight storage and the first file's titles.

Histogram objects

hist.Hist or Histogram objects, one or a list, are drawn as they are: rf.plot([h_sig, h_bkg], label=["Signal", "Background"], panel="ratio"). label= names plain hist.Hist objects (otherwise their first axis name is used), observed= takes histogram objects for the data, variable= optionally supplies the axis label, unit and log flag, and rf.plot2d(h2) draws a 2D one, with x and y Variables optionally describing its axes the same way (rf.plot2d(h2, rf.Variable("mass", label="Mass", unit="GeV"), rf.Variable("recoil", bins=6)); a name= renames the axis, the histogram you passed is left untouched). As for stored histograms, bins= (given directly or on the Variable) crops and merges bins: an integer count, or edges that coincide with the existing ones, so the Variable a histogram was filled with can be passed along with it (rf.plot(rf.histogram(sample, pt), pt)), also after normalising it, as long as it asks for the bins the histogram has. An explicit (low, high) range without a bin count crops to those ends, which must be existing edges, keeping the bins between them and moving the rest into flow. Options that fill from event data (tree, selection, weight, lumi, systematics) raise; range="auto"/"robust" are no-ops. Histogram.variations carries systematics instead. Stacks, sums and comparisons of histograms with category axes (ROOT bin labels) require the same categories in the same order; the flow bins of such an axis hold entries of categories it does not list, which flow="hint" marks with an arrow and flow="show"/flow="sum" refuse, since they have no bin beyond the last category.