python から本家ROOT (pyROOT) を使わずに ROOTファイルを扱う、そんなお話。
背景
オンラインのデータ取得は ROOT/C++ ながら、オフライン解析は python なケースが増えてきた。
python でデータ処理をするなら pandas/numpy 形式にすると扱いやすい。
root_pandas やそのベースとなる root_numpy というものがあったようだが、
以前サラッと見た印象では、少なくとも私にはあまり直感的ではなかったし、
今現在はいずれも更新が停滞し、DEPRECATED/UNMAINTAINED 状態になっているようだ。
今回は、uproot を用いた方法を紹介する。
 
uproot
uproot は Scikit-HEP project が公開しているパッケージの一つ。
Scikit-HEP は Particle Physics のデータ解析を python でシームレスに行える枠組みを作ることを目的としており、
ROOT との接続は切っても切れない要素であり、uproot はその一部のようだ。
# 他にも iminit とか Goofit とか面白そうなプロジェクトが多数ある。
uproot は ROOTファイルと内包される ROOTオブジェクトとの I/Oに特化したライブラリで、
本家 ROOT (pyROOT) が無くても単独で利用でき、
比較的容易に numpy/pandas にデータを乗せ換えることができる。
以下、uproot 本家の記載。
Unlike the standard C++ ROOT implementation, Uproot is only an I/O library, primarily intended to stream data into machine learning libraries in Python. Unlike PyROOT and root_numpy, Uproot does not depend on C++ ROOT. Instead, it uses Numpy to cast blocks of data from the ROOT file as Numpy arrays.
導入
$ conda install uproot (miniforge/conda-forge) だけでインストールできる。
実は ROOT も conda からインストールできるがそのサイズは数GB。
一方、uproot のtarボールは 220kB?、展開後も 3.3MB? 程度なので、
実に簡単に手軽にすぐ導入できる。
使い方については 公式ドキュメント の Getting Started を参照。
以下は主に TTree 扱うために必要な部分の抜粋。
 
TFile
データを読み出す時は以下。
| 1 2 | import uproot file = uproot.open("data.root") | 
なお、python ではよくやる方法だが以下のようにするとファイルのクローズなどを意識せずに済む。
| 1 2 | with uproot.open("data.root") as file:     # do something with the 'file' | 
TFile::ls() に近いのは以下。
| 1 2 3 | [in]: file.classnames() [out]: {'tr;1': 'TTree', ...} | 
これで格納されている ROOTオブジェクトとその名前がわかる。
格納されているオブジェクトには名前でアクセスできる。
| 1 2 3 | [in]: file['tr'] [out]: <TTree 'tr' (N branches) at 0xXXXXXXXXXX> | 
或いは、名前さえ知っていれば以下のように記述することもできる。
| 1 2 3 | [in]: uproot.open("data.root:tr") [out]: <TTree 'tr' (N branches) at 0xXXXXXXXXXX> | 
TTree
TTree の Branch など構成を知りたい場合、TTree::Print() に近いのは以下。
| 1 2 3 4 5 6 7 8 9 | [in]: tr=uproot.open("data.root:tr") tr.show() [out]: name                 | typename                 | interpretation                 ---------------------+--------------------------+------------------------------- unixtime             | uint32_t                 | AsDtype('>u4') val                  | float[4]                 | AsDtype("('>f4', (4,))") | 
或いは tr.typenames() でも良いかもしれない。
 
pandas 形式にする
pandas.DataFrame 形式で取り扱うには以下のようにする。
| 1 2 3 4 5 6 7 8 9 | [in]: tr.arrays(["unixtime","val"], library="pd") [out]:          unixtime      val[0]      val[1]      val[2]      val[3] 0      163xxxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx ...           ...         ...         ...         ...         ... n      163xxxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx [n rows x 5 columns] | 
そのほか、pandas ではなく branch名の付いた numpy の dictionary で受けたり、
特定の branch 単体を numpy で受けたりもできるようだが、
よほどメモリサイズや処理時間が気にならない限りは、冒頭の pandas.DataFrame で受けて処理すれば良いかと思う。
これはちょうど TTree::SetBranchStatus("*",0) などとして TTree::GetEntry(...) の読み込みを限定する感じだろう。
| 1 2 3 4 5 6 7 8 9 10 | [in]: tr.arrays(['unixtime','val'],library='np') [out]: {'unixtime': array([163xxxxxxx, ..., 163xxxxxxx], dtype=uint32),  'val': array([[xxx.xxxxxx, xxx.xxxxxx,xxx.xxxxxx,xxx.xxxxxx], ...,        [xxx.xxxxxx, xxx.xxxxxx,xxx.xxxxxx,xxx.xxxxxx]], dtype=float32)} [in]: tr['unixtime'].array(library='np') [out]: array([163xxxxxxx, ..., 163xxxxxxx], dtype=uint32) | 
複数ファイル連結する ~ TChain
複数ファイルに分散する同じ構成の TTree を連結する TChain を使う場合に対応。
| 1 2 3 4 5 6 7 8 9 | [in]: uproot.concatenate(["data1.root:tr","data2.root:tr"],filter_name="*",library="pd") [out]:          unixtime      val[0]      val[1]      val[2]      val[3] 0      163xxxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx ...           ...         ...         ...         ...         ... N      164xxxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx  xxx.xxxxxx [N rows x 5 columns] |