11.6. Least squares solutions#

11.6.1. Exercise 11.1#

After defining, in a dedicated library, a linear function \(\phi(x, \theta)\) with two parameters \(\theta\):

  • Write a program that generates a set of 10 pairs \((x_i, y_i)\) such that the points \(x_i\) are randomly distributed along the horizontal axis between 0 and 10, and the points \(y_i\) are constructed using the formula \(y_i = \phi(x_i, \theta) + \epsilon_i\).

  • Plot the obtained sample, including the expected error bars.

# the model to be used for the exercise

def func (x, m, q) :
    '''
    reference model to be fitted
    '''
    return m * x + q

# initial parameters of the problem

m_true = 0.5
q_true = 1.1
epsilon_sigma = 0.3
from matplotlib import pyplot as plt
import numpy as np
from myrand import generate_TCL_ms

epsilons = generate_TCL_ms (0., epsilon_sigma, 10)

x_coord = np.arange (0, 10, 1)
y_coord = np.zeros (10)
for i in range (x_coord.size) :
    y_coord[i] = func (x_coord[i], m_true, q_true) + epsilons[i]

sigma_y = epsilon_sigma * np.ones (len (y_coord))
    
fig, ax = plt.subplots ()
ax.set_title ('linear model', size=14)
ax.set_xlabel ('x')
ax.set_ylabel ('y')
ax.errorbar (x_coord, y_coord, xerr = 0.0, yerr = sigma_y, linestyle = 'None', marker = 'o') 
plt.show ()
../../../_images/7de2bff68ff9558313d2b1186149b386df7a7a167ab74aebb7572c26014866fc.png

11.6.2. Exercise 11.2#

Use the iMinuit library to perform a fit on the simulated sample.

  • Check if the fit was successful.

  • Print the values of the determined parameters and their sigmas on the screen.

from iminuit import Minuit
from iminuit.cost import LeastSquares

# generate a least-squares cost function
least_squares = LeastSquares (x_coord, y_coord, sigma_y, func)
my_minuit = Minuit (least_squares, m = 0, q = 0)  # starting values for m and q
my_minuit.migrad ()  # finds minimum of least_squares function
my_minuit.hesse ()   # accurately computes uncertainties
# NB: adding additional instructions prevents the automatic visualisation of the fit result
Migrad
FCN = 6.225 (χ²/ndof = 0.8) Nfcn = 40
EDM = 3.64e-21 (Goal: 0.0002)
Valid Minimum Below EDM threshold (goal x 10)
No parameters at limit Below call limit
Hesse ok Covariance accurate
Name Value Hesse Error Minos Error- Minos Error+ Limit- Limit+ Fixed
0 m 0.511 0.033
1 q 1.04 0.18
m q
m 0.00109 -0.0049 (-0.843)
q -0.0049 (-0.843) 0.0311
2024-11-17T07:55:51.757037 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/
# information about the function minimum can be directly accessed with Minuit.fmin
my_minuit.fmin
Migrad
FCN = 6.225 (χ²/ndof = 0.8) Nfcn = 40
EDM = 3.64e-21 (Goal: 0.0002)
Valid Minimum Below EDM threshold (goal x 10)
No parameters at limit Below call limit
Hesse ok Covariance accurate
# global characteristics of the fit
is_valid = my_minuit.valid
print ('success of the fit: ', is_valid)
success of the fit:  True
# fit parameters
for par, val, err in zip (my_minuit.parameters, my_minuit.values, my_minuit.errors) :
    print(f'{par} = {val:.3f} +/- {err:.3f}') # formatted output
    
m_fit = my_minuit.values[0]
q_fit = my_minuit.values[1]
m = 0.511 +/- 0.033
q = 1.044 +/- 0.176

11.6.3. Exercise 11.3#

  • Calculate the value of \(Q^2\) using the points and the fitted function obtained in the previous exercise.

  • Compare the value obtained with iminuit with the calculated one.

  • Print the value of the degrees of freedom of the fit

Q_squared = my_minuit.fval
print ('value of the fit Q-squared', Q_squared)
N_dof = my_minuit.ndof
print ('value of the number of degrees of freedom', N_dof)
value of the fit Q-squared 6.224740068177643
value of the number of degrees of freedom 8.0
Q_squared_calc = 0.
for x, y, ey in zip (x_coord, y_coord, sigma_y) :
    Q_squared_calc = Q_squared_calc + pow ( (y - func (x, m_fit, q_fit)) /ey , 2 )  
print ('Difference between Q-squared values:', Q_squared_calc - Q_squared) 
Difference between Q-squared values: 8.881784197001252e-16

11.6.4. Exercise 11.4#

Using the toy experiments technique, generate 10,000 fit experiments with the model studied in the previous exercises and fill a histogram with the obtained values of \(Q^2\).

  • Compare the expected value of \(Q^2\) obtained from the toy experiments with the degrees of freedom of the problem.

from math import floor
from stats import stats

N_toys = 10000
Q_squares = []
x_coord_toy = np.arange (0, 10, 1)
y_coord_toy = np.zeros (10)

for i_toy in range (N_toys) :
    epsilons_toy = generate_TCL_ms (0., epsilon_sigma, 10)
    for i in range (x_coord_toy.size) :
        y_coord_toy[i] = func (x_coord_toy[i], m_true, q_true) + epsilons_toy[i]
    least_squares = LeastSquares (x_coord_toy, y_coord_toy, sigma_y, func)
    my_minuit_toy = Minuit (least_squares, m = 0, q = 0)  # starting values for m and q
    my_minuit_toy.migrad ()  # finds minimum of least_squares function
    my_minuit_toy.hesse ()   # accurately computes uncertainties
    if my_minuit_toy.valid : 
        Q_squares.append (my_minuit_toy.fval)

fig, ax = plt.subplots ()
ax.set_title ('Q-squared distribution', size=14)
ax.set_xlabel('q_squared')
ax.set_ylabel('events in bin')
bin_edges = np.linspace (0, 4 * N_dof, floor (N_toys/100))   # edges o the histogram bins
ax.hist (Q_squares,
         bins = bin_edges,
         color = 'orange',
        )
plt.show ()

Q_squares_stats = stats (Q_squares)
print ('average Q_squared expected value:', Q_squares_stats.mean ())
../../../_images/09d13ce27a15010b41c0e34fd12bb321886140564bd3947f0d8af2a6e6e69ebb.png
average Q_squared expected value: 7.993422327691504

11.6.5. Exercise 11.5#

Modify the previous program by deliberately changing the experimental uncertainty associated with the points \(y_i\) in the sample and verify that it’s possible to recover the uncertainty used in generating the points through the expected value of the variable \(Q^2\).

from math import floor, sqrt
from stats import stats

N_toys = 10000
Q_squares_mod = []
x_coord_toy = np.arange (0, 10, 1)
y_coord_toy = np.zeros (10)
epsilon_sigma_mod = 0.4 * epsilon_sigma

for i_toy in range (N_toys) :
    epsilons_toy = generate_TCL_ms (0., epsilon_sigma_mod, 10)
    for i in range (x_coord_toy.size) :
        y_coord_toy[i] = func (x_coord_toy[i], m_true, q_true) + epsilons_toy[i]
    least_squares = LeastSquares (x_coord_toy, y_coord_toy, sigma_y, func)
    my_minuit_toy = Minuit (least_squares, m = 0, q = 0)  # starting values for m and q
    my_minuit_toy.migrad ()  # finds minimum of least_squares function
    my_minuit_toy.hesse ()   # accurately computes uncertainties
    if my_minuit_toy.valid : 
        Q_squares_mod.append (my_minuit_toy.fval)

Q_squares_mod_stats = stats (Q_squares_mod)
print ('average Q_squared expected value:', Q_squares_mod_stats.mean ())
print ('sigma scale factor:', sqrt (Q_squares_mod_stats.mean () / N_dof))
print ('sigma:', epsilon_sigma_mod / sqrt (Q_squares_mod_stats.mean () / N_dof))
average Q_squared expected value: 1.2915964154792148
sigma scale factor: 0.40180785449627765
sigma: 0.2986502096889987

11.6.6. Exercise 11.6#

Add to Exercise 11.3 the screen printout of the entire covariance matrix of the fit parameters.

print (my_minuit.covariance)
┌───┬─────────────────┐
│   │       m       q │
├───┼─────────────────┤
│ m │ 0.00109 -0.0049 │
│ q │ -0.0049  0.0311 │
└───┴─────────────────┘
print ('variance of the first parameter (m):', my_minuit.covariance[0][0])
print ('variance of the second parameter (q):', my_minuit.covariance[1][1])
print ('covariance of the two parameters:', my_minuit.covariance[1][0])
variance of the first parameter (m): 0.0010909090832466714
variance of the second parameter (q): 0.03109090827845803
covariance of the two parameters: -0.004909090837587764

11.6.7. Exercise 11.7#

Repeat the fitting exercise for a parabolic trend.

def para (x, a, b, c) :
    '''
    reference model to be fitted
    '''
    return a + b * x + c * x * x

# initial parameters of the problem

a_true = -1.
b_true = 0.7
c_true = 0.3

epsilon_sigma = 1.5
epsilons = generate_TCL_ms (0., epsilon_sigma, 10)

x_coord = np.arange (0, 10, 1)
y_coord = np.zeros (10)
for i in range (x_coord.size) :
    y_coord[i] = para (x_coord[i], a_true, b_true, c_true) + epsilons[i]

sigma_y = epsilon_sigma * np.ones (len (y_coord))

least_squares = LeastSquares (x_coord, y_coord, sigma_y, para)
my_minuit = Minuit (least_squares, a = 0., b = 0., c = 0.)
my_minuit.migrad ()  # finds minimum of least_squares function
my_minuit.hesse ()   # accurately computes uncertainties
Migrad
FCN = 6.529 (χ²/ndof = 0.9) Nfcn = 70
EDM = 1.05e-19 (Goal: 0.0002)
Valid Minimum Below EDM threshold (goal x 10)
No parameters at limit Below call limit
Hesse ok Covariance accurate
Name Value Hesse Error Minos Error- Minos Error+ Limit- Limit+ Fixed
0 a -0.3 1.2
1 b 0.1 0.6
2 c 0.38 0.07
a b c
a 1.39 -0.6 (-0.810) 0.051 (0.664)
b -0.6 (-0.810) 0.372 -0.038 (-0.963)
c 0.051 (0.664) -0.038 (-0.963) 0.00426
2024-11-17T07:56:12.402423 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/
# show the likelihood scans for the various parameters in 1D and 2D
my_minuit.draw_mnmatrix()
(<Figure size 640x480 with 9 Axes>,
 array([[<Axes: >, <Axes: >, <Axes: >],
        [<Axes: ylabel='b'>, <Axes: >, <Axes: >],
        [<Axes: xlabel='a', ylabel='c'>, <Axes: xlabel='b'>,
         <Axes: xlabel='c'>]], dtype=object))
../../../_images/caaf3ffd05a7a43676f552ef72b0c32de4c101f1f411b1ffbf969f9b762bdd8a.png