Fits with shared parameters

We demonstrate how to simultaneously fit two datasets with different models that shares a common parameter.

[1]:
from iminuit import Minuit
from iminuit.cost import UnbinnedNLL
from iminuit.util import describe
from matplotlib import pyplot as plt
import numpy as np
from numba_stats import norm
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
File __init__.pxd:942, in numpy.import_array()

RuntimeError: module compiled against API version 0x10 but this version of numpy is 0xf

During handling of the above exception, another exception occurred:

ImportError                               Traceback (most recent call last)
Input In [1], in <cell line: 6>()
      4 from matplotlib import pyplot as plt
      5 import numpy as np
----> 6 from numba_stats import norm

File ~/python-iminuit/src/python-iminuit/test-env/lib/python3.10/site-packages/numba_stats/norm.py:9, in <module>
      1 """
      2 Normal distribution.
      3
   (...)
      6 scipy.stats.norm: Scipy equivalent.
      7 """
      8 import numpy as np
----> 9 from ._special import erfinv as _erfinv
     10 from ._util import _jit, _trans, _generate_wrappers, _prange
     11 from math import erf as _erf

File ~/python-iminuit/src/python-iminuit/test-env/lib/python3.10/site-packages/numba_stats/_special.py:7, in <module>
      5 from numba.extending import get_cython_function_address
      6 from numba.types import WrapperAddressProtocol, float64
----> 7 import scipy.special.cython_special as cysp
     10 def get(name, signature):
     11     # create new function object with correct signature that numba can call by extracting
     12     # function pointer from scipy.special.cython_special; uses scipy/cython internals
     13     index = 1 if signature.return_type is float64 else 0

File /usr/lib/python3.10/site-packages/scipy/special/__init__.py:649, in <module>
      1 """
      2 ========================================
      3 Special functions (:mod:`scipy.special`)
   (...)
    644
    645 """
    647 from ._sf_error import SpecialFunctionWarning, SpecialFunctionError
--> 649 from . import _ufuncs
    650 from ._ufuncs import *
    652 from . import _basic

File /usr/lib/python3.10/site-packages/scipy/special/_ufuncs.pyx:1, in init scipy.special._ufuncs()

File scipy/special/_ufuncs_extra_code_common.pxi:34, in init scipy.special._ufuncs_cxx()

File __init__.pxd:944, in numpy.import_array()

ImportError: numpy.core.multiarray failed to import
[2]:
# generate two data sets which are fitted simultaneously
rng = np.random.default_rng(1)

width = 2.0
data1 = rng.normal(0, width, size=1000)
data2 = rng.normal(5, width, size=1000)
[3]:
# use two pdfs with different names for non-shared parameters,
# so that they are fitted independently

def pdf1(x, μ_1, σ):
    return norm.pdf(x, μ_1, σ)

def pdf2(x, μ_2, σ):
    return norm.pdf(x, μ_2, σ)

# combine two log-likelihood functions by adding them
lh = UnbinnedNLL(data1, pdf1) + UnbinnedNLL(data2, pdf2)

print(f"{describe(lh)=}")
describe(lh)=['μ_1', 'σ', 'μ_2']

The σ parameter is shared between the data sets, while the means of the two normal distributions are independently fitted.

[4]:
def plot(cost, xe, minuit, ax, **style):
    signature = describe(cost)
    data = cost.data

    values = minuit.values[signature]
    errors = minuit.errors[signature]

    cx = (xe[1:] + xe[:-1]) / 2

    ym = np.diff(norm.cdf(xe, *values)) * np.sum(w)
    t = []
    for n, v, e in zip(signature, values, errors):
        t.append(f"${n} = {v:.3f} ± {e:.3f}$")
    ax.plot(cx, ym, label="\n".join(t), **style)
[5]:
m = Minuit(lh, μ_1=1, μ_2=2, σ=1)

fig, ax = plt.subplots(1, 2, figsize=(14, 5))

hists = [np.histogram(lhi.data, bins=50) for lhi in lh]

# draw data and model with initial parameters
for lhi, (w, xe), axi in zip(lh, hists, ax):
    cx = (xe[1:] + xe[:-1]) / 2
    axi.errorbar(cx, w, np.sqrt(w), fmt="ok", capsize=0, zorder=0)
    plot(lhi, xe, m, axi, ls="--")

m.migrad()

# draw model with fitted parameters
for lhi, (w, xe), axi in zip(lh, hists, ax):
    plot(lhi, xe, m, axi)
    axi.legend()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [5], in <cell line: 8>()
      9     cx = (xe[1:] + xe[:-1]) / 2
     10     axi.errorbar(cx, w, np.sqrt(w), fmt="ok", capsize=0, zorder=0)
---> 11     plot(lhi, xe, m, axi, ls="--")
     13 m.migrad()
     15 # draw model with fitted parameters

Input In [4], in plot(cost, xe, minuit, ax, **style)
      6 errors = minuit.errors[signature]
      8 cx = (xe[1:] + xe[:-1]) / 2
---> 10 ym = np.diff(norm.cdf(xe, *values)) * np.sum(w)
     11 t = []
     12 for n, v, e in zip(signature, values, errors):

NameError: name 'norm' is not defined
../_images/notebooks_simultaneous_fits_6_1.svg

The dashed line shows the initial model before the fit, the solid line shows the model after the fit. Note that the σ parameter is shared.