There are two main ways of looking at "binned fits"
Some templated fits with uniform binning, no analytic components and specific morphing and constraints fit into the HistFactory model, implemented in pyhf. These fits make a large portion of CMS and ATLAS analyses.
zfit can, in principle, reproduce them too. However, it's comparably inefficient, a lot of code and slow. The main purpose is to support anything that is beyond HistFactory.
import matplotlib.pyplot as plt
import mplhep
import numpy as np
import zfit
import zfit.z.numpy as znp # numpy-like backend interface
from zfit import z # backend, special functions
zfit.settings.set_seed(43)
zfit introduces binned equivalents to unbinned components and transformations from one to the other. For example:
SumPDF
-> BinnedSumPDF
Data
-> BinnedData
UnbinnedNLL
-> BinnedNLL
There are converters and new, histogram specific PDFs and methods.
Let's start with an example, namely a simple, unbinned fit that we want to perform binned.
normal_np = np.random.normal(loc=2., scale=1.3, size=10000)
obs = zfit.Space("x", -10, 10)
mu = zfit.Parameter("mu", 1., -4, 6)
sigma = zfit.Parameter("sigma", 1., 0.1, 10)
model_nobin = zfit.pdf.Gauss(mu, sigma, obs)
data_nobin = zfit.Data.from_numpy(obs, normal_np)
loss_nobin = zfit.loss.UnbinnedNLL(model_nobin, data_nobin)
minimizer = zfit.minimize.Minuit()
# make binned
nbins = 50
data = data_nobin.to_binned(nbins)
model = model_nobin.to_binned(data.space)
loss = zfit.loss.BinnedNLL(model, data)
result = minimizer.minimize(loss)
print(result)
result.hesse(name="hesse")
result.errors(name="errors")
print(result)
to_binned
creates a binned (and to_unbinned
an unbinned) version of objects. It takes a binned Space, a binning or (as above), an integer (in which case a uniform binning is created).
This creates implicitly a new, binned space.
obs_binned_auto = data.space
print(obs_binned_auto)
print(f"is_binned: {obs_binned_auto.is_binned}, binned obs binning: {obs_binned_auto.binning}")
print(f"is_binned: {obs.is_binned}, unbinned obs binning:{obs.binning}")
We can explicitly convert spaces, data and models to binned parts.
Either number of bins for uniform binning or explicit binning.
obs_binned = obs.with_binning(nbins)
print(obs_binned)
# or we can create binnings
binning_regular = zfit.binned.RegularBinning(nbins, -10, 10, name='x')
binning_variable = zfit.binned.VariableBinning([-10, -6, -1, -0.1, 0.4, 3, 10], name='x')
Since a binning contains all the information needed to create a Space, a binning can be used to define a space directly.
obs_binned_variable = zfit.Space(binning=binning_variable)
print(obs_binned_variable, obs_binned_variable.binning)
data_nobin.to_binned(obs_binned_variable)
model_nobin.to_binned(obs_binned_variable)
zfit keeps compatibility with Universal Histogram Interface (UHI) and libraries that implement it (boost-histogram, hist).
BinnedData
directly adheres to UHI (and has a to_hist
attribute)BinnedPDF
has a to_binneddata
and to_hist
attributeWhere a BinnedData
object is expected, a (named) UHI object is also possible. Same goes for the binning axes.
h = model.to_hist()
h_scaled = h * [10_000]
Binned data has counts
, values
and variances
attributes, it has a binning
(aliased with axes).
data.values()
A binned PDF has the same methods as the unbinned counterparts, namely pdf
, integrate
(and their ext_*
parts) and sample
that can respond to binned as well as unbinned data.
Additionally, there are two more methods, namely
counts
returns the absolute counts as for a histogram. Equivalent to ext_pdf
, ext_integrate
, this only works if the PDF is extended.rel_counts
relative counts, like a histogram, but the sum is normalized to 1Counts are the integrated density, i.e. they differ by a factor bin_width
. For regular binning, this is "just" a constant factor, as it's the same for all bins,
but for Variable binning, this changes "the shape" of the histogram.
binned_sample = model.sample(n=1_000)
This allows plotting to become a lot easier using mplhep
, also for unbinned models.
plt.title("Counts plot")
mplhep.histplot(data, label="data")
mplhep.histplot(model.to_hist() * [data.nevents],
label="model") # scaling up since model is not extended, i.e. has no yield
plt.legend()
plt.title("Counts plot")
mplhep.histplot(binned_sample, label="sampled data")
mplhep.histplot(model.to_hist() * [binned_sample.nevents],
label="model") # scaling up since model is not extended, i.e. has no yield
plt.legend()
# or using unbinned data points, we can do a density plot
plt.title("Density plot")
mplhep.histplot(data.to_hist(), density=True, label="data")
x = znp.linspace(-10, 10, 200)
plt.plot(x, model.pdf(x), label="model")
plt.legend()
We used above the BinnedNLL
, but zfit offers more, namely an extended version and a BinnedChi2 (or least-square).
print(zfit.loss.__all__)
There are a few new PDFs that are specific to histogram-like shapes, such as morphing interpolation and shape variations.
Most simple a HistogramPDF wraps a histogram and acts as a PDF.
By default, these histograms are extended automatically (which can be overruled using the extended
argument)
histpdf = zfit.pdf.HistogramPDF(h_scaled) # fixed yield
print(np.sum(histpdf.counts()))
sig_yield = zfit.Parameter('sig_yield', 4_000, 0, 100_000)
histpdf = zfit.pdf.HistogramPDF(h, extended=sig_yield)
print(np.sum(histpdf.counts()))
We may want to add modifiers, i.e. scale each bin by a value. BinwiseScaleModifier
offers this functionality.
Note however that these are just free parameters and not in any way constraint. This needs to be done manually.
histpdf.space.binning.size
sig_pdf = zfit.pdf.BinwiseScaleModifier(histpdf,
modifiers=True) # or we could give a list of parameters matching each bin
modifiers = sig_pdf.params
# modifiers = {f'modifier_{i}': zfit.Parameter(f'modifier_{i}', 1, 0, 10) for i in range(histpdf.space.binning.size[0])}
# histpdf_scaled = zfit.pdf.BinwiseScaleModifier(histpdf, modifiers=modifiers)
modifiers
sig_pdf.get_yield()
Let's create a background from simulation. Let's assume, we have a parameter in the simulation that we're unsure about.
A common used technique is to use "morphing": creating multiple templates and interpolating between them. Typically, they are created at +1 and -1 sigma of the nuisance parameter (however, zfit allows arbitrary values and as many as wanted)
bkg_hist = zfit.Data(np.random.exponential(scale=20, size=100_000) - 10, obs=obs_binned)
bkg_hist_m1 = zfit.Data.from_numpy(obs=obs,
array=np.random.exponential(scale=35, size=100_000) - 10).to_binned(
obs_binned)
bkg_hist_m05 = zfit.Data.from_numpy(obs=obs,
array=np.random.exponential(scale=26, size=100_000) - 10).to_binned(
obs_binned)
bkg_hist_p1 = zfit.Data.from_numpy(obs=obs,
array=np.random.exponential(scale=17, size=100_000) - 10).to_binned(
obs_binned)
bkg_hists = {-1: bkg_hist_m1, -0.5: bkg_hist_m05, 0: bkg_hist, 1: bkg_hist_p1}
bkg_histpdfs = {k: zfit.pdf.HistogramPDF(v) for k, v in bkg_hists.items()}
mplhep.histplot(list(bkg_hists.values()), label=list(bkg_hists.keys()))
plt.legend();
alpha = zfit.Parameter("alpha", 0, -3, 3)
bkg_yield = zfit.Parameter("bkg_yield", 15_000)
bkg_pdf = zfit.pdf.SplineMorphingPDF(alpha, bkg_histpdfs, extended=bkg_yield)
with alpha.set_value(-0.6): # we can change this value to play around
mplhep.histplot(bkg_pdf.to_hist())
bkg_pdf = zfit.pdf.HistogramPDF(bkg_hist, extended=bkg_yield) # we don't use the spline for simplicity
model = zfit.pdf.BinnedSumPDF([sig_pdf, bkg_pdf])
model.to_hist()
with zfit.param.set_values([alpha] + list(modifiers.values()),
[0.] + list(np.random.normal(1.0, scale=0.14, size=len(modifiers)))):
data = bkg_pdf.sample(n=10_000).to_hist() + sig_pdf.sample(1000).to_hist()
data
modifier_constraints = zfit.constraint.GaussianConstraint(params=list(modifiers.values()), observation=np.ones(len(modifiers)),
uncertainty=0.1 * np.ones(len(modifiers)))
alpha_constraint = zfit.constraint.GaussianConstraint(alpha, 0, sigma=1)
loss_binned = zfit.loss.ExtendedBinnedNLL(model, data, constraints=[modifier_constraints, alpha_constraint])
result = minimizer.minimize(loss_binned)
print(result)
mplhep.histplot(model.to_hist(), label='model')
mplhep.histplot(data, label='data')
plt.legend()
print(sig_pdf.get_yield())
We can convert a histogram directly to an unbinned PDF with to_unbinned
or smooth it out by interpolating with splines.
unbinned_spline = zfit.pdf.SplinePDF(sig_pdf)
plt.plot(x, unbinned_spline.pdf(x))
mplhep.histplot(sig_pdf.to_hist(), density=True)
As before, we can now use hepstats to do further statistical treatment (supports binned PDFs).
More tutorials on hepstats can be found in the zfit guides or in the hepstats tutorials