KAHIBARO
Discord Login Register

18.3 Filtering Data

`Filter()`

In RDataFrame, Filter() is the central tool to select only the entries (events, rows) that satisfy a given condition. Conceptually, it plays the same role as an if statement inside a manual TTree loop, but it is expressed in a declarative way.

You always call Filter() on an existing RDataFrame object. The method takes at least two arguments: a boolean expression and a string label. The expression is written in C++ and refers to existing columns of the dataframe, either read from branches in a TTree or created with Define().

A typical call looks like this:

cpp
ROOT::RDataFrame df("Events", "data.root");
auto df_selected = df.Filter("energy > 5.0", "energy cut");

The first argument "energy > 5.0" is evaluated for every entry. Only entries where it returns true are passed to df_selected. The second argument "energy cut" is a human readable name used in progress reports and in some statistics that RDataFrame can provide. It is optional but strongly recommended.

You can also write more complex logical conditions. RDataFrame uses the same logical operators as C++:

cpp
auto df_good = df.Filter("energy > 5.0 && nTracks >= 2", "basic quality selection");

Here, an entry is kept only if both conditions are satisfied. Logical || means at least one condition is satisfied, and ! negates a condition.

Filter() can also take a C++ callable such as a lambda instead of a string expression. This is useful when the condition cannot easily be expressed as a short expression or when you prefer to write explicit C++ code:

cpp
auto df_filtered = df.Filter(
  [](double energy, int charge) {
    return energy > 3.0 && charge != 0;
  },
  {"energy", "charge"},      // list of columns used as arguments
  "non-zero charged particles with E > 3"
);

In this form, the second parameter is a list of column names that are passed as arguments to the lambda in the same order.

From the user point of view, Filter() is lazy. The filtering is not executed when you call Filter(). Instead, RDataFrame builds a computation graph. The actual work only happens when you trigger an action such as Histo1D(), Mean(), or Snapshot(). This means you can define many filters and derived columns without paying a cost until you actually request an output.

It is important to remember that Filter() does not modify the original dataframe. Instead, it returns a new RDataFrame node that represents the filtered view. The original df remains unchanged and can be reused for other selections.

Rule: Filter() selects entries where the condition is true and returns a new RDataFrame node. The original dataframe is not modified, and no data processing happens until an action is executed.

In many analyses, Filter() is used directly before histogram creation or statistical operations, for example:

cpp
auto df_muons = df.Filter("abs(pdgId) == 13", "select muons");
auto h_pt_muons = df_muons.Histo1D({"h_pt_muons", "Muon p_{T}", 100, 0., 100.}, "pt");

Here, only entries that pass the muon selection contribute to the histogram of transverse momentum.

Multiple filters

Real analyses rarely use a single cut. Instead, you typically apply a sequence of cuts that represent different selection steps, such as basic quality requirements, detector acceptance, and final signal region definitions. With RDataFrame you express this as a chain of Filter() calls.

A chain can be written by applying Filter() one after another on the result of the previous filter:

cpp
auto df_base   = ROOT::RDataFrame("Events", "data.root");
auto df_basic  = df_base.Filter("energy > 1.0", "basic energy cut");
auto df_quality= df_basic.Filter("nHits >= 10", "hit quality cut");
auto df_signal = df_quality.Filter("mass > 2.9 && mass < 3.3", "signal mass window");

Each call creates a new node in a computation graph. df_signal sees only the entries that passed all previous filters. You can attach different actions at different stages of the chain:

cpp
auto h_all    = df_base.Histo1D({"h_all", "All events;mass;Entries", 100, 0., 5.}, "mass");
auto h_basic  = df_basic.Histo1D({"h_basic", "After basic cut;mass;Entries", 100, 0., 5.}, "mass");
auto h_signal = df_signal.Histo1D({"h_signal", "Signal region;mass;Entries", 100, 0., 5.}, "mass");

All three histograms are produced from a single pass over the data. RDataFrame automatically shares work between the different branches of the graph, which is efficient even when many filters and actions are used together.

You can also express several requirements inside a single Filter() by combining them with && and ||. Often it is clearer to separate logical stages into distinct filters with descriptive labels, because RDataFrame can report statistics for each filter stage. This helps you monitor how many events survive each cut.

The following table illustrates two equivalent ways to implement two cuts on energy and nTracks:

StyleExample expressionComment
Single filter"energy > 5.0 && nTracks >= 2"One node, combined logical condition
Multiple filtersfirst "energy > 5.0", then "nTracks >= 2"Clear separation of cuts and cutflow

Both styles produce the same final selection. The choice is mainly about readability and about how you want to track cut efficiencies.

Because RDataFrame is lazy, the order in which you write multiple filters in your script matters logically but not in terms of when the code runs. The filters are applied in the order they appear when an action is triggered. If one filter is much more restrictive than another, placing it earlier in the chain can reduce the amount of work done by subsequent filters and computations.

Multiple filters can be mixed with Define() calls that construct new columns. A common pattern is to define derived quantities first, then filter on them:

cpp
auto df_with_pt = df_base.Define("pt", "sqrt(px*px + py*py)");
auto df_high_pt = df_with_pt.Filter("pt > 20.0", "high pT selection");

In summary, you construct complex selections in RDataFrame by chaining Filter() calls. Each filter represents a logical step, and the full sequence defines the final event selection used by your actions.

Rule: You can chain several Filter() calls to represent different selection steps. All filters in the chain must be satisfied for an entry to reach the final node, and a single pass over the data can serve all branches and actions of the graph.

Views: 11

Comments

Please login to add a comment.

Don't have an account? Register now!