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
Cualquier instancia de clase (ROOT TObject, uproot.Model) puede almacenarse en un directorio, incluidos tipos como histogramas (por ejemplo, ROOT TH1, uproot
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, uprootextend 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.

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
# sin entry_stop, tomará mucho tiempo e incluso podría fallar
tree["nMuon"].array(entry_stop=10_000)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)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 uprootentry_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
)
muonesAhora 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"]muones["Muon_eta"]muones["Muon_phi"] # etc.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)(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();
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 uprootentry_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

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)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).