12.11. cost functions comparison: exercise 12.4#

12.11.1. Exercise 12.4#

Starting from samples of pseudo-random numbers generated according to a Gaussian probability density function, using the technique of the toy experiments study the distribution of the minimum of the cost function used by iminuit in Gaussian fits.

  • Does it match a \(\chi^2\) distribution for any number of events, for the cases of maximum likelihood, extended maximum likelihood, and least squares?

from matplotlib import pyplot as plt
import numpy as np
from myrand import generate_TCL_ms
from iminuit import Minuit
from scipy.stats import norm
from math import floor, ceil, sqrt

# for the binning choice
def sturges (N) :
    return floor (1 + np.log2 (N))

# parameters valid for all tests
N_toys = 5000
sample_size = 500    
bins = sturges (sample_size)

12.11.1.1. binned likelihood#

from iminuit.cost import BinnedNLL

def mod_signal_bin_LL (bin_edges, mu, sigma) :
    ''' fitting model for binned likelihood'''
    return norm.cdf (bin_edges, mu, sigma)
        
means_bin_LL = []
sigmas_bin_LL = []
Q2_bin_LL = []
N_dof_LL = 0

for iToy in range (N_toys) :
    subsample = generate_TCL_ms (1., 0.7, sample_size)
    bin_content, bin_edges = np.histogram (subsample, bins, 
                                           range = (floor (min (subsample)), ceil (max (subsample))))

    my_cost_func_bin_LL = BinnedNLL (bin_content, bin_edges, mod_signal_bin_LL)
    my_minuit_bin_LL = Minuit (
                            my_cost_func_bin_LL, 
                            mu = np.mean (subsample), 
                            sigma = np.std (subsample),
                           )
    my_minuit_bin_LL.limits['sigma'] = (0, None)
    my_minuit_bin_LL.migrad ()
    if not my_minuit_bin_LL.valid : continue
    means_bin_LL.append (my_minuit_bin_LL.values['mu'])
    sigmas_bin_LL.append (my_minuit_bin_LL.values['sigma'])
    Q2_bin_LL.append (my_minuit_bin_LL.fval)
    if (iToy == 0) : N_dof_LL = my_minuit_bin_LL.ndof

12.11.1.2. extended binned likelihood#

from iminuit.cost import ExtendedBinnedNLL

def mod_signal_bin_ext_LL (bin_edges, N_signal, mu, sigma) :
    ''' fitting model for extended binned likelihood'''
    return N_signal * norm.cdf (bin_edges, mu, sigma)
        
means_bin_ext_LL = []
sigmas_bin_ext_LL = []
Q2_bin_ext_LL = []
N_dof_ext_LL = 0

for iToy in range (N_toys) :
    subsample = generate_TCL_ms (1., 0.7, sample_size)
    bin_content, bin_edges = np.histogram (subsample, bins, 
                                           range = (floor (min (subsample)), ceil (max (subsample))))

    my_cost_func_bin_ext_LL = ExtendedBinnedNLL (bin_content, bin_edges, mod_signal_bin_ext_LL)
    my_minuit_bin_ext_LL = Minuit (
                            my_cost_func_bin_ext_LL, 
                            N_signal = len (subsample),
                            mu = np.mean (subsample), 
                            sigma = np.std (subsample),
                           )
    my_minuit_bin_ext_LL.limits['N_signal', 'sigma'] = (0, None)
    my_minuit_bin_ext_LL.migrad ()
    if not my_minuit_bin_ext_LL.valid : continue
    means_bin_ext_LL.append (my_minuit_bin_ext_LL.values['mu'])
    sigmas_bin_ext_LL.append (my_minuit_bin_ext_LL.values['sigma'])
    Q2_bin_ext_LL.append (my_minuit_bin_ext_LL.fval)
    if (iToy == 0) : N_dof_ext_LL = my_minuit_bin_ext_LL.ndof

12.11.1.3. least squares#

from iminuit.cost import LeastSquares

def func_approx (x, N_events, mean, sigma, bin_width) :
    return N_events * norm.pdf (x, mean, sigma) * bin_width

means_LS = []
sigmas_LS = []
Q2_bin_LS = []
N_dof_LS = 0

for iToy in range (N_toys) :
    subsample = generate_TCL_ms (1., 0.7, sample_size)
    bin_content, bin_edges = np.histogram (subsample, bins, 
                                           range = (floor (min (subsample)), ceil (max (subsample))))
    bin_centres = 0.5 * (bin_edges[1:] + bin_edges[:-1])
    sigma_y = [max (sqrt (num), 1.) for num in bin_content]

    least_squares = LeastSquares (bin_centres, bin_content, sigma_y, func_approx)
    my_minuit_LS = Minuit (least_squares,
                           N_events = sample_size,
                           mean = np.mean (subsample), 
                           sigma = np.std (subsample),
                           bin_width = bin_centres[1] - bin_centres[0]
                          )
    my_minuit_LS.fixed["bin_width", "N_events"] = True
    my_minuit_LS.migrad ()
    my_minuit_LS.hesse ()
    if not my_minuit_LS.valid : continue
    means_LS.append (my_minuit_LS.values['mean'])
    sigmas_LS.append (my_minuit_LS.values['sigma'])
    Q2_bin_LS.append (my_minuit_LS.fval)
    if (iToy == 0) : N_dof_LS = my_minuit_LS.ndof
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[4], line 26
     19 my_minuit_LS = Minuit (least_squares,
     20                        N_events = sample_size,
     21                        mean = np.mean (subsample), 
     22                        sigma = np.std (subsample),
     23                        bin_width = bin_centres[1] - bin_centres[0]
     24                       )
     25 my_minuit_LS.fixed["bin_width", "N_events"] = True
---> 26 my_minuit_LS.migrad ()
     27 my_minuit_LS.hesse ()
     28 if not my_minuit_LS.valid : continue

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/iminuit/minuit.py:789, in Minuit.migrad(self, ncall, iterate, use_simplex)
    787 t = mutil._Timer(self._fmin)
    788 with t:
--> 789     fm = _robust_low_level_fit(
    790         self._fcn,
    791         self._last_state,
    792         replace_none(ncall, 0),
    793         self._strategy,
    794         self._tolerance,
    795         self._precision,
    796         iterate,
    797         use_simplex,
    798     )
    800 self._last_state = fm.state
    802 self._fmin = mutil.FMin(
    803     fm,
    804     "Migrad",
   (...)
    809     t.value,
    810 )

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/iminuit/minuit.py:2940, in _robust_low_level_fit(fcn, state, ncall, strategy, tolerance, precision, iterate, use_simplex)
   2938 if precision is not None:
   2939     migrad.precision = precision
-> 2940 fm = migrad(ncall, tolerance)
   2941 strategy = MnStrategy(2)
   2942 migrad = MnMigrad(fcn, fm.state, strategy)

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/iminuit/cost.py:632, in Cost.__call__(self, *args)
    617 def __call__(self, *args: float) -> float:
    618     """
    619     Evaluate the cost function.
    620 
   (...)
    630     float
    631     """
--> 632     r = self._value(args)
    633     if self.verbose >= 1:
    634         print(args, "->", r)

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/iminuit/cost.py:2289, in LeastSquares._value(self, args)
   2287 def _value(self, args: Sequence[float]) -> float:
   2288     y, ye = self._masked.T[self._ndim :]
-> 2289     ym = self._pred(args)
   2290     return self._cost(y, ye, ym)

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/iminuit/cost.py:2277, in LeastSquares._pred(self, args)
   2275 def _pred(self, args: Sequence[float]) -> NDArray:
   2276     x = self._masked.T[0] if self._ndim == 1 else self._masked.T[: self._ndim]
-> 2277     ym = self._model(x, *args)
   2278     return _normalize_output(ym, "model", self._ndata())

Cell In[4], line 4, in func_approx(x, N_events, mean, sigma, bin_width)
      3 def func_approx (x, N_events, mean, sigma, bin_width) :
----> 4     return N_events * norm.pdf (x, mean, sigma) * bin_width

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/scipy/stats/_distn_infrastructure.py:2100, in rv_continuous.pdf(self, x, *args, **kwds)
   2098 putmask(output, (1-cond0)+np.isnan(x), self.badvalue)
   2099 if np.any(cond):
-> 2100     goodargs = argsreduce(cond, *((x,)+args+(scale,)))
   2101     scale, goodargs = goodargs[-1], goodargs[:-1]
   2102     place(output, cond, self._pdf(*goodargs) / scale)

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/scipy/stats/_distn_infrastructure.py:580, in argsreduce(cond, *args)
    576     newargs = [newargs, ]
    578 if np.all(cond):
    579     # broadcast arrays with cond
--> 580     *newargs, cond = np.broadcast_arrays(*newargs, cond)
    581     return [arg.ravel() for arg in newargs]
    583 s = cond.shape

File <__array_function__ internals>:200, in broadcast_arrays(*args, **kwargs)

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/lib/stride_tricks.py:546, in broadcast_arrays(subok, *args)
    542 if all(array.shape == shape for array in args):
    543     # Common case where nothing needs to be broadcasted.
    544     return args
--> 546 return [_broadcast_to(array, shape, subok=subok, readonly=False)
    547         for array in args]

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/lib/stride_tricks.py:546, in <listcomp>(.0)
    542 if all(array.shape == shape for array in args):
    543     # Common case where nothing needs to be broadcasted.
    544     return args
--> 546 return [_broadcast_to(array, shape, subok=subok, readonly=False)
    547         for array in args]

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/numpy/lib/stride_tricks.py:349, in _broadcast_to(array, shape, subok, readonly)
    346     raise ValueError('all elements of broadcast shape must be non-'
    347                      'negative')
    348 extras = []
--> 349 it = np.nditer(
    350     (array,), flags=['multi_index', 'refs_ok', 'zerosize_ok'] + extras,
    351     op_flags=['readonly'], itershape=shape, order='C')
    352 with it:
    353     # never really has writebackifcopy semantics
    354     broadcast = it.itviews[0]

KeyboardInterrupt: 

12.11.1.4. compare the five estimates#

from scipy.stats import chi2

def plot_histo (ax, sample, ndof, label, color) :
    xMin = floor (min (sample))
    xMax = ceil (max (sample))
    N_bins = sturges (len (sample))
    bin_edges = np.linspace (xMin, xMax, N_bins)
    ax.set_xlabel ('Q2')
    ax.set_ylabel ('events per bin')
    ax.hist (sample,
             bins = bin_edges,
             color = color,
             label = label + ', ' + str (ndof),
            )
    vertical_limits = ax.get_ylim ()
    ax.plot ([ndof, ndof], vertical_limits, color = 'black')
    ax.legend ()

    x = np.arange (xMin, xMax, 0.001)
    bin_width = (xMax - xMin) / N_bins
    ax.plot (x, bin_width * len (sample) * chi2.pdf (x, df=ndof))
    ax.text (0.66 * xMax, len (sample) / 20., str (sum (sample) / len (sample)))
    
    
    
fig, axes = plt.subplots (nrows = 3, ncols = 1)
axes[0].set_title ('compare cost function minimum distributions', size=14)

plot_histo (axes[0], Q2_bin_LS, N_dof_LS, 'LS', 'red')
plot_histo (axes[1], Q2_bin_LL, N_dof_LL, 'LL', 'orange')
plot_histo (axes[2], Q2_bin_ext_LL, N_dof_ext_LL, 'ext LL', 'yellow')
../../../_images/e94d08ba9f1569a60cdfe871af03bdcd0d7ad3da5ed7942c381c60a06205c7b0.png