Model selection using refnx and dynesty

refnx + dynesty can be used to obtain the Bayesian evidence, which allows you to perform model selection.

%matplotlib inline
import numpy as np
import dynesty

from refnx.analysis import Objective, Model, Parameter, Parameters
from refnx.dataset import Data1D
import matplotlib.pyplot as plt
def gauss(x, p):
    A, loc, sd = p
    y = A * np.exp(-((x - loc) / sd)**2)

    return y

We’ll synthesise some experimental data from two Gaussians with a linear background. We’ll also add on some noise.

x = np.linspace(3, 7, 250)
rng = np.random.default_rng(0)

y = 4 + 10 * x + gauss(x, [200, 5, 0.5]) + gauss(x, [60, 5.8, 0.2])
dy = np.sqrt(y)
y += dy * rng.normal(size=np.size(y))

data = Data1D((x, y, dy))
data.plot();
_images/be4d5434592a83634b11e5a59cfa4f5bdede822c3fea6b123dceb8b2cb0df40e.png
# this is our model that we want to fit.
# It will have a linear background and a number of Gaussian peaks
# The parameters for the background and each of the Gaussian peaks
# will be held in separate entries in `p`.
def n_gauss(x, p):
    y = np.zeros_like(x)
    
    # background parameters
    a, b = np.array(p[0])
    y += a + b*x
    
    for i in range(1, len(p)):
        g_pars = p[i]
        A, loc, sd = np.array(g_pars)
        y += gauss(x, [A, loc, sd])
    return y
# the overall parameter set
pars = Parameters(name="overall_parameters")

# parameters for the background
bkg_pars = Parameters(name='bkg')  
intercept = Parameter(1, name='intercept', bounds=(0, 200), vary=True)
gradient = Parameter(1, name='gradient', bounds=(-20, 250), vary=True)
bkg_pars.extend([intercept, gradient])

pars.append(bkg_pars)

# now go through and add in gaussian peaks and calculate the log-evidence
model = Model(pars, n_gauss)
logz = []
for i in range(4):
    if i:
        A = Parameter(5, name=f"A{i}", bounds=(40, 250), vary=True)
        loc = Parameter(5, name=f"loc{i}", bounds=(3, 7), vary=True)
        sd = Parameter(5, name=f"sd{i}", bounds=(0.1, 2), vary=True)
        g_pars = Parameters(data=[A, loc, sd], name=f"gauss{i}")
        
        pars.append(g_pars)
    
    objective = Objective(model, data)
    nested_sampler = dynesty.NestedSampler(
        objective.logl,
        objective.prior_transform,
        ndim=len(pars.varying_parameters())
    )
    nested_sampler.run_nested()
    logz.append(nested_sampler.results.logz[-1])
5907it [00:05, 1075.69it/s, +500 | bound: 14 | nc: 1 | ncall: 28160 | eff(%): 22.752 | loglstar:   -inf < -4233.325 <    inf | logz: -4244.268 +/-  0.198 | dlogz:  0.001 >  0.509]
12557it [00:18, 696.48it/s, +500 | bound: 48 | nc: 1 | ncall: 53297 | eff(%): 24.499 | loglstar:   -inf < -1048.585 <    inf | logz: -1072.989 +/-  0.313 | dlogz:  0.001 >  0.509]
17403it [11:47, 24.60it/s, +500 | bound: 2186 | nc: 1 | ncall: 1701762 | eff(%):  1.052 | loglstar:   -inf < -913.031 <    inf | logz: -947.230 +/-  0.372 | dlogz:  0.001 >  0.509]
19222it [02:40, 119.67it/s, +500 | bound: 261 | nc: 1 | ncall: 461894 | eff(%):  4.270 | loglstar:   -inf < -911.813 <    inf | logz: -949.640 +/-  0.392 | dlogz:  0.001 >  0.509]
print(logz)
[-4244.267669942061, -1072.989448001302, -947.2295781758878, -949.6400447012933]
plt.plot(logz)
[<matplotlib.lines.Line2D at 0x7fbe3bd87130>]
_images/68323f50033443af4271a8aedb80413d987c8bbe55cebfaaf1bdb131c06fa666.png

The log-evidence points to the use of 2 Gaussians to fit the data. There is a sufficient increase in evidence over 1 Gaussian. However, 3 Gaussians is not justified, the logz term does not increase.