With version 0.20, zfit prepares for a more stable and user-friendly interface. This guide will help you to upgrade your code to the new version and demonstrate the most significant changes. It is meant for people who are already familiar with zfit and want to upgrade their code.
Not all changes are everywhere reflected in the docs, help is highly appreciated!
from __future__ import annotations
import numpy as np
# standard imports
import zfit
import zfit.z.numpy as znp # use this "numpy-like" for mathematical operations
from zfit import z
# example usage of the new numpy-like
@z.function
def maximum(x, y):
return znp.maximum(x, y)
The largest news comes from parameters: the NameAlreadyTakenError
is gone (!). Multiple parameters with the same name can now be created and co-exist. The only limit is: they must not exist within the same PDF/loss etc., as this would lead to ambiguities.
param1 = zfit.Parameter("param1", 1, 0, 10)
param1too = zfit.Parameter("param1", 2, 0, 10)
# no error!
Many objects, including parameters, can now have a label. This label is purely human-readable and can be used for plotting, printing, etc. It can contain arbitrary characters.
The name
of objects still exists and will in a future version probably be used for identification purpose (in conjunction with serialization). Therefore, use label
for human-readable names and avoid name
for that purpose.
param1again = zfit.Parameter("param1", 3, 0, 10, label=r"$param_1$ [GeV$^2$]")
As explained in the github discussion thread there are major improvements and changes.
TruncatedPDF
supports multiple spaces as limits and achieves a similar, if not better, functionality.Space.limit
will be changed in the future. For forwards compatibility, use Space.v1.limit
(or similar methods) instead of Space.limit
. The old one is still available via Space.v0.limit
.obs1 = zfit.Space("obs1", -1, 1) # no tuple needed anymore
obs2 = zfit.Space("obs2", lower=-1, upper=1, label="observable two")
# create a space with multiple observables
obs12 = obs1 * obs2
# limits are now as one would naively expect, and area has been renamed to volume (some are tensors, but that doesn't matter: they behave like numpy arrays)
# this allows, for example, for a more intuitive way
np.linspace(*obs12.v1.limits, 7)
Data handling has been significantly simplified and streamlined.
numpy
arrays, tensors or pandas DataFrame
as input, using a Data
object is not necessary anymore (but convenient, as it cuts the data to the space)Data
objects can now be created without the specific constructors (e.g. zfit.Data.from_pandas
), but directly with the data. The constructors are still available for convenience and for more options.Data
objects are now stateless and offer instead with
-methods to change the data. For example, with_obs
, with_weights
(can be None
to have without weights) etc.SamplerData
has been overhauld and has now a more public API including a update_data
method that allows to change the data without creating a new object and without relying on a create_sampler
method from a PDF.zfit.data.concat
has been added to concatenate data objects, similar to pd.concat
.data1_obs1 = zfit.Data(np.random.uniform(0, 1, 1000), obs=obs1)
data1_obs2 = zfit.Data(np.random.uniform(0, 1, 1000), obs=obs2, label="My favourite $x$")
data2_obs1 = zfit.Data(np.random.normal(0, 1, 1000), obs=obs1)
# similar like pd.concat
data_obs12 = zfit.data.concat([data1_obs1, data1_obs2], axis="columns")
data_obs1 = zfit.data.concat([data1_obs1, data2_obs1], axis="index")
# data can be accessed with "obs" directly
Using a binned space has now the effect of creating a binned version. This happens for Data
and PDF
objects.
obs1_binned = obs1.with_binning(12)
data_binned = zfit.Data(np.random.normal(0, 0.2, 1000), obs=obs1_binned)
GeneralizedCB
, a more general version of the DoubleCB
that should be preferred in the future. Also a Voigt profile is available, Bernstein polynomials, QGauss, GaussExpTail, etc.TruncatedPDF
has been added to allow for a more flexible way of truncating a PDF. Any PDF can be converted to a truncated version using to_truncated
(which, by default, truncates to the limits of the space).plot
method that allows for a quick plotting of the PDF (it takes an "obs" argument that allows to simply project it!). This is still experimental and may changes, the main purpose is to allow for a quick check of the PDF in interactive environments. The function is fully compatible with matplotlib and takes an ax
argument, it also allows to pass through any keyword arguments to the plotting function.# all the new PDFs
# create a PDF
pdf = zfit.pdf.Gauss(
mu=zfit.Parameter("mu", 1.2), sigma=param1again, obs=obs1, extended=1000
) # using an extended PDF, the truncated pdf automatically rescales
pdf.plot.plotpdf() # quick plot
# truncate it
pdf_truncated = pdf.to_truncated(limits=(-0.7, 0.1))
pdf_truncated.plot.plotpdf()
# binned pdfs from space work like the data
gauss_binned = zfit.pdf.Gauss(mu=zfit.Parameter("mu", 2.5), sigma=param1again, obs=obs1_binned, extended=1000)
# as mentioned before, PDFs can be evaluated directly on numpy arrays or pandas DataFrames
pdf.pdf(data_obs1.to_pandas())
They stay mostly the same (apart from improvements behind the scenes and bugfixes).
Losses take now directly the data and the model, without the need of a Data
object (if the data is already cut to the space).
To use the automatic gradient, set gradient="zfit"
in the minimizer. This can speed up the minimization for larger fits.
The minimizer currently updates the parameter default values after each minimization. To keep this behavior, add update_params()
call after the minimization.
(experimentally, the update can be disabled with zfit.run.experimental_disable_param_update(True)
, this will probably be the default in the future. Pay attention that using this experimental features most likely breaks current scripts. Feedback on this new feature is highly welcome!)
loss = zfit.loss.ExtendedUnbinnedNLL(model=pdf, data=data2_obs1.to_pandas())
minimizer = zfit.minimize.Minuit(
gradient="zfit"
) # to use the automatic gradient -> can fail, but can speed up the minimization
result = minimizer.minimize(loss).update_params()
pdf.plot.plotpdf(full=False) # plot only the pdf, no labels
The result stays similar but can now be pickled like any object in zfit!
(this was not possible before, only after calling freeze
on the result)
This works directly using dill
(a library that extends pickle
), but can fail if the garbage collector is not run. Therefore, zfit provides a slightly modified dill
that can work as a drop-in replacement.
The objects can be saved and loaded again and used as before. Ideal to store the minimum of a fit and use it later for statistical treatments, plotting, etc.
result_serialized = zfit.dill.dumps(result)
result_deserialized = zfit.dill.loads(result_serialized)
result_deserialized.errors()
The values of the parameters can now be directly used as arguments in functions of PDFs/loss. Methods in the pdf also take the parameters as arguments.
The name of the argument has to match the name of the parameter given in initialization (or can also be the parameter itself).
from matplotlib import pyplot as plt
x = znp.linspace(*obs1.v1.limits, 1000)
plt.plot(x, pdf.pdf(x, params={"param1": 1.5}), label="param1=1.5")
plt.plot(x, pdf.pdf(x, params={param1again: 2.5}), label="param1=2.5")
plt.legend()
import tqdm
# similar for the loss
param1dict = result_deserialized.params[param1again]
param1min = param1dict["value"]
lower, upper = param1dict["errors"]["lower"], param1dict["errors"]["upper"]
x = np.linspace(param1min + 2 * lower, param1min + 2 * upper, 50)
y = []
param1again.floating = False # not minimized
for x_i in tqdm.tqdm(x):
param1again.set_value(x_i)
minimizer.minimize(loss).update_params() # set nuisance parameters to minimum
y.append(loss.value())
plt.plot(x, y)
# creating a PDF looks also different, but here we use the name of the parametrization and the axis (integers)
class MyGauss2D(zfit.pdf.ZPDF):
_PARAMS = ("mu", "sigma")
_N_OBS = 2
@zfit.supports() # this allows the params magic
def _unnormalized_pdf(self, x, params):
x0 = x[0] # this means "axis 0"
x1 = x[1] # this means "axis 1"
mu = params["mu"]
sigma = params["sigma"]
return znp.exp(-0.5 * ((x0 - mu) / sigma) ** 2) * x1 # fake, just for demonstration
gauss2D = MyGauss2D(mu=0.8, sigma=param1again, obs=obs12, label="2D Gaussian$^2$")
gauss2D.plot.plotpdf(obs="obs1") # we can project the 2D pdf to 1D