24.5. Apply Data Selection
Table of Contents
From Raw Events to a Clean Sample
In the final project you move from simply looking at all available events to working with a carefully selected subset that is relevant for your physics question. This process is called data selection, or in high energy physics often just “cuts.” In this chapter you will design, implement, and validate the selection you will use for the rest of the project.
The goal here is not to teach every possible selection technique, but to guide you through applying a concrete, well documented set of cuts to your project dataset using ROOT. Everything you do in later steps, such as fitting and extracting physical quantities, will depend critically on the choices you make now.
A clear, documented, and reproducible event selection is essential. Never use cuts that you cannot write down precisely or reproduce in code.
Clarify the Physics Goal and Selection Strategy
Before writing any ROOT code, decide what “good” events mean for your analysis. The exact definitions depend on the project dataset provided for this course, but the structure is similar in all cases.
Ask yourself:
- What is the signal I want to study?
For example, “a narrow peak in energy,” “two body decay with a given invariant mass,” or “tracks above a certain momentum.” - What types of background do I expect?
For example, noise hits, poorly reconstructed events, or different physics processes that look similar but are not what you want. - Which variables in the TTree are relevant to distinguish signal from background?
These are the variables you saw in “Explore the Dataset” such as energy, time, position, number of hits, chi-square of a fit, flags, or trigger bits. - Which minimal quality criteria must any event satisfy?
These usually come from the detector or reconstruction, such as “no error flag,” “number of hits above 0,” or “chi-square below 10.”
Write this down in plain language first. For example:
“Select events where the reconstructed energy is between 450 and 550 keV, the time is within the main acquisition window, the number of detector hits is at least 3, and there are no reconstruction error flags set.”
You will then translate these conditions into a combination of comparisons and logical operators in ROOT.
Designing Concrete Selection Cuts
Once your strategy is clear, you can turn each verbal condition into a precise mathematical one. For each variable $x$, decide a range or condition like
$$x_{\min} \le x \le x_{\max}, \quad x > x_0, \quad x < x_0, \quad x \neq 0,$$
or more complex logical combinations such as
$$(x > x_0 \,\land\, y < y_0) \,\lor\, (z == 0).$$
Typical categories of cuts are:
Kinematic or observable cuts
For example, energy windows, momentum thresholds, or invariant mass ranges:
$$E_{\text{min}} < E < E_{\text{max}}, \quad p_T > p_{T,\text{min}}, \quad m_{\text{low}} < m_{\text{inv}} < m_{\text{high}}.$$
Quality cuts
Use variables that indicate how reliable the measurement is, such as chi-square, number of hits, or fit quality flags. For example:
$$\chi^2 / \text{ndf} < 5, \quad N_{\text{hits}} \ge 3.$$
Geometric and timing cuts
Restrict events to regions where the detector response is understood or where the signal appears:
$$r < r_{\text{max}}, \quad t_{\text{min}} < t < t_{\text{max}}.$$
Flag and category cuts
Events may have integer flags or categorical variables. Use equality or bitwise conditions:
$$\text{trigger} == 1, \quad \text{status} == 0.$$
At this stage you should produce a small “cut table” for your analysis. For example:
| Variable | Condition | Purpose |
|---|---|---|
| energy | 450 < energy < 550 | Select region around signal |
| nhits | nhits ≥ 3 | Remove poorly reconstructed |
| chi2 | chi2 < 10 | Ensure good fit quality |
| time | 0 < time < 1000 | Keep events in main time window |
| statusFlag | statusFlag == 0 | Exclude events with errors |
You will use this table when writing your ROOT selection expression.
Write your selection cuts in one place as a human readable table and keep them in sync with your code. If you change a cut in code, update the table and your notes.
Implementing Selection with TTree::Draw
For quick checks and for building intuition, TTree::Draw() is extremely useful, because it lets you apply selection expressions directly as strings without writing a full event loop.
The general pattern is
tree->Draw("variable", "selection_expression");
where "selection_expression" is any valid C++-like logical expression built from your TTree branches.
For example, suppose your branches are energy, nhits, chi2, time, and statusFlag. You could start with:
tree->Draw("energy",
"energy > 450 && energy < 550 && nhits >= 3 && chi2 < 10 && time > 0 && time < 1000 && statusFlag == 0");This command does two things at once:
It applies the selection to each entry.
It fills an implicit histogram of energy for events that pass the cuts and draws it on the current canvas.
Use this method to:
Check that your cuts are syntactically correct.
Look at how individual distributions change when you add or remove specific conditions.
Get an immediate visual impression of what your selection is doing.
If you want to reuse your selection, define it as a C++ string or TCut object after you open your file and TTree:
TCut baseCuts = "nhits >= 3 && chi2 < 10 && statusFlag == 0";
TCut signalWindow = "energy > 450 && energy < 550";
TCut timeWindow = "time > 0 && time < 1000";
TCut totalCuts = baseCuts && signalWindow && timeWindow;
tree->Draw("energy", totalCuts);
This way, your selection is more readable and you can turn parts on and off by commenting individual TCut definitions.
When applying selections with TTree::Draw, remember that only events that satisfy the selection expression are used. Always keep a record of how many events you started with and how many survive your cuts.
Implementing Selection in a Manual Event Loop
For the final project you will probably need more control than TTree::Draw provides, especially when filling multiple histograms, computing derived quantities, or saving selected events.
In that case you write a standard event loop over the TTree and implement your cuts in C++:
Long64_t nEntries = tree->GetEntries();
Long64_t nSelected = 0;
for (Long64_t i = 0; i < nEntries; ++i) {
tree->GetEntry(i);
// example branch variables (previously bound with SetBranchAddress)
// double energy, chi2, time;
// int nhits, statusFlag;
bool passBase = (nhits >= 3) && (chi2 < 10) && (statusFlag == 0);
bool passEnergy = (energy > 450.0) && (energy < 550.0);
bool passTime = (time > 0.0) && (time < 1000.0);
if (!(passBase && passEnergy && passTime)) {
continue; // skip this event
}
++nSelected;
// Fill histograms, compute derived quantities, etc.
hEnergy->Fill(energy);
}
std::cout << "Selected " << nSelected << " / " << nEntries << " events\n";This approach is flexible and makes it easy to:
Separate different logical groups of cuts, such as base quality vs signal region.
Keep counters for how many events each group of cuts rejects or accepts.
Fill histograms for both accepted and rejected events if needed.
Use the same numerical values for the selection here as in your quick TTree::Draw checks to ensure consistency.
Using RDataFrame for Declarative Selection
If you choose to use modern ROOT with RDataFrame in your project, your selection will look more declarative. Instead of writing explicit loops, you express filters as operations on a data frame.
The typical pattern is:
ROOT::RDataFrame df(*tree);
auto dfBase = df.Filter("nhits >= 3 && chi2 < 10 && statusFlag == 0", "Base quality cuts");
auto dfSignal = dfBase.Filter("energy > 450 && energy < 550", "Signal energy window");
auto dfSelected = dfSignal.Filter("time > 0 && time < 1000", "Time window");
// Example: book a histogram on the selected dataset
auto hEnergy = dfSelected.Histo1D({"hEnergy", "Energy after selection;E;Events", 100, 400, 600});
// Trigger the computation
hEnergy->Draw();Here you build a chain of filters, each with an optional label. RDataFrame automatically keeps track of how many entries passed each filter stage.
You can inspect this with:
auto report = dfSelected.Report();
report->Print();which will print a summary of how many events passed and failed each filter, which is very useful when documenting your selection efficiency.
For the final project, you can choose between classic TTree loops and RDataFrame. Use whichever approach you find clearer, but keep the logical conditions identical.
Quality Cuts, Signal Region, and Control Regions
A simple and effective way to organize your selection is to separate it into three conceptual layers.
Base quality cuts
These ensure that you only use events the detector and reconstruction handled properly. They are typically independent of the specific signal you are looking for. Examples include:
Good status flags.
Sufficient number of hits or tracks.
Acceptable chi-square values.
Detector regions known to be well calibrated.
In code you might group them into a single boolean or a dedicated filter, such as passBase or dfBase.
Signal region cuts
These cuts isolate the part of phase space where your signal is expected to dominate, such as a narrow energy or invariant mass window, or a range in time corresponding to a beam spill. These cuts are usually where you get your “interesting” sample that you will fit or otherwise analyze quantitatively.
Control or sideband regions
If your dataset and project goal allow it, define regions in variable space that are adjacent to the signal region, but where the signal is expected to be small or absent. These regions help you study the background behavior.
For example, if your signal region in energy is 450 to 550 keV, you could define sidebands:
$$400 < E < 430 \quad \text{and} \quad 570 < E < 600.$$
You can then apply the same base quality cuts to these sidebands to check how background behaves and to validate that your signal region is not dominated by some artifact.
In ROOT, this might look like:
TCut baseCuts = "nhits >= 3 && chi2 < 10 && statusFlag == 0";
TCut signalRegion = "energy > 450 && energy < 550";
TCut sidebandLow = "energy > 400 && energy < 430";
TCut sidebandHigh = "energy > 570 && energy < 600";
tree->Draw("energy", baseCuts && signalRegion);
tree->Draw("energy", baseCuts && sidebandLow, "same");
tree->Draw("energy", baseCuts && sidebandHigh, "same");This separation makes your analysis structure clearer and helps you explain it in your final report.
Checking the Effect of Each Cut
Once you have implemented your selection, you must verify that it behaves as expected. Do not jump directly to the final set of cuts. Instead, build them up step by step and inspect the effect at each step.
A practical sequence is:
- No cuts
Draw simple histograms such as energy, time, or multiplicity without any cuts. Record the total number of events. - Base quality cuts only
Apply only the base cuts and redraw the same histograms. Check how many events are removed and whether the shapes now look smoother or more physical. - Add the signal region cuts
Introduce energy or invariant mass windows. Observe how the distribution narrows and what fraction of events is kept. - Add additional refinements if needed
For instance, additional geometric or timing cuts to suppress specific backgrounds.
Keep a small “cut flow” table in your notes or print it from your code:
| Step | Condition added | Events remaining | Fraction of original |
|---|---|---|---|
| All events | none | N0 | 1.00 |
| Base quality cuts | baseCuts | N1 | N1 / N0 |
| + energy window | baseCuts && energy window | N2 | N2 / N0 |
| + time window | baseCuts && energy && time | N3 | N3 / N0 |
If you use RDataFrame you can extract these numbers directly from the filter report. With a manual loop you can maintain counters for each selection stage.
Always check how many events are removed by each cut. If a single cut removes almost all events, make sure its definition and units are correct.
Guarding Against Common Selection Mistakes
Data selection is a common source of subtle errors. A few practical checks will help you avoid serious problems later in the project.
Check units and ranges
Make sure the cut values match the units used in the TTree branches. For example, if time is stored in nanoseconds and you are thinking in microseconds, your cuts may be off by factors of 1000.
Inspect extreme values
Plot distributions on wide ranges before cutting and look for strange spikes or long tails. Some cuts might be needed just to remove unphysical values.
Compare distributions before and after cuts
Whenever you introduce a new cut, look at how the distribution of key variables changes. If a cut affects variables that it should not, something might be correlated in an unexpected way or your selection syntax might be wrong.
Use logical complements to sanity check
For a given cut, you can compare events that pass and fail. For example:
tree->Draw("energy", "nhits >= 3");
tree->Draw("energy", "nhits < 3");
If both look similar, your nhits cut might not be doing what you expected.
Avoid over-cutting
Too aggressive cuts can remove legitimate signal events and bias your final results. Prefer minimal cuts that clearly remove pathological events and backgrounds you understand. You will discuss the trade-off between purity and efficiency in your report.
Document all changes
If you adjust a cut threshold during your exploration, note what changed and why. This will make your final analysis and report more transparent and easier to reproduce.
Preparing for the Next Steps
At the end of your data selection stage you should have:
A clear written list of all cuts you apply, grouped into base quality, signal region, and possibly control regions.
Code that implements these cuts in a reproducible way, either using TTree::Draw, a manual loop, or RDataFrame.
A record of how many events survive each selection step.
A set of histograms or graphs showing the main variables after selection.
These selected events form the input for the next stages of the final project, where you will create detailed histograms, fit them with physics motivated functions, and extract physical quantities with uncertainties. Your cuts will directly influence those results, so keep your selection code clean, simple, and well documented.
Views: 12
KAHIBARO