-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmlFit.py
More file actions
145 lines (131 loc) · 5.56 KB
/
mlFit.py
File metadata and controls
145 lines (131 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# Example of maximum-likelihood fit with iminuit version 2.
# pdf is a mixture of Gaussian (signal) and exponential (background),
# truncated in [xMin,xMax].
# G. Cowan / RHUL Physics / December 2022
import numpy as np
import scipy.stats as stats
from scipy.stats import truncexpon
from scipy.stats import truncnorm
from scipy.stats import chi2
import iminuit
from iminuit import Minuit
import matplotlib.pyplot as plt
from matplotlib import container
plt.rcParams["font.size"] = 14
print("iminuit version:", iminuit.__version__) # need 2.x
# define pdf and generate data
np.random.seed(seed=1234567) # fix random seed
theta = 0.2 # fraction of signal
mu = 10. # mean of Gaussian
sigma = 2. # std. dev. of Gaussian
xi = 5. # mean of exponential
xMin = 0.
xMax = 20.
def f(x, par):
theta = par[0]
mu = par[1]
sigma = par[2]
xi = par[3]
fs = stats.truncnorm.pdf(x, a=(xMin-mu)/sigma, b=(xMax-mu)/sigma, loc=mu, scale=sigma)
fb = stats.truncexpon.pdf(x, b=(xMax-xMin)/xi, loc=xMin, scale=xi)
return theta*fs + (1-theta)*fb
numVal = 200
xData = np.empty([numVal])
for i in range (numVal):
r = np.random.uniform();
if r < theta:
xData[i] = stats.truncnorm.rvs(a=(xMin-mu)/sigma, b=(xMax-mu)/sigma, loc=mu, scale=sigma)
else:
xData[i] = stats.truncexpon.rvs(b=(xMax-xMin)/xi, loc=xMin, scale=xi)
# Function to be minimized is negative log-likelihood
def negLogL(par):
pdf = f(xData, par)
return -np.sum(np.log(pdf))
# Initialize Minuit and set up fit:
parin = np.array([theta, mu, sigma, xi]) # initial values (here = true values)
parname = ['theta', 'mu', 'sigma', 'xi']
parname_latex = [r'$\theta$', r'$\mu$', r'$\sigma$', r'$\xi$']
parstep = np.array([0.1, 1., 1., 1.]) # initial setp sizes
parfix = [False, True, True, False] # change these to fix/free parameters
parlim = [(0.,1), (None, None), (0., None), (0., None)] # set limits
m = Minuit(negLogL, parin, name=parname)
m.errors = parstep
m.fixed = parfix
m.limits = parlim
m.errordef = 0.5 # errors from lnL = lnLmax - 0.5
# Do the fit, get errors, extract results
m.migrad() # minimize -logL
MLE = m.values # max-likelihood estimates
sigmaMLE = m.errors # standard deviations
cov = m.covariance # covariance matrix
rho = m.covariance.correlation() # correlation coeffs.
print(r"par index, name, estimate, standard deviation:")
for i in range(m.npar):
if not m.fixed[i]:
print("{:4d}".format(i), "{:<10s}".format(m.parameters[i]), " = ",
"{:.6f}".format(MLE[i]), " +/- ", "{:.6f}".format(sigmaMLE[i]))
print()
print(r"free par indices, covariance, correlation coeff.:")
for i in range(m.npar):
if not(m.fixed[i]):
for j in range(m.npar):
if not(m.fixed[j]):
print(i, j, "{:.6f}".format(cov[i,j]), "{:.6f}".format(rho[i,j]))
# Plot fitted pdf
yMin = 0.
yMax = f(0., MLE)*1.1
fig = plt.figure(figsize=(8,6))
xCurve = np.linspace(xMin, xMax, 100)
yCurve = f(xCurve, MLE)
plt.plot(xCurve, yCurve, color='dodgerblue')
plt.xlabel(r'$x$')
plt.ylabel(r'$f(x)$')
y_fitval = 0.8
delta_y_fitval = 0.08
plt.figtext(0.6, y_fitval, 'Maximum Likelihood')
for i in range(len(parin)):
if not parfix[i]:
y_fitval -= delta_y_fitval
plt.figtext(0.6, y_fitval, parname_latex[i] + ' = ' + f'{MLE[i]:.4f}' + r'$\pm$' + f'{sigmaMLE[i]:.4f}')
# Plot data as tick marks
tick_height = 0.05*(yMax - yMin)
xvals = [xData, xData]
yvals = [np.zeros_like(xData), tick_height * np.ones_like(xData)]
plt.plot(xvals, yvals, color='black', linewidth=1)
plt.xlim(xMin, xMax)
plt.ylim(yMin, yMax)
plt.show()
# Make scan of lnL (for theta, if free)
if not(m.fixed['theta']):
plt.figure()
m.draw_mnprofile('theta')
plt.show()
# Make a contour plot of lnL = lnLmax - 1/2 (here for theta and xi).
# The tangents to this contour give the standard deviations.
CL = stats.chi2.cdf(1.,2) # Q_alpha = 1, npar = 2
print('CL = ', CL)
if not(m.fixed['theta'] | m.fixed['xi']):
fig, ax = plt.subplots(1,1)
con = m.mncontour('theta', 'xi', cl=CL, size=200)
con = np.vstack([con, con[0]]) # close contour
plt.plot(MLE[0], MLE[3], marker='o', linestyle='None', color='black', label=r'$(\hat{\theta}, \hat{\xi})$')
plt.plot(con[:,0], con[:,1], color='black', linewidth=1)
plt.xlabel(r'$\theta$', labelpad=5)
plt.ylabel(r'$\xi$', labelpad=5)
handles, labels = ax.get_legend_handles_labels()
plt.legend(handles, labels, loc='upper right', fontsize=14, frameon=False)
plt.figtext(0.4, 0.93, r'$\ln L = \ln L_{\rm max} - 1/2$')
plt.show()
# Confidence region from lnL = lnLmax - Q/2 (here for theta and xi)
# where Q is the chi2 quantile of CL = 1-alpha = 0.683 and 0.95 for 2 dof.
if not(m.fixed['theta'] | m.fixed['xi']):
fig, ax = plt.subplots(1,1)
m.draw_mncontour('theta', 'xi', cl=[0.683, 0.95], size=200);
plt.plot(MLE[0], MLE[3], marker='o', linestyle='None', color='black', label=r'$(\hat{\theta}, \hat{\xi})$')
plt.xlabel(r'$\theta$', labelpad=10)
plt.ylabel(r'$\xi$', labelpad=10)
plt.subplots_adjust(left=0.2, right=0.9, top=0.9, bottom=0.2)
handles, labels = ax.get_legend_handles_labels()
plt.legend(handles, labels, loc='upper right', fontsize=14, frameon=False)
plt.figtext(0.3, 0.93, r'$\ln L = \ln L_{\rm max} - \frac{1}{2} F^{-1}_{\chi^2}(1-\alpha;N)$')
plt.show()