Skip to content

Commit 51d8a39

Browse files
lukasheinrichmatthewfeickert
authored andcommitted
refactor: Use MLE API (#690)
* Simplify optimizer API to only have 'minimize()' * Add option to return minimized function in 'minimize()' * Add option to return uncertainties in the parameters for 'minimize()' * Move concrete fits that are needed for maximum likelihood estimation / inference to pyhf.infer.mle
1 parent 3a2a1d0 commit 51d8a39

File tree

18 files changed

+442
-260
lines changed

18 files changed

+442
-260
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ jobs:
9999
- name: Check docstrings
100100
run: |
101101
# Group 1 is related to docstrings
102-
pydocstyle --select D1 src/pyhf/pdf.py src/pyhf/probability.py src/pyhf/interpolators
102+
pydocstyle --select D1 src/pyhf/pdf.py src/pyhf/probability.py src/pyhf/interpolators src/pyhf/infer src/pyhf/optimize
103103
- name: Test and build docs
104104
run: |
105105
python -m doctest README.md

docs/api.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ Inference
119119

120120
hypotest
121121
test_statistics.qmu
122-
utils.loglambdav
122+
mle.twice_nll
123+
mle.fit
124+
mle.fixed_poi_fit
123125
utils.generate_asimov_data
124126
utils.pvals_from_teststat
125127
utils.pvals_from_teststat_expected

docs/examples/notebooks/ImpactPlot.ipynb

Lines changed: 11 additions & 21 deletions
Large diffs are not rendered by default.

docs/examples/notebooks/ShapeFactor.ipynb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,7 @@
181181
"source": [
182182
"print('initialization parameters: {}'.format(pdf.config.suggested_init()))\n",
183183
"\n",
184-
"unconpars = pyhf.optimizer.unconstrained_bestfit(pyhf.infer.utils.loglambdav, data, pdf,\n",
185-
" pdf.config.suggested_init(), pdf.config.suggested_bounds())\n",
184+
"unconpars = pyhf.infer.mle.fit(data, pdf)\n",
186185
"print('parameters post unconstrained fit: {}'.format(unconpars))"
187186
]
188187
},
@@ -284,4 +283,4 @@
284283
},
285284
"nbformat": 4,
286285
"nbformat_minor": 2
287-
}
286+
}

docs/examples/notebooks/binderexample/StatisticalAnalysis.ipynb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,8 +1072,7 @@
10721072
"nominal = pdf.config.suggested_init()\n",
10731073
"background_only = pdf.config.suggested_init()\n",
10741074
"background_only[pdf.config.poi_index] = 0.0\n",
1075-
"best_fit = pyhf.optimizer.unconstrained_bestfit(\n",
1076-
" pyhf.infer.utils.loglambdav, data, pdf, pdf.config.suggested_init(), pdf.config.suggested_bounds())"
1075+
"best_fit = pyhf.infer.mle.fit(data, pdf)"
10771076
]
10781077
},
10791078
{
@@ -9051,4 +9050,4 @@
90519050
},
90529051
"nbformat": 4,
90539052
"nbformat_minor": 2
9054-
}
9053+
}

docs/examples/notebooks/multiBinPois.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@
200200
"\n",
201201
"print(init_pars)\n",
202202
"\n",
203-
"bestfit_pars = optimizer.unconstrained_bestfit(pyhf.infer.utils.loglambdav, data, pdf, init_pars, par_bounds)\n",
203+
"bestfit_pars = pyhf.infer.mle.fit(data, pdf, init_pars, par_bounds)\n",
204204
"bestfit_cts = pdf.expected_data(bestfit_pars, include_auxdata = False)"
205205
]
206206
},
@@ -371,4 +371,4 @@
371371
},
372372
"nbformat": 4,
373373
"nbformat_minor": 1
374-
}
374+
}

docs/examples/notebooks/multichannel-coupled-histo.ipynb

Lines changed: 123 additions & 117 deletions
Large diffs are not rendered by default.

docs/examples/notebooks/pullplot.ipynb

Lines changed: 44 additions & 39 deletions
Large diffs are not rendered by default.

src/pyhf/infer/mle.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Module for Maximum Likelihood Estimation."""
2+
from .. import get_backend
3+
4+
5+
def twice_nll(pars, data, pdf):
6+
"""
7+
Twice the negative Log-Likelihood.
8+
9+
Args:
10+
data (`tensor`): the data
11+
pdf (~pyhf.pdf.Model): The statistical model adhering to the schema model.json
12+
13+
Returns:
14+
Twice the negative log likelihood.
15+
16+
"""
17+
return -2 * pdf.logpdf(pars, data)
18+
19+
20+
def fit(data, pdf, init_pars=None, par_bounds=None, **kwargs):
21+
"""
22+
Run a unconstrained maximum likelihood fit.
23+
24+
Args:
25+
data (`tensor`): the data
26+
pdf (~pyhf.pdf.Model): The statistical model adhering to the schema model.json
27+
kwargs: keyword arguments passed through to the optimizer API
28+
29+
Returns:
30+
see optimizer API
31+
32+
"""
33+
_, opt = get_backend()
34+
init_pars = init_pars or pdf.config.suggested_init()
35+
par_bounds = par_bounds or pdf.config.suggested_bounds()
36+
return opt.minimize(twice_nll, data, pdf, init_pars, par_bounds, **kwargs)
37+
38+
39+
def fixed_poi_fit(poi_val, data, pdf, init_pars=None, par_bounds=None, **kwargs):
40+
"""
41+
Run a maximum likelihood fit with the POI value fixzed.
42+
43+
Args:
44+
data: the data
45+
pdf (~pyhf.pdf.Model): The statistical model adhering to the schema model.json
46+
kwargs: keyword arguments passed through to the optimizer API
47+
48+
Returns:
49+
see optimizer API
50+
51+
"""
52+
_, opt = get_backend()
53+
init_pars = init_pars or pdf.config.suggested_init()
54+
par_bounds = par_bounds or pdf.config.suggested_bounds()
55+
return opt.minimize(
56+
twice_nll,
57+
data,
58+
pdf,
59+
init_pars,
60+
par_bounds,
61+
[(pdf.config.poi_index, poi_val)],
62+
**kwargs
63+
)

src/pyhf/infer/test_statistics.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .. import get_backend
2-
from .utils import loglambdav
2+
from .mle import fixed_poi_fit, fit
33

44

55
def qmu(mu, data, pdf, init_pars, par_bounds):
@@ -30,13 +30,13 @@ def qmu(mu, data, pdf, init_pars, par_bounds):
3030
Float: The calculated test statistic, :math:`q_{\mu}`
3131
"""
3232
tensorlib, optimizer = get_backend()
33-
mubhathat = optimizer.constrained_bestfit(
34-
loglambdav, mu, data, pdf, init_pars, par_bounds
33+
mubhathat, fixed_poi_fit_lhood_val = fixed_poi_fit(
34+
mu, data, pdf, init_pars, par_bounds, return_fitted_val=True
3535
)
36-
muhatbhat = optimizer.unconstrained_bestfit(
37-
loglambdav, data, pdf, init_pars, par_bounds
36+
muhatbhat, unconstrained_fit_lhood_val = fit(
37+
data, pdf, init_pars, par_bounds, return_fitted_val=True
3838
)
39-
qmu = loglambdav(mubhathat, data, pdf) - loglambdav(muhatbhat, data, pdf)
39+
qmu = fixed_poi_fit_lhood_val - unconstrained_fit_lhood_val
4040
qmu = tensorlib.where(
4141
muhatbhat[pdf.config.poi_index] > mu, tensorlib.astensor([0]), qmu
4242
)

0 commit comments

Comments
 (0)