|
| 1 | +## \file |
| 2 | +## \ingroup tutorial_ml |
| 3 | +## \notebook -nodraw |
| 4 | +## This tutorial demonstrates training a classifier model directly |
| 5 | +## from remote ROOT data without intermediate preparation steps, |
| 6 | +## using XGBoost and the RDataLoader interface. |
| 7 | +## |
| 8 | +## \macro_code |
| 9 | +## \macro_output |
| 10 | +## |
| 11 | +## \date July 2026 |
| 12 | +## \author Silia Taider |
| 13 | + |
| 14 | +import ROOT |
| 15 | + |
| 16 | +variables = ["Muon_pt_1", "Muon_pt_2", "Electron_pt_1", "Electron_pt_2"] |
| 17 | + |
| 18 | + |
| 19 | +def filter_events(df): |
| 20 | + """Reduce initial dataset to only events which shall be used for training""" |
| 21 | + return df.Filter("nElectron>=2 && nMuon>=2", "At least two electrons and two muons") |
| 22 | + |
| 23 | + |
| 24 | +def define_variables(df): |
| 25 | + """Define the variables which shall be used for training""" |
| 26 | + return ( |
| 27 | + df |
| 28 | + .Define("Muon_pt_1", "Muon_pt[0]") |
| 29 | + .Define("Muon_pt_2", "Muon_pt[1]") |
| 30 | + .Define("Electron_pt_1", "Electron_pt[0]") |
| 31 | + .Define("Electron_pt_2", "Electron_pt[1]") |
| 32 | + ) |
| 33 | + |
| 34 | + |
| 35 | +def prepare_rdf(filename, label_value): |
| 36 | + """Load, filter, define variables, and add label column""" |
| 37 | + filepath = "root://eospublic.cern.ch//eos/root-eos/cms_opendata_2012_nanoaod/" + filename |
| 38 | + df = ROOT.RDataFrame("Events", filepath) |
| 39 | + df = filter_events(df) |
| 40 | + df = define_variables(df) |
| 41 | + df = df.Define("label", f"{label_value}.0") |
| 42 | + return df |
| 43 | + |
| 44 | + |
| 45 | +def load_data(): |
| 46 | + """Load signal and background data""" |
| 47 | + rdf_sig = prepare_rdf("SMHiggsToZZTo4L.root", 1) |
| 48 | + rdf_bkg = prepare_rdf("ZZTo2e2mu.root", 0) |
| 49 | + |
| 50 | + # Compute class-balancing weights |
| 51 | + num_sig = rdf_sig.Count().GetValue() |
| 52 | + num_bkg = rdf_bkg.Count().GetValue() |
| 53 | + num_all = num_sig + num_bkg |
| 54 | + |
| 55 | + rdf_sig = rdf_sig.Define("weight", f"{num_all}.0/{num_sig}.0") |
| 56 | + rdf_bkg = rdf_bkg.Define("weight", f"{num_all}.0/{num_bkg}.0") |
| 57 | + |
| 58 | + loader = ROOT.Experimental.ML.RDataLoader( |
| 59 | + [rdf_sig, rdf_bkg], |
| 60 | + columns=variables + ["label", "weight"], |
| 61 | + target="label", |
| 62 | + weights="weight", |
| 63 | + batch_size=num_all, # Load all data in one batch |
| 64 | + drop_remainder=False, |
| 65 | + set_seed=42, |
| 66 | + ) |
| 67 | + |
| 68 | + # Split into training and testing sets |
| 69 | + train, test = loader.train_test_split(test_size=0.5) |
| 70 | + |
| 71 | + # train.as_numpy() and test.as_numpy() return generators of batches. |
| 72 | + # Since batch_size=num_all, each split contains exactly one batch; |
| 73 | + # next(iter(...)) materializes it into in-memory numpy arrays. |
| 74 | + X_train, y_train, w_train = next(iter(train.as_numpy())) |
| 75 | + X_test, y_test, w_test = next(iter(test.as_numpy())) |
| 76 | + |
| 77 | + # Flatten target and weights from (n,1) to (n,) as expected by XGBoost |
| 78 | + return (X_train, y_train.ravel(), w_train.ravel(), X_test, y_test.ravel(), w_test.ravel()) |
| 79 | + |
| 80 | + |
| 81 | +if __name__ == "__main__": |
| 82 | + from sklearn.metrics import roc_auc_score |
| 83 | + from xgboost import XGBClassifier |
| 84 | + |
| 85 | + X_train, y_train, w_train, X_test, y_test, w_test = load_data() |
| 86 | + |
| 87 | + print(f"Training events: {X_train.shape[0]}") |
| 88 | + print(f"Testing events: {X_test.shape[0]}") |
| 89 | + |
| 90 | + bdt = XGBClassifier(max_depth=3, n_estimators=500) |
| 91 | + bdt.fit(X_train, y_train, sample_weight=w_train) |
| 92 | + |
| 93 | + # Evaluate on test set |
| 94 | + y_proba = bdt.predict_proba(X_test)[:, 1] |
| 95 | + auc = roc_auc_score(y_test, y_proba) |
| 96 | + |
| 97 | + print(f"Training done. ROC AUC: {auc:.4f}") |
0 commit comments