处理TTree的几种方法

TTree是ROOT的一个存储数据的类,大概可以理解成一个二维数据表格。每一个Branch代表一种属性(或者一列),Entry代表一个事例(或者一行)。用户可以通过读取每一Entry中的特定Branch,来统计一个样本的某个属性的分布。
读取TTree数据的方法有很多,这里总结几个比较常用的,他们有的比较复杂但比较灵活,有的简单快捷,但是面对更复杂的数据结构或者操作就无能为力。不同的场景适用不同的方法,但个人觉得RDataFrame在各种情况都相对好用一些,写这篇博客也是起因于此。

其实可以直接去看文档

https://root.cern/manual/trees/#showing-an-entry-of-a-tree

https://root.cern/doc/master/tree2_8C.html

https://root.cern/doc/master/classTTree.html#a73450649dc6e54b5b94516c468523e45

https://root.cern/doc/master/classROOT_1_1RDataFrame.html


利用TTree本身的成员方法

以下内容大部分来自于官方文档的这几个部分:

https://root.cern/manual/trees/#showing-an-entry-of-a-tree

https://root.cern/doc/master/tree2_8C.html

https://root.cern/doc/master/classTTree.html#a73450649dc6e54b5b94516c468523e45

从一个.root文件中读取TTree的方法:

TFile *f = new TFile("tree2.root");
TTree *t2 = (TTree*)f->Get("t2");

其中TFile的构造函数打开了一个.root文件,Get方法从文件中找到名字对应的TObject,转换成TTree之后即可拿到一个TTree的指针。

TTree::Draw

TTree内置的Draw方法可以快捷的统计某个Branch或者某几个Branch的分布,还可以设置筛选条件,以及画图选项。

例如我们有一个Tree,其中包含这些Branch:

root [3] pion->Print()
******************************************************************************
*Tree    :pion      : pion                                                   *
*Entries :    14228 : Total =          860959 bytes  File  Size =     553940 *
*        :          : Tree compression factor =   1.55                       *
******************************************************************************
*Br    0 :RunNum    : RunNum/I                                               *
*Entries :    14228 : Total  Size=      57547 bytes  File Size  =        547 *
*Baskets :        2 : Basket Size=      32000 bytes  Compression= 104.31     *
*............................................................................*
*Br    1 :PiNumInEvent : PiNumInEvent/I                                      *
*Entries :    14228 : Total  Size=      57583 bytes  File Size  =       7760 *
*Baskets :        2 : Basket Size=      32000 bytes  Compression=   7.35     *
*............................................................................*
*Br    2 :EventNum  : EventNum/I                                             *
*Entries :    14228 : Total  Size=      57559 bytes  File Size  =      17574 *
*Baskets :        2 : Basket Size=      32000 bytes  Compression=   3.25     *
*............................................................................*
*Br    3 :MCPEn     : MCPEn/D                                                *
*Entries :    14228 : Total  Size=     114605 bytes  File Size  =      88737 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.29     *
*............................................................................*
*Br    4 :MCPTheta  : MCPTheta/D                                             *
*Entries :    14228 : Total  Size=     114629 bytes  File Size  =      89337 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.28     *
*............................................................................*
*Br    5 :MCPPhi    : MCPPhi/D                                               *
*Entries :    14228 : Total  Size=     114613 bytes  File Size  =      90367 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.26     *
*............................................................................*
*Br    6 :DauPhotonEnMax : DauPhotonEnMax/D                                  *
*Entries :    14228 : Total  Size=     114677 bytes  File Size  =      86169 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.32     *
*............................................................................*
*Br    7 :DauPhotonEnMin : DauPhotonEnMin/D                                  *
*Entries :    14228 : Total  Size=     114677 bytes  File Size  =      86793 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.32     *
*............................................................................*
*Br    8 :DauPhotonCluDis : DauPhotonCluDis/D                                *
*Entries :    14228 : Total  Size=     114685 bytes  File Size  =      85405 *
*Baskets :        4 : Basket Size=      32000 bytes  Compression=   1.34     *
*............................................................................*

统计其中“MCPID”的分布:

pion->Draw("MCPEn")

处理TTree的几种方法
用一个Branch做筛选

pion->Draw("MCPEn","MCPEn != 0")

处理TTree的几种方法
这些都比较简单,文档里都很好找。还可以用TBrowser在交互式界面中去观察数据。

可以通过gDirectory拿到这个直方图。

	pion->Draw("DauPhotonCluDis>>h(40,0,200)");
	TH1D* h = (TH1D*)gDirectory->FindObject("h");

如果要处理的Branch对应的数据类型是基本的double,int等,那么以上方法非常简便。

如果处理的Branch对应的是一个容器,比如vector,甚至是一个用户自定义的结构体或对象。Draw()的能力就比较有限。

Draw函数提供了一些基本的操作来访问所存储的vector的属性。例如这样一个Tree

root [2] Event->Show(0)
======> EVENT:0
 RunNum          = 1
 EventNum        = 0
 MCPEn           = 2.8601
 MCPTheta        = 2.46585
 MCPPhi          = 1.49832
 Conv            = 0
 LeadingEn       = 1.85087
 SubLeadingEn    = (vector<double>*)0x7fffbc4e5700

SubLeadingEn是一个vector<double>的Branch,每个vector都有一个size()方法,可以通过@SubLeadingEn.size()的方式统计这个方法的返回值:

Event->Draw("@SubLeadingEn.size()")

处理TTree的几种方法

这样也是可以解决一部分问题。但如果要进行更复杂的操作,比如说统计SubLeadingEn这个vector中所有不为零的项目的个数,或者两个等长vector的Branch(A,B),把B按照A的降序排列,统计A最大的那个元素所对应的B元素,或者计算所有非零A元素对应的B元素之和,这个时候上述方法就会比较麻烦。

TBranch::GetEntry

这是一个非常通用的办法,也是我第一次接触ROOT所见到的方法。

它的逻辑大概是这样的:

  1. 获取需要的Branch:GetBranch
  2. 为处理的Branch都指定一个对应数据类型的地址:SetAddress
  3. 遍历所有Entries,每经过一个Entry,对应地址的数据就刷新一次GetEntry(i)

下面是官方教程给出的示例:

TFile *f = new TFile("tree2.root");
TTree *t2 = (TTree*)f->Get("t2");
static Float_t destep;
TBranch *b_destep = t2->GetBranch("destep");
b_destep->SetAddress(&destep);

//create one histogram
TH1F *hdestep   = new TH1F("hdestep","destep in Mev",100,1e-5,3e-5);

//read only the destep branch for all entries
Long64_t nentries = t2->GetEntries();
for (Long64_t i=0;i<nentries;i++) {
   b_destep->GetEntry(i);
   hdestep->Fill(destep);  // 注意这句要放在循环里
}

// hdestep->Fill(destep); // 而不能放在循环外,否则TH1F只Fill到最后一个数据

这个方法几乎可以满足所有的需求,无论是处理复杂的对象还是跨Entries处理数据,都是用户自己定义的。缺点就是代码太长了,要定义的东西太多了,需求稍有变化,代码就要大改,而且可读性不好,经常多个循环嵌套在一起,时间长了不知道自己写的是什么。

RDataFrame

https://root.cern/doc/master/classROOT_1_1RDataFrame.html

RDataFrame是ROOT命名空间的一个类,它(以及其他配和使用的类一起)构成一套比较整洁的数据处理模式。GetEntry的方法就像一个手工作坊,每一步都要亲自一步一步做好。RDataFrame有自己的多个车间,每个车间都可以接收一个半成品,加工之后送给下一个车间,用户只需要把车间顺序安排好,把标准设定好,其中很多原本要设置的中间变量都可以放下不管。这样代码的思路就非常清晰,修改的时候也会比较简单。

在这之前先设置好命名空间:

using namespace ROOT;

.root文件中获取一个Tree,填入RDataFrame:

RDataFrame df("Event","tautau.root");

对其中的事例进行筛选,可以使用Filter方法:

double en_threshold = 0.3;
double acceptance = 0.99;
auto filter_en = [&en_threshold, &acceptance](double t, double r, double theta)
{
	return (t > en_threshold || t == 0) && (fabs(TMath::Cos(theta)) < acceptance || t == 0);
};

auto define_df = df.Filter(filter_en,{"MCPEn","RecoPEn","MCPTheta"});

其中你需要给Filter一个筛选的判据,即Lambda表达式filter_en,然后告诉Fitler需要用到的信息来自于哪些Branch{"MCPEn","RecoPEn","MCPTheta"}。这样,Fiter会返回一个对象ROOT::RDF::RInterface,这个类型不需要太在意,总之就是类似与一个处理过的DataFrame,可以用auto来承接这个返回值,也可以用ROOT::RDF::RNode

此外,也可以使用类似TTree::Draw()的写法,输入一条字符串来完成筛选。多个Filter可以叠加在一起,还可以为每一个Filter命名用以区分。最后,可以获得这个Filter的效率:

auto cut_df = df.Filter("Num_Pi0 >= 0", "1 Pion")
    .Filter("newRecoPID != 0 || MCPID != 0","ToF cut");

cut_df.Report()->Print();

输出如下:

1 Pion    : pass=875634     all=875634     -- eff=100.00 % cumulative eff=100.00 %
ToF cut   : pass=875634     all=875634     -- eff=100.00 % cumulative eff=100.00 %

这个Filter的好处在于,它几乎可以完成任何你想完成的操作,因为Lambda表达式相当于一个代码段。同时它又非常直观,你不需要去考虑处理每个Entry的时候要把数据放在那里,判据不是分散在循环中,而是独立存在于一个Lambda表达式中,如果需要修改代码,你可以一目了然地找到定义判据的位置。(当然,如果coding习惯很好,把每个代码段都包装得很规范,也可以用GetEntry达到相同的效果,但相对就还是稍微麻烦一点。)

这里Filter的判据可以使用Lambda表达式或者函数对象。用Lambda的一个好处在于它有变量捕获的功能,可以抓取外部变量,也是相对比较整洁。
这个Filter是一个模板,文档里是这么写的:

template<typename Proxied , typename DataSource = void>
template<typename F , std::enable_if_t<!std::is_convertible< F, std::string >::value, int > = 0>
RInterface<RDFDetail::RFilter<F, Proxied>, DS_t> ROOT::RDF::RInterface< Proxied, DataSource >::Filter	(	F 	f,
const ColumnNames_t & 	columns = {},
std::string_view 	name = "" 
)		

Append a filter to the call graph.
Parameters
[in] f Function, lambda expression, functor class or any other callable object. It must return a bool signalling whether the event has passed the selection (true) or not (false).
[in] columns Names of the columns/branches in input to the filter function.
[in] name Optional name of this filter. See Report.

要从已知的数据中定义新的数据,比如说从动量能量信息计算质量,可以用Define方法。这里还是用到Lambda表达式举例:

auto cut_df = df.Filter("Num_Pi0 >= 0", "1 Pion")
    .Filter("RecoPID != 0 || MCPID != 0","ToF cut");
    
auto paired_photon = cut_df.Filter("MCPID == 22 && RecoPID == 22","Paired");

double tof_smear = 0;
TRandom3 rdm_generator;
/************ TOF distribution **************/
double tof_min = -6;
double tof_max = 2;
int nbins_tof = 100;

auto paired_tof = paired_photon.Define("ToF_smear", [&tof_smear, &rdm_generator](double tof){ return (rdm_generator.Gaus(tof, tof_smear)); },{"RecoPTOF"})
                               .Define("logTOF","log10(ToF_smear)")
                               .Histo1D({"","paired photons",nbins_tof,tof_min,tof_max},"logTOF");

paired_photon之前,都是叠加多个Filter,然后在paired_photon 的基础上使用Define,首先告诉Define方法,想要定义的这个新的量叫什么(“ToF_smear”),然后用“lambda, branch name list”来告诉Define,对RecoPTOF这个量加一个高斯的smear。这类似于Filter,只不过Filter要求输入一个bool返回值的函数对象或lambda,而Define要求返回一个double或者int之类的变量。

此外,上面的代码段在Define之后还调用了Histo1D来生成直方图,除了Histo1D还有Histo2D,Profile1D,Count,Sum,Foreach等等统计工具可以使用。官方文档里讲的听清楚的,这里只是举例解释一下它的逻辑,以及展示一下它还是挺好用的。目前来讲我不是很喜欢GetEntry的方法,用RDataFrame可以完全取代。

RDataFrame这一套工具有很鲜明的现代C++的编程风格,广泛使用模板使他的很多函数可以处理不同类型的数据,并且一定程度上接近python弱类型语言,你不需要关心它给你的东西是什么类型(函数传参的时候可以统一用ROOT::RDF::RNode来代替RDataFrame
RInterface)。

总之就是挺方便的,精力有限~~(懒)~~ ,先草草说这么几句,官网上讲的很全,还有很多有意思的功能(比如转化成numpy,开启多线程等)以及配套的示例,没事可以去翻翻。

上一篇:Git小知识点理解


下一篇:gitlab分支管理