6.2. Filling 2D Histograms
Table of Contents
Filling X-Y data
A two dimensional histogram stores counts as a function of two variables, for example energy versus time or position $x$ versus $y$. In ROOT, classes such as TH2F and TH2D represent these histograms. To use them effectively, you must understand how to fill them with pairs of values.
The basic method is the Fill member function. For a 2D histogram h2, the simplest form is
h2->Fill(xValue, yValue);
This call finds the bin whose $x$ range contains xValue and whose $y$ range contains yValue, then increases that bin content by 1. This is completely analogous to TH1::Fill, but with an extra coordinate. Just as with 1D histograms, there is also a weighted version,
h2->Fill(xValue, yValue, weight);
which adds weight to the bin instead of 1. This is crucial when events carry different statistical weights, for example when correcting for detector efficiency or when using simulated events with event weights.
The mapping from numeric coordinates to bins uses the axis definitions that you chose when you created the histogram. If you created
TH2F *h2 = new TH2F("h2", "X vs Y",
50, 0.0, 5.0, // X axis: 50 bins from 0 to 5
40, -2.0, 2.0); // Y axis: 40 bins from -2 to 2
then any call to Fill(x, y) will place the pair (x, y) into one of $50 \times 40$ interior bins, or into the underflow or overflow along one or both axes if the values lie outside the specified ranges.
It is often useful to know which bin a given coordinate will fall into. For this, use the axis helpers:
Int_t binx = h2->GetXaxis()->FindBin(xValue);
Int_t biny = h2->GetYaxis()->FindBin(yValue);
Each of these returns an integer bin index, including underflow and overflow. For 2D histograms, there is also a global bin number that corresponds to a particular (binx, biny) pair:
Int_t globalBin = h2->GetBin(binx, biny);
Although you normally just use Fill, understanding the bin indices is helpful when you later read or modify the bin contents directly.
By default, both coordinates are treated symmetrically. ROOT does not assign any special meaning to the X or Y axis beyond the labels you give them. In practice, however, it is good style to keep a consistent convention, for example using X for the more fundamental or independent variable and Y for the response or dependent variable. This consistency makes your plots easier to interpret.
If your X and Y values are stored in parallel C++ arrays or std::vectors, you will typically loop over them and call Fill once per pair. For example:
std::vector<double> xs, ys;
// fill xs and ys with your data, same length N
for (size_t i = 0; i < xs.size(); ++i) {
double x = xs[i];
double y = ys[i];
h2->Fill(x, y);
}The crucial point is that each entry in the 2D histogram corresponds to one observation of the pair $(x, y)$, not to independent counts in X and Y. Keeping the one to one pairing of values is essential for a correct 2D distribution.
Important rule: Always call Fill(x, y) with the correct paired values from the same event or measurement. Never mix X values from one source with Y values from another, or you will destroy genuine correlations and create misleading patterns in the 2D histogram.
Weights can also come from arrays or be computed on the fly. For example, if each event has an associated weight w[i], then:
for (size_t i = 0; i < xs.size(); ++i) {
h2->Fill(xs[i], ys[i], w[i]);
}This is the standard way to encode variable statistical importance of events into a 2D histogram.
Event-by-event filling
In real analyses, you typically fill 2D histograms directly inside an event loop. Often the event data are stored in a TTree or come from a custom data structure in your code. In both cases, the logic is similar: for every event, compute or read the relevant X and Y quantities, apply any selection criteria, and then call Fill(x, y).
A common pattern with TTrees is:
TFile *f = TFile::Open("data.root");
TTree *t = (TTree*)f->Get("tree");
float x, y;
t->SetBranchAddress("x", &x);
t->SetBranchAddress("y", &y);
TH2F *h2 = new TH2F("h2", "Y vs X",
100, 0.0, 10.0,
80, -4.0, 4.0);
Long64_t nentries = t->GetEntries();
for (Long64_t i = 0; i < nentries; ++i) {
t->GetEntry(i);
// Optional: event selection
if (x < 0.0) continue;
if (std::abs(y) > 4.0) continue;
h2->Fill(x, y);
}
In this case, each call to GetEntry(i) loads the branches for one event, and the variables x and y hold the values for that event only. You then use exactly one Fill call per event that passes your selection cuts.
Event by event filling is also where you typically construct derived quantities. For example, suppose your TTree contains momentum components px and py, and you want to study the distribution of transverse momentum $p_T = \sqrt{p_x^2 + p_y^2}$ as a function of azimuthal angle $\phi = \arctan2(p_y, p_x)$:
float px, py;
t->SetBranchAddress("px", &px);
t->SetBranchAddress("py", &py);
TH2F *hPtPhi = new TH2F("hPtPhi", "p_{T} vs #phi",
64, -3.1416, 3.1416, // phi bins
100, 0.0, 10.0); // pT bins
for (Long64_t i = 0; i < nentries; ++i) {
t->GetEntry(i);
double pt = std::sqrt(px*px + py*py);
double phi = std::atan2(py, px);
// apply any physics selection here
if (pt < 0.1) continue;
hPtPhi->Fill(phi, pt);
}
Here you do not store pt and phi in the tree, but you calculate them on the fly from the raw components. The 2D histogram captures the correlation between these two derived observables across your entire dataset.
If events have associated weights, for example a branch w, you incorporate it directly:
float w;
t->SetBranchAddress("weight", &w);
for (Long64_t i = 0; i < nentries; ++i) {
t->GetEntry(i);
// compute x and y for this event
double x = /* ... */;
double y = /* ... */;
// selection
if (!passSelection(x, y)) continue;
h2->Fill(x, y, w);
}In simulations, weights might represent cross section normalization or efficiency corrections. In data analyses, they may encode luminosity scaling or background subtraction factors.
Event by event filling is not limited to TTrees. You can apply exactly the same pattern when you read text files line by line, iterate over a container of event objects, or loop through generated Monte Carlo events. The essential structure is always:
- Access one event.
- Compute or read the X and Y variables.
- Check any required conditions.
- Call
Fill(x, y)orFill(x, y, weight).
Important rule: The event loop must be structured so that all per event quantities are updated before each Fill call. Never reuse values from the previous event unintentionally, and ensure that each event contributes at most the intended number of entries to the 2D histogram.
Once you have filled your 2D histogram event by event, you can proceed to visualization and further analysis, for example by drawing the histogram with color scales, extracting projections along one axis, or converting the 2D distribution into a profile, all covered in subsequent sections.
Views: 10
KAHIBARO