KAHIBARO
Discord Login Register

6.2. Filling 2D Histograms

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

cpp
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,

cpp
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

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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:

cpp
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)$:

cpp
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:

cpp
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:

  1. Access one event.
  2. Compute or read the X and Y variables.
  3. Check any required conditions.
  4. Call Fill(x, y) or Fill(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

Comments

Please login to add a comment.

Don't have an account? Register now!