KAHIBARO
Discord Login Register

18.4. Defining New Columns

`Define()`

In RDataFrame, Define() is the central tool for creating new columns from existing data without modifying the original TTree or file. Conceptually, you can think of it as adding a new named expression to the dataset that will be computed lazily, only when needed for an action such as creating a histogram.

The basic form is

cpp
auto df2 = df.Define("newCol", "expression using existing columns");

Here df is an existing ROOT::RDF::RDataFrame, "newCol" is the name of the new column, and the second argument is either a C++ expression in a string or a callable such as a lambda function.

When you pass a string expression, RDataFrame uses ROOT's C++ interpreter to evaluate it. Any existing column names can appear directly in the expression, and standard C++ operators and most ROOT math functions are available. For example, if your tree has branches px and py, you can define the transverse momentum as:

cpp
auto df_pt = df.Define("pt", "sqrt(px*px + py*py)");

Alternatively, you can use a C++ function or lambda. This is often clearer when the expression is long or when you want to reuse it. With a lambda, you specify the argument types that match the column types:

cpp
auto df_pt = df.Define(
    "pt",
    [](double px, double py) { return std::sqrt(px*px + py*py); },
    {"px", "py"}
);

In this form the third argument lists the input columns for the lambda. RDataFrame will call the lambda once per entry, passing the values of those columns, and store the returned value as the new column.

You can chain multiple Define() calls. Each new column can depend on previously defined ones:

cpp
auto df2 = df.Define("pt",  "sqrt(px*px + py*py)")
             .Define("pt2", "pt*pt");

RDataFrame keeps track of these dependencies internally. The definitions are not executed immediately. Instead, RDataFrame builds a computation graph in which Define() nodes represent column transformations. Evaluation happens only when you run an action such as Histo1D, Mean, or Snapshot.

Define() does not change the original TTree or write anything to disk by itself. It only adds a virtual column to the RDataFrame computation. To persist new columns to a file, you must run an action such as Snapshot that writes them out.

A defined column behaves like any other column. You can use it in further Define() calls, in Filter(), in histogram creation, or in statistical operations. This makes it straightforward to build complex analyses step by step, while keeping each transformation small and readable.

Because RDataFrame supports multithreading, the function or expression you use in Define() must be thread safe. Pure numerical transformations and simple uses of ROOT math utilities are safe. Avoid modifying global state, writing to shared objects without protection, or relying on side effects when you use Define().

Calculating derived quantities

A typical use of Define() is to calculate derived physical or analysis quantities from the raw branches stored in a TTree. Instead of manually looping over entries and writing values into arrays, you express the computation once, attach it to the RDataFrame with Define(), and then reuse the new column wherever needed.

For simple scalar derived quantities, you can often write the formula directly as a string expression. Suppose a tree stores energy measurements E1 and E2, and you want their sum and difference:

cpp
auto df2 = df.Define("Esum", "E1 + E2")
             .Define("Ediff", "E1 - E2");

You can then create histograms of these derived quantities without any extra loops:

cpp
auto hEsum  = df2.Histo1D({"hEsum", "Total energy;E_{sum};Events", 100, 0.0, 10.0}, "Esum");
auto hEdiff = df2.Histo1D({"hEdiff", "Energy difference;E_{1} - E_{2};Events", 100, -5.0, 5.0}, "Ediff");

More complex quantities benefit from using lambdas or functions. For example, if you have Cartesian components of a momentum vector px, py, and pz, you can compute its magnitude and then a normalized direction:

cpp
auto df3 = df.Define("p",    "sqrt(px*px + py*py + pz*pz)")
             .Define("ux",   "px / p")
             .Define("uy",   "py / p")
             .Define("uz",   "pz / p");

or, using a lambda for clarity and to share logic:

cpp
auto df4 = df.Define(
    "p",
    [](double px, double py, double pz) {
        return std::sqrt(px*px + py*py + pz*pz);
    },
    {"px", "py", "pz"}
);

When derived quantities involve conditions, Define() can incorporate branching logic just like regular C++. For instance, to define a flag that identifies events in a signal region:

cpp
auto df_sig = df.Define(
    "isSignal",
    [](double mass) { return (mass > 2.9) && (mass < 3.3); },
    {"mass"}
);

You can later use this boolean column in Filter() or to create separate histograms for signal-like and non signal-like events.

Define() can also handle vector-like branches, such as std::vector<float> representing multiple hits or particles in an event. In that case, your function or expression operates on vectors, and can return either a scalar summary or another vector. For example, to compute the number of tracks in each event given a vector branch track_pt:

cpp
auto df_tracks = df.Define(
    "nTracks",
    [](const std::vector<float> &pt) { return static_cast<int>(pt.size()); },
    {"track_pt"}
);

You can then use nTracks like any other integer column in your analysis.

If you have a reusable mathematical or physics quantity that appears in many analyses, you can encapsulate it in a separate C++ function, compile it, and then call it from Define(). This improves readability and keeps formulas in one place. For instance:

cpp
double transverseMass(double pt, double met, double dphi) {
    return std::sqrt(2.0*pt*met*(1.0 - std::cos(dphi)));
}
// In your macro:
auto df_mt = df.Define("mt", "transverseMass(pt, met, dphi)");

Because RDataFrame evaluates all these derived quantities lazily, it will compute each one at most once per entry, regardless of how many histograms or statistics you request from it. This lets you build rich sets of derived variables without worrying about extra loops or manual bookkeeping.

When calculating derived quantities with Define(), keep formulas in C++ syntax, not in other languages. Use && and || for logical operators, == for comparison, and standard C++ math functions like sqrt, sin, cos, and log. This is required because RDataFrame evaluates Define() expressions in C++.

Views: 11

Comments

Please login to add a comment.

Don't have an account? Register now!