19.5 PyROOT and Matplotlib
Table of Contents
Combining ROOT analysis with Python plotting
Using PyROOT you can treat ROOT as a powerful data and I/O backend while relying on Matplotlib for flexible and familiar plotting. This is especially attractive if you already use the scientific Python stack and want ROOT’s file format, TTrees, and statistics, but prefer Python-style plots.
In this chapter the focus is on how to move data and results from ROOT objects into forms that Matplotlib understands, then build plots with Matplotlib. Concepts such as reading ROOT files, creating histograms, or using NumPy with PyROOT are covered elsewhere, so here they are only used as ingredients.
Getting ROOT data into NumPy
Matplotlib expects data in simple containers such as Python lists or, more commonly, NumPy arrays. The basic strategy is therefore:
- Use PyROOT to read or compute something.
- Convert the result into NumPy arrays.
- Pass those arrays to Matplotlib plotting functions.
Many ROOT classes support a direct or convenient conversion pathway.
For a TTree you can use the TTree.AsMatrix (older) or RDataFrame.AsNumpy interface. For example, if you have a TTree named t with branches x and y, you can write in Python:
import ROOT
import numpy as np
f = ROOT.TFile.Open("data.root")
t = f.Get("tree")
# Using RDataFrame for modern ROOT
df = ROOT.RDataFrame(t)
np_dict = df.AsNumpy(["x", "y"])
x = np_dict["x"]
y = np_dict["y"]
The variables x and y are now NumPy arrays suitable for any Matplotlib function. This approach is very convenient for 1D and 2D distributions, scatter plots, or correlation studies.
Histograms require a slightly different approach. A ROOT histogram stores bin contents and bin edges. Matplotlib’s most common plotting functions use arrays of bin centers or edges. You can extract both from a TH1 using something like:
h = f.Get("h_energy") # TH1F or TH1D
nbins = h.GetNbinsX()
edges = np.array([h.GetBinLowEdge(i) for i in range(1, nbins + 2)])
contents = np.array([h.GetBinContent(i) for i in range(1, nbins + 1)])
errors = np.array([h.GetBinError(i) for i in range(1, nbins + 1)])
Here edges has length nbins + 1, while contents and errors have length nbins. You can convert edges to bin centers if you prefer:
centers = 0.5 * (edges[:-1] + edges[1:])This pattern also works for 2D histograms, except that you need to loop separately over x and y bins, then reshape arrays. For more complex objects, such as graphs or profiles, you access the underlying arrays in a similar way through their getters.
When converting ROOT histograms to NumPy for Matplotlib, always be explicit about bin definitions. Use GetBinLowEdge, GetBinWidth, and consistent indexing so that the x range of your Matplotlib plot matches the original ROOT histogram range.
Plotting ROOT histograms with Matplotlib
Once you have the bin contents and edges, you can build Matplotlib plots that faithfully reproduce the ROOT histogram. For simple visual checks, it often suffices to draw bin contents as a step plot.
A typical 1D example looks like this:
import matplotlib.pyplot as plt
# centers, contents, and errors extracted as shown above
plt.figure()
plt.errorbar(
centers,
contents,
yerr=errors,
fmt="o",
markersize=3,
linestyle="none",
label="Data"
)
plt.step(edges[:-1], contents, where="post", color="C0", alpha=0.6)
plt.xlabel("Energy [MeV]")
plt.ylabel("Counts per bin")
plt.legend()
plt.tight_layout()
plt.show()
The combination of errorbar and step gives you both clear marker points with uncertainties and the underlying binned structure. If you want a purely binned look without markers, you can omit errorbar and use only step.
Sometimes you want to compare several ROOT histograms on the same axes, but still use Matplotlib. In that case, extract centers and contents for each histogram, then plot them together:
centers1, contents1 = ...
centers2, contents2 = ...
plt.figure()
plt.step(centers1, contents1, where="mid", label="Sample A", color="C0")
plt.step(centers2, contents2, where="mid", label="Sample B", color="C1")
plt.xlabel("Observable X")
plt.ylabel("Events")
plt.legend()
plt.tight_layout()
plt.show()
Using where="mid" with centers produces a clean overlay if the binning is identical. If the bins differ, use the original edges for each histogram to keep them accurate.
You can also reconstruct ROOT-style logarithmic axes by calling Matplotlib’s log methods:
plt.yscale("log")
plt.xscale("log") # if neededThese behave similarly to the log options you might use on ROOT canvases.
Plotting functions and fit results
ROOT functions and fit results are often central to an analysis, and Matplotlib can display them if you evaluate the fitted model on a NumPy grid. The typical workflow is:
- Fit a ROOT histogram or graph in PyROOT.
- Get the fitted TF1 (or define a TF1 yourself).
- Sample the function on a dense grid of x values.
- Plot both data and model in Matplotlib.
Suppose you fit a Gaussian to a ROOT histogram h using PyROOT:
f_gaus = ROOT.TF1("f_gaus", "gaus", 0, 10)
h.Fit(f_gaus, "Q") # quiet fitYou can then evaluate the fitted function in Python:
x_vals = np.linspace(0, 10, 400)
y_vals = np.array([f_gaus.Eval(x) for x in x_vals])Finally, plot everything:
plt.figure()
plt.errorbar(centers, contents, yerr=errors, fmt="o", markersize=3, label="Data")
plt.plot(x_vals, y_vals, "r-", label="Gaussian fit")
plt.xlabel("x")
plt.ylabel("Events")
plt.legend()
plt.tight_layout()
plt.show()If you have multiple fitted components, such as signal and background parts, you can define separate TF1 objects in ROOT and evaluate each one on the same x grid. You can then display them with different line styles in Matplotlib, which is convenient for publication-quality decomposition plots.
For TGraph or TGraphErrors, you can convert the graph directly into NumPy arrays and let Matplotlib take over:
g = f.Get("g_data") # TGraphErrors
n = g.GetN()
x = np.array([g.GetPointX(i) for i in range(n)])
y = np.array([g.GetPointY(i) for i in range(n)])
ex = np.array([g.GetErrorX(i) for i in range(n)])
ey = np.array([g.GetErrorY(i) for i in range(n)])
plt.figure()
plt.errorbar(x, y, xerr=ex, yerr=ey, fmt="o", markersize=3)
plt.xlabel("X")
plt.ylabel("Y")
plt.tight_layout()
plt.show()This preserves the original measurement uncertainties while giving you Matplotlib’s control over fonts, colors, and export formats.
Coordinating ROOT and Matplotlib in one analysis
To keep your analysis maintainable, it is helpful to separate responsibilities. ROOT is particularly strong at reading ROOT files, working with TTrees, and producing histograms and fits. Matplotlib excels at styling and exporting figures as part of a Python ecosystem. A clean structure is often:
- Use PyROOT and possibly ROOT’s RDataFrame to read data and compute histograms, profiles, and fit results.
- Extract results to NumPy or simple Python containers.
- Pass these containers to Matplotlib-only plotting functions.
You might implement this separation in code by placing the ROOT-specific logic in dedicated functions or modules, and the plotting logic in another. For example, you can have a function that takes no ROOT types as arguments, only plain arrays:
def plot_spectrum(centers, counts, errors, model_x=None, model_y=None):
import matplotlib.pyplot as plt
plt.figure()
plt.errorbar(centers, counts, yerr=errors, fmt="o", markersize=3, label="Data")
if model_x is not None:
plt.plot(model_x, model_y, "r-", label="Model")
plt.xlabel("Energy [MeV]")
plt.ylabel("Events")
plt.legend()
plt.tight_layout()
plt.show()The calling code can stay inside a PyROOT environment, but it only needs to pass NumPy arrays. This pattern makes it easier to test and reuse the plotting part independently from the ROOT analysis.
When you combine ROOT and Matplotlib, there are a few practical details to keep in mind. ROOT has its own GUI event loop, and Matplotlib also interacts with windowing backends. To avoid conflicts, it is often simplest to run PyROOT with graphics disabled or to avoid using ROOT canvases and Draw() in the same script where you use interactive Matplotlib windows. Alternatively, you can treat ROOT as a pure computational and I/O library, never calling TCanvas or Draw(), and let Matplotlib manage all graphics.
When using PyROOT and Matplotlib together, avoid mixing ROOT’s GUI event loop with Matplotlib’s interactive windows in the same script. Prefer a workflow where ROOT is used for data handling and computation, and Matplotlib is used exclusively for plotting. This reduces conflicts and makes your code more portable.
Finally, Matplotlib integrates very well with other Python tools such as Jupyter notebooks and LaTeX typesetting. By converting ROOT results to NumPy arrays and plotting with Matplotlib, you can place complex ROOT-based analyses into a wider Python workflow, while still keeping the strengths of ROOT for data handling and physics analysis.
Views: 10
KAHIBARO