Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Detalles de TTree

Estructura y terminología de los archivos ROOT

Un archivo ROOT (ROOT TFile, uproot.ReadOnlyFile) es como un pequeño sistema de archivos que contiene directorios anidados (ROOT TDirectory, uproot.ReadOnlyDirectory). En Uproot, los directorios anidados se presentan como diccionarios anidados.

Cualquier instancia de clase (ROOT TObject, uproot.Model) puede almacenarse en un directorio, incluidos tipos como histogramas (por ejemplo, ROOT TH1, uproot.behaviors.TH1.TH1).

Una de estas clases, TTree (ROOT TTree, uproot.TTree), es una puerta de entrada a conjuntos de datos grandes. Un TTree es algo parecido a un DataFrame de Pandas en el sentido de que representa una tabla de datos. Las columnas se llaman TBranches (ROOT TBranch, uproot.TBranch), que pueden estar anidadas (a diferencia de Pandas), y los datos pueden tener cualquier tipo de C++ (a diferencia de Pandas, que puede almacenar cualquier tipo de Python).

Un TTree a menudo es demasiado grande para caber en la memoria, y a veces (raramente) incluso un solo TBranch es demasiado grande para caber en la memoria. Por eso cada TBranch se divide en TBaskets (ROOT TBasket, uproot.models.TBasket.Model_TBasket), que son “lotes” de datos. (Estos son los mismos lotes que escribe cada llamada a extend en la lección anterior). Los TBaskets son la unidad más pequeña que se puede leer de un TTree: si deseas leer la primera entrada, tienes que leer el primer TBasket.

terminología

Como analista de datos, probablemente te ocuparás de los TTrees y TBranches de manera directa, pero solo de los TBaskets cuando surjan problemas de eficiencia. Los archivos con TBaskets grandes pueden requerir mucha memoria para leerse; los archivos con TBaskets pequeños serán más lentos de leer (en ROOT también, pero especialmente en Uproot). Los TBaskets del orden de megabytes suelen ser ideales.

Ejemplos con un TTree grande

Este archivo tiene 2.1 GB y está alojado en el Portal de Datos Abiertos del CERN.

import uproot

url_archivo = "root://eospublic.cern.ch//eos/opendata/cms/derived-data/AOD2NanoAODOutreachTool/Run2012BC_DoubleMuParked_Muons.root"

# Si estás en Windows o no tienes XRootD instalado, puedes usar esta url en su lugar
# url_archivo = "https://root.cern/files/rootbench/Run2012BC_DoubleMuParked_Muons.root"

archivo = uproot.open(url_archivo)
archivo.classnames()
{'Events;75': 'TTree', 'Events;74': 'TTree'}

Simplemente pedir el objeto uproot.TTree e imprimirlo no lee todo el conjunto de datos.

tree = archivo["Events"]
tree.show()
name                 | typename                 | interpretation                
---------------------+--------------------------+-------------------------------
nMuon                | uint32_t                 | AsDtype('>u4')
Muon_pt              | float[]                  | AsJagged(AsDtype('>f4'))
Muon_eta             | float[]                  | AsJagged(AsDtype('>f4'))
Muon_phi             | float[]                  | AsJagged(AsDtype('>f4'))
Muon_mass            | float[]                  | AsJagged(AsDtype('>f4'))
Muon_charge          | int32_t[]                | AsJagged(AsDtype('>i4'))

Leer una parte de un TTree

En la lección anterior aprendimos que la forma más directa de leer un TBranch es llamando a uproot.TBranch.array.

# sin entry_stop, tomará mucho tiempo e incluso podría fallar
tree["nMuon"].array(entry_stop=10_000)
Loading...

Leer el TBranch completo tomaría mucho tiempo, porque habría que enviar todos sus datos a través de la red. Por eso la celda anterior pasa entry_stop.

De forma más general, establece entry_start y entry_stop en el rango que desees. entry_start es inclusivo, entry_stop es exclusivo, y la primera entrada se indexa con 0, al igual que las rebanadas en una interfaz de array (primera lección). Uproot solo lee tantos TBaskets como sean necesarios para proporcionar estas entradas.

tree["nMuon"].array(entry_start=1_000, entry_stop=2_000)
Loading...

Estos son los bloques de construcción de un lector de datos en paralelo: cada uno es responsable de una rebanada diferente. (Consulta también uproot.TTree.num_entries_for y uproot.TTree.common_entry_offsets, que se pueden usar para elegir entry_start/entry_stop de manera óptima).

Leer múltiples TBranches a la vez

Supongamos que sabes que vas a necesitar todos los TBranches de muones. Pedirlos en una sola solicitud es más eficiente que pedir cada TBranch individualmente, porque el servidor puede ir leyendo del disco los TBaskets posteriores mientras los TBaskets anteriores se te envían a través de la red. Mientras que un TBranch tiene un método array, el TTree tiene un método arrays (en plural) para obtener múltiples arrays.

muones = tree.arrays(
    ["Muon_pt", "Muon_eta", "Muon_phi", "Muon_mass", "Muon_charge"], entry_stop=1_000
)
muones
Loading...

Ahora los cinco TBranches están en la salida, muones, que es un Awkward Array. Un Awkward Array de múltiples TBranches tiene una interfaz similar a un diccionario, por lo que podemos obtener cada variable de él así

muones["Muon_pt"]
Loading...
muones["Muon_eta"]
Loading...
muones["Muon_phi"]  # etc.
Loading...

Seleccionar TBranches por nombre

Supongamos que tienes muchos TBranches de muones y no quieres enumerarlos todos. Tanto uproot.TTree.keys como uproot.TTree.arrays aceptan un argumento filter_name que puede seleccionarlos de varias maneras (consulta la documentación). En particular, es recomendable usar primero keys para saber qué TBranches coinciden con tu filtro, seguido de arrays para leerlas realmente.

tree.keys(filter_name="Muon_*")
['Muon_pt', 'Muon_eta', 'Muon_phi', 'Muon_mass', 'Muon_charge']
tree.arrays(filter_name="Muon_*", entry_stop=1_000)
Loading...

(También hay filter_typename y filter_branch para más opciones).

Escalar el análisis y hacer un gráfico

La mejor manera de entender lo que estás haciendo es experimentar con conjuntos de datos pequeños y luego escalarlos. Aquí tomamos 1000 eventos y calculamos las masas de los dimuones.

muones = tree.arrays(entry_stop=1_000)
corte = muones["nMuon"] == 2

pt0 = muones["Muon_pt", corte, 0]
pt1 = muones["Muon_pt", corte, 1]
eta0 = muones["Muon_eta", corte, 0]
eta1 = muones["Muon_eta", corte, 1]
phi0 = muones["Muon_phi", corte, 0]
phi1 = muones["Muon_phi", corte, 1]

import numpy as np

masa = np.sqrt(2 * pt0 * pt1 * (np.cosh(eta0 - eta1) - np.cos(phi0 - phi1)))

import hist

histmasa = hist.Hist(hist.axis.Regular(120, 0, 120, label="masa [GeV]"))
histmasa.fill(masa)
histmasa.plot();
<Figure size 640x480 with 1 Axes>

Eso funcionó (hay un pico del Z). Ahora, para hacer esto sobre todo el archivo, debemos tener más cuidado con lo que estamos leyendo,

tree.keys(filter_name=["nMuon", "/Muon_(pt|eta|phi)/"])
['nMuon', 'Muon_pt', 'Muon_eta', 'Muon_phi']

y acumular datos gradualmente con uproot.TTree.iterate. Esto maneja entry_start/entry_stop en un bucle.

histmasa = hist.Hist(hist.axis.Regular(120, 0, 120, label="masa [GeV]"))

# Pon entry_stop igual a tree.num_entries para procesar todo el archivo, pero toma mucho tiempo
entry_stop = 250_000
total = min(entry_stop, tree.num_entries)

procesadas = 0
for muones in tree.iterate(filter_name=["nMuon", "/Muon_(pt|eta|phi)/"], step_size=50_000, entry_stop=entry_stop):
    corte = muones["nMuon"] == 2
    pt0 = muones["Muon_pt", corte, 0]
    pt1 = muones["Muon_pt", corte, 1]
    eta0 = muones["Muon_eta", corte, 0]
    eta1 = muones["Muon_eta", corte, 1]
    phi0 = muones["Muon_phi", corte, 0]
    phi1 = muones["Muon_phi", corte, 1]
    masa = np.sqrt(2 * pt0 * pt1 * (np.cosh(eta0 - eta1) - np.cos(phi0 - phi1)))
    histmasa.fill(masa)
    procesadas += len(muones)
    print(f"procesadas {procesadas} de {total} entradas")

histmasa.plot();
procesadas 50000 de 250000 entradas
procesadas 100000 de 250000 entradas
procesadas 150000 de 250000 entradas
procesadas 200000 de 250000 entradas
procesadas 250000 de 250000 entradas
<Figure size 640x480 with 1 Axes>

Pasar los datos a NumPy o Pandas

En todos los ejemplos anteriores, los métodos array, arrays e iterate devuelven Awkward Arrays. La librería Awkward Array es útil exactamente para este tipo de datos (arrays irregulares: más sobre esto en la próxima lección), pero es posible que estés trabajando con librerías que solo reconocen arrays de NumPy o DataFrames de Pandas.

Utiliza library="np" o library="pd" para obtener NumPy o Pandas, respectivamente.

tree["nMuon"].array(library="np", entry_stop=10_000)
array([2, 2, 1, ..., 2, 2, 2], shape=(10000,), dtype=uint32)
tree.arrays(library="np", entry_stop=10_000)
{'nMuon': array([2, 2, 1, ..., 2, 2, 2], shape=(10000,), dtype=uint32), 'Muon_pt': array([array([10.763697, 15.736523], dtype=float32), array([10.53849 , 16.327097], dtype=float32), array([3.2753265], dtype=float32), ..., array([30.238283, 13.035936], dtype=float32), array([17.35597 , 15.874119], dtype=float32), array([39.6421 , 42.273067], dtype=float32)], shape=(10000,), dtype=object), 'Muon_eta': array([array([ 1.0668273, -0.5637865], dtype=float32), array([-0.42778006, 0.34922507], dtype=float32), array([2.2108555], dtype=float32), ..., array([-1.1984524, -2.0278058], dtype=float32), array([-0.83613676, -0.8279834 ], dtype=float32), array([-2.090575 , -1.0396558], dtype=float32)], shape=(10000,), dtype=object), 'Muon_phi': array([array([-0.03427272, 2.5426154 ], dtype=float32), array([-0.2747921, 2.5397813], dtype=float32), array([-1.2234136], dtype=float32), ..., array([-2.2813563 , 0.60287297], dtype=float32), array([-1.4231573, -1.4103615], dtype=float32), array([ 2.2101276, -0.9990832], dtype=float32)], shape=(10000,), dtype=object), 'Muon_mass': array([array([0.10565837, 0.10565837], dtype=float32), array([0.10565837, 0.10565837], dtype=float32), array([0.10565837], dtype=float32), ..., array([0.10565837, 0.10565837], dtype=float32), array([0.10565837, 0.10565837], dtype=float32), array([0.10565837, 0.10565837], dtype=float32)], shape=(10000,), dtype=object), 'Muon_charge': array([array([-1, -1], dtype=int32), array([ 1, -1], dtype=int32), array([1], dtype=int32), ..., array([ 1, -1], dtype=int32), array([ 1, -1], dtype=int32), array([ 1, -1], dtype=int32)], shape=(10000,), dtype=object)}
tree.arrays(library="pd", entry_stop=10_000)
Loading...

NumPy es excelente para datos no irregulares como el TBranch "nMuon", pero tiene que representar un número desconocido de muones por evento como un array de arrays de NumPy (es decir, objetos de Python).

Se puede hacer que Pandas represente múltiples partículas por evento colocando esta estructura en un pd.MultiIndex, pero no cuando el DataFrame contiene más de un tipo de partícula (por ejemplo, muones y electrones). Usa DataFrames separados para estos casos. Si ayuda, ten en cuenta que hay otra ruta hacia los DataFrames: leer los datos como un Awkward Array y llamar a ak.to_dataframe sobre él. (Algunos métodos usan más memoria que otros; Pandas tiende a consumir una cantidad de memoria inusualmente alta).

O usa Awkward Arrays (próxima lección).