18.5. Creating Histograms
Table of Contents
`Histo1D()`
When using RDataFrame, histograms are not created by calling the TH1 constructor directly. Instead, you ask the dataframe to produce histograms from its columns. The method Histo1D() is the standard way to create one dimensional histograms from a column or an expression.
A minimal example starts from a dataframe, typically built from a TTree:
ROOT::RDataFrame df("tree", "file.root");
auto h_pt = df.Histo1D("pt");
Here "pt" is the name of a column in the dataframe, often corresponding to a TTree branch. The result h_pt is an RResultPtr to a TH1 object. It behaves like a smart pointer and the actual histogram is only filled when needed, for example when you draw it or access its contents.
You can also specify the histogram model explicitly. This controls the number of bins, axis range, and optionally the title:
ROOT::RDataFrame df("tree", "file.root");
// Explicit binning: 100 bins from 0 to 200
auto h_pt = df.Histo1D(
{"h_pt", "Transverse momentum; p_{T} [GeV]; Events", 100, 0.0, 200.0},
"pt"
);
The first argument here is a ROOT::RDF::TH1DModel or similar, constructed implicitly from the initializer list. The second argument is the column or expression to be filled. The histogram name "h_pt" is used when saving to a ROOT file, and the title sets both the main title and axis titles through the usual ROOT semicolon convention.
You can also fill from an expression instead of a simple column name. RDataFrame expressions are given as strings in C++ syntax and can combine existing columns:
auto h_mass = df.Define("m2", "E*E - px*px - py*py - pz*pz")
.Histo1D({"h_m2", "Invariant mass squared; m^{2}; Events", 80, -1.0, 3.0},
"m2");
Here Define creates a new column "m2" which is then used by Histo1D(). The key point is that Histo1D() always takes a single value per event, either directly from a column or from a defined expression.
Often you want to account for per event weights. For that there is a second parameter to Histo1D() that specifies the weight column:
auto h_weighted = df.Histo1D(
{"h_weighted", "Weighted distribution; x; Weighted events", 50, 0.0, 5.0},
"x",
"weight"
);
In this case, every entry contributes to the histogram with a weight given by the value of the "weight" column. Both value and weight columns must be of compatible numeric types.
The interaction with lazy evaluation is important. Calling Histo1D() does not immediately loop over all entries. The loop runs only when the result is accessed. For example:
auto h = df.Histo1D("pt");
h->Draw(); // triggers the event loop and fills the histogramAfter the draw call, the histogram is materialized and you can query its properties or save it:
std::cout << "Entries: " << h->GetEntries() << "\n";
TFile out("histos.root", "RECREATE");
h->Write();Because RDataFrame supports multiple independent actions in one pass over the data, you can ask for several histograms and benefit from a single event loop:
auto h_pt = df.Histo1D({"h_pt", "p_{T}", 100, 0, 200}, "pt");
auto h_eta = df.Histo1D({"h_eta", "#eta", 60, -3, 3}, "eta");
// First access to any result triggers filling of both
h_pt->Draw();The binning choices follow the same principles as for standard TH1 objects. Very fine binning on large datasets increases memory usage and reduces statistical power per bin, while too coarse binning hides structure in the data. Use the knowledge of your variables and number of entries to choose appropriate binning.
If you later need to normalize or scale the histogram, you do it on the resulting TH1, not through Histo1D():
auto h = df.Histo1D({"h_x", "x; x; Normalized events", 50, 0, 10}, "x");
h->Scale(1.0 / h->Integral()); // unit normalization
Remember that Histo1D() creates a histogram as a lazy action. The underlying data loop runs only when you first access the histogram result, for example via Draw(), Write(), or numerical queries. This makes it efficient to define many histograms and other actions that are executed together in a single pass over the data.
`Histo2D()`
For two dimensional histograms, RDataFrame provides Histo2D(). Instead of a single value per event, you now provide a pair of values, one for the X axis and one for the Y axis. As with Histo1D(), you work with column names or expressions, and the method returns an RResultPtr to a TH2 object.
A simple example uses two existing columns, for example "pt" and "eta":
ROOT::RDataFrame df("tree", "file.root");
auto h2_pt_eta = df.Histo2D("pt", "eta");In this simplest form, ROOT uses default binning based on the column types, which is rarely what you want for real analyses. Explicit binning is normally preferable:
auto h2_pt_eta = df.Histo2D(
{"h2_pt_eta",
"p_{T} vs #eta; p_{T} [GeV]; #eta; Events",
100, 0.0, 200.0, // X axis: pt
60, -3.0, 3.0}, // Y axis: eta
"pt",
"eta"
);The first argument is a TH2 model which defines the histogram name, title, number of bins, and ranges in both dimensions. The second and third arguments are the X and Y value sources. As for 1D histograms, these can be either simple column names or more complex expressions.
For expressions, you can use the same Define mechanism to build derived quantities before histogramming:
auto df2 = df.Define("pt_over_mass", "pt / mass")
.Define("logE", "log(E)");
auto h2 = df2.Histo2D(
{"h2_ptoM_logE",
"p_{T}/m vs log(E); p_{T}/m; log(E); Events",
80, 0.0, 8.0,
80, 0.0, 10.0},
"pt_over_mass",
"logE"
);If each event has an associated weight, you can pass a weight column as a third data argument:
auto h2_weighted = df.Histo2D(
{"h2_xy_w",
"Weighted 2D distribution; x; y; Weighted events",
50, 0.0, 5.0,
50, -2.0, 2.0},
"x",
"y",
"weight"
);
Here each event contributes weight to the bin that corresponds to (x, y). The behavior is analogous to Histo1D() with weights.
The same lazy evaluation rules apply. Defining Histo2D() does not immediately loop over the dataset. The loop happens on first use of any result:
auto h2 = df.Histo2D({"h2", "X vs Y; X; Y; Events", 40, 0, 10, 40, -5, 5},
"X", "Y");
// Trigger event loop by drawing the histogram
TCanvas c("c", "c", 800, 600);
h2->Draw("COLZ");
c.SaveAs("xy_distribution.png");After this, the TH2 is fully filled and can be further manipulated or saved to a file.
Binning and axis ranges in two dimensions require particular attention. Too narrow a range along one axis may cut away events of interest or move many entries into underflow and overflow bins, which can distort the visual impression. Conversely, a range that is too wide can waste bins on empty regions. It is often useful to build a quick coarse histogram with generous ranges to explore the variable space, then refine the binning for the final analysis.
As with 1D histograms, multiple 2D histograms and other actions can be defined on the same dataframe. RDataFrame then processes all of them in a single pass, which is particularly beneficial for large datasets:
auto h2_pt_eta = df.Histo2D({"h2_pt_eta", "", 80, 0, 200, 60, -3, 3}, "pt", "eta");
auto h2_pt_phi = df.Histo2D({"h2_pt_phi", "", 80, 0, 200, 64, -3.2, 3.2}, "pt", "phi");
auto h2_eta_phi = df.Histo2D({"h2_eta_phi", "", 60, -3, 3, 64, -3.2, 3.2}, "eta","phi");
// Accessing any of the histograms triggers a single event loop producing all three
h2_pt_eta->Draw("COLZ");
Once created, 2D histograms obtained from Histo2D() behave like standard TH2 objects. You can compute projections, profiles, or perform fits using the regular ROOT tools that operate on histograms.
Views: 12
KAHIBARO