diff --git a/.gitignore b/.gitignore index bf8e780..ff6f4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,6 @@ _build *.tar.gz book/data/ book/1Lbb-likelihoods/ + +# Mac artifacts +.DS_Store diff --git a/binder/requirements.txt b/binder/requirements.txt index bd260bf..9274e23 100644 --- a/binder/requirements.txt +++ b/binder/requirements.txt @@ -4,6 +4,7 @@ ipywidgets~=8.0.7 ipympl~=0.9.3 pandas~=2.0.3 altair~=5.0.1 +rich==13.5.2 # jupyter notebooks notebook>=7.0.0 jupyterlab>=4.0.3 diff --git a/book/UpperLimitsTable.ipynb b/book/UpperLimitsTable.ipynb new file mode 100644 index 0000000..55aa5c9 --- /dev/null +++ b/book/UpperLimitsTable.ipynb @@ -0,0 +1,589 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "960720c4", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import tarfile\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pyhf\n", + "import pyhf.contrib.utils\n", + "import scipy\n", + "from pyhf.contrib.viz import brazil\n", + "\n", + "pyhf.set_backend(\"numpy\", \"minuit\")\n", + "\n", + "from rich import box\n", + "from rich.console import Console\n", + "from rich.table import Table" + ] + }, + { + "cell_type": "markdown", + "id": "e2335cc8", + "metadata": {}, + "source": [ + "# Model-Independent Upper Limits\n", + "\n", + "This section is aiming to cover some common concepts about (model-independent) upper limits. While it won't necessarily be exhaustive, the aim is to summarize the current working knowledge of how High Energy Physics experiments such as ATLAS produce tables such as Table 20 from the Supersymmetry Electroweak 2-lepton, 2-jet analysis ([SUSY-2018-05](https://atlas.web.cern.ch/Atlas/GROUPS/PHYSICS/PAPERS/SUSY-2018-05/)):\n", + "\n", + "
\n", + "
\n", + " \n", + "
Table 20: Model-independent upper limits on the observed visible cross-section in the five electroweak search discovery regions, derived using pseudo-experiments. Left to right: background-only model post-fit total expected background, with the combined statistical and systematic uncertainties; observed data; 95 CL upper limits on the visible cross-section ($\\langle A\\varepsilon\\sigma\\rangle_\\mathrm{obs}^{95}$) and on the number of signal events ($\\mathrm{S}_\\mathrm{obs}^{95}$). The sixth column ($\\mathrm{S}_\\mathrm{exp}^{95}$) shows the expected 95 CL upper limit on the number of signal events, given the expected number (and $\\pm1\\sigma$ excursions of the expectation) of background events. The last two columns indicate the confidence level of the background-only hypothesis ($\\mathrm{CL}_\\mathrm{b}$) and discovery $p$-value with the corresponding Gaussian significance ($Z(s=0)$). $\\mathrm{CL}_\\mathrm{b}$ provides a measure of compatibility of the observed data with the signal strength hypothesis at the 95 CL limit relative to fluctuations of the background, and $p(s=0)$ measures compatibility of the observed data with the background-only hypothesis relative to fluctuations of the background. The $p$-value is capped at 0.5.\n", + "
\n", + "
\n", + "
\n", + "\n", + "Here, the ATLAS collaboration provided some text describing each of the individual columns. How does that translate to statistical fits and `pyhf`? Let's explore below. We will use the public probability models from the analysis published on [HEPData entry](https://www.hepdata.net/record/116034)." + ] + }, + { + "cell_type": "markdown", + "id": "bcfb863f", + "metadata": {}, + "source": [ + "## Downloading the public probability models\n", + "\n", + "First, we'll use `pyhf.contrib` to download the models from HEPData:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c411ed7c", + "metadata": {}, + "outputs": [], + "source": [ + "pyhf.contrib.utils.download(\n", + " \"https://doi.org/10.17182/hepdata.116034.v1/r34\", \"workspace_2L2J\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "10f9e136", + "metadata": {}, + "source": [ + "Then use Python's `tarfile` (part of standard library) to extract out the JSONs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acae3295", + "metadata": {}, + "outputs": [], + "source": [ + "with tarfile.open(\"workspace_2L2J/ewk_discovery_bkgonly.json.tgz\") as fp:\n", + " bkgonly = pyhf.Workspace(json.load(fp.extractfile(\"ewk_discovery_bkgonly.json\")))\n", + "\n", + "with tarfile.open(\"workspace_2L2J/ewk_discovery_patchset.json.tgz\") as fp:\n", + " patchset = pyhf.PatchSet(json.load(fp.extractfile(\"ewk_discovery_patchset.json\")))" + ] + }, + { + "cell_type": "markdown", + "id": "05c9738f", + "metadata": {}, + "source": [ + "and then build our workspace for a particular signal region **DR-Int-EWK**:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfffecd3", + "metadata": {}, + "outputs": [], + "source": [ + "ws = patchset.apply(bkgonly, \"DR_Int_EWK\")\n", + "\n", + "model = ws.model()" + ] + }, + { + "cell_type": "markdown", + "id": "92851f11", + "metadata": {}, + "source": [ + "## Understanding expected and observed events\n", + "\n", + "The background estimate(s) in the signal region(s) are obtained from the background-only fit to control region(s) extrapolated to the signal region(s). \n", + "\n", + "This number, combined with the number of observed events in the signal region, allows us to redo the limit-setting or $p$-value determination without the public probability models.\n", + "\n", + "For the uncertainties on the background estimate(s) in the signal region(s), they're symmetric and calculated through standard error propagation described below." + ] + }, + { + "cell_type": "markdown", + "id": "c5e284e2", + "metadata": {}, + "source": [ + "### Observed Events\n", + "\n", + "The observed events are just `ws.data(model)` which we will call `data`. These are ordered in the same way as the ordering of the channels in the model (`model.config.channels`):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0ee1d9d", + "metadata": {}, + "outputs": [], + "source": [ + "data = ws.data(model)\n", + "print(list(zip(model.config.channels, data)))" + ] + }, + { + "cell_type": "markdown", + "id": "e7068fd4", + "metadata": {}, + "source": [ + "### Expected Events\n", + "\n", + "The expected events are determined from a background-only fit. This is done by fixing the parameter of interest (i.e. for a search the non-SM signal strength) to zero. This fit will give us the fitted parameters $\\hat{\\theta}$. From this, we can print out the expected background events in each channel after this fit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f79763d5", + "metadata": {}, + "outputs": [], + "source": [ + "pars_uncrt_bonly, corr_bonly = pyhf.infer.mle.fixed_poi_fit(\n", + " 0.0, data, model, return_uncertainties=True, return_correlations=True\n", + ")\n", + "pars_bonly, uncrt_bonly = pars_uncrt_bonly.T\n", + "\n", + "table = Table(title=\"Per-region yields from background-only fit\", box=box.HORIZONTALS)\n", + "table.add_column(\"Region\", justify=\"left\", style=\"cyan\", no_wrap=True)\n", + "table.add_column(\"Total Bkg.\", justify=\"center\")\n", + "\n", + "for region, count in zip(model.config.channels, model.expected_actualdata(pars_bonly)):\n", + " table.add_row(region, f\"{count:0.2f}\")\n", + "\n", + "console = Console()\n", + "console.print(table)" + ] + }, + { + "cell_type": "markdown", + "id": "3940313d", + "metadata": {}, + "source": [ + "The uncertainty on the total background (expected event rate) is done by *linearly* propagating errors on parameters using the covariance matrix from a fit result. The error is calculated in the same way that ROOT's [`RooAbsReal::getPropagatedError()`](https://root.cern.ch/doc/master/classRooAbsReal.html#a4d7678837410aabcd93e6f0937de525b) calculates it. It is summarized as follows\n", + "\n", + "$$\n", + "\\mathrm{error}^2(x) = F_\\mathbf{a}(x) \\cdot \\mathrm{Corr}(\\mathbf{a},\\mathbf{a}') \\cdot F_{\\mathbf{a}'}^{\\mathrm{T}}(x)\n", + "$$\n", + "\n", + "where \n", + "\n", + "$$\n", + "F_\\mathbf{a}(x) = \\frac{ f(x, \\mathbf{a} + \\mathrm{d}\\mathbf{a}) - f(x, \\mathbf{a} - \\mathrm{d}\\mathbf{a}) }{2},\n", + "$$\n", + "\n", + "with $f(x)$ the probability model and $\\mathrm{d}\\mathbf{a}$ the vector of one-sigma uncertainties of all fit parameters taken from the fit result and $\\mathrm{Corr}(\\mathbf{a},\\mathbf{a}')$ the correlation matrix from the fit result.\n", + "\n", + "References:\n", + "- [Statistical Error Propagation (J. Phys. Chem. A 2001, 105, 15, 3917–3921)](https://web.stanford.edu/~kimth/www-mit/8.13/error_propagation.pdf)\n", + "- [root-project/root#13404](https://github.com/root-project/root/issues/13404)\n", + "\n", + "**Note**: be aware that some definitions of error propagation use covariance and some use correlation. You can translate between the two, turning covariance into correlation by multiplying by $\\partial a$ on both sides. If one uses covariance, you'll need information about the derivatives (e.g. through auto-differentiation).\n", + "\n", + "This could be done with a for-loop like so:\n", + "\n", + "```python\n", + "up_yields = []\n", + "dn_yields = []\n", + "for i, variation in enumerate(uncrt_bonly):\n", + " varied_up_pars = pars_bonly[:] # copy\n", + " varied_up_pars[i] += variation\n", + " \n", + " varied_dn_pars = pars_bonly[:] # copy\n", + " varied_dn_pars[i] -= variation\n", + "\n", + " up_yields.append(model.expected_actualdata(varied_up_pars))\n", + " dn_yields.append(model.expected_actualdata(varied_dn_pars))\n", + "```\n", + "\n", + "which gives us the up/down variations on the yields for each systematic. However, we'll batch this calculation to calculate all yield variations simultaneously. To do this, we set up our model to allow for batching." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34c480b3", + "metadata": {}, + "outputs": [], + "source": [ + "npars = len(model.config.parameters)\n", + "model_batch = ws.model(batch_size=npars)\n", + "pars_batch = np.tile(pars_bonly, (npars, 1))" + ] + }, + { + "cell_type": "markdown", + "id": "ebf6a42f", + "metadata": {}, + "source": [ + "Then we calculate the `up_yields` and `dn_yields` which are used to calculate the $F_\\mathbf{a}(x)$ term above, denoted as `variations`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a9f6848", + "metadata": {}, + "outputs": [], + "source": [ + "up_yields = model_batch.expected_actualdata(pars_batch + np.diag(uncrt_bonly))\n", + "dn_yields = model_batch.expected_actualdata(pars_batch - np.diag(uncrt_bonly))\n", + "variations = (up_yields - dn_yields) / 2" + ] + }, + { + "cell_type": "markdown", + "id": "4fd1b3b9", + "metadata": {}, + "source": [ + "Now we can calculate $\\mathrm{error}^2(x)$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bcc4aa16", + "metadata": {}, + "outputs": [], + "source": [ + "error_sq = np.einsum(\"il,ik,kl->l\", variations, corr_bonly, variations)\n", + "error_sq" + ] + }, + { + "cell_type": "markdown", + "id": "409d3365", + "metadata": {}, + "source": [ + "Taking the square root will give us the propagated uncertainty on the yields per-region for the background-only fit. We can update our table:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c3023bf", + "metadata": {}, + "outputs": [], + "source": [ + "table = Table(title=\"Per-region yields from background-only fit\", box=box.HORIZONTALS)\n", + "table.add_column(\"Region\", justify=\"left\", style=\"cyan\", no_wrap=True)\n", + "table.add_column(\"Total Bkg.\", justify=\"center\")\n", + "\n", + "for region, count, uncrt in zip(\n", + " model.config.channels, model.expected_actualdata(pars_bonly), np.sqrt(error_sq)\n", + "):\n", + " table.add_row(region, f\"{count:0.2f} ± {uncrt:0.2f}\")\n", + "\n", + "console = Console()\n", + "console.print(table)" + ] + }, + { + "cell_type": "markdown", + "id": "8123d0f3", + "metadata": {}, + "source": [ + "## Understanding visible cross-section\n", + "\n", + "_See [ATL-PHYS-PUB-2022-017](https://atlas.web.cern.ch/Atlas/GROUPS/PHYSICS/PUBNOTES/ATL-PHYS-PUB-2022-017/) for descriptions of detector efficiency and selection acceptance._\n", + "\n", + "**Production cross section upper limits** is another way of saying model-dependent limits. Without the detector efficiency (but keeping selection acceptance), the term **visible cross section upper limits** is used. \n", + "\n", + "For upper limits on numbers of events (and the corresponding cross section), after detector efficiency and selection acceptance effects, the term **model independent upper limits** is used. These upper limits are typically free from assumptions about the acceptance and efficiency of any model (and are model independent in that sense), but they do make the assumption that signal contamination in control regions is negligible.\n", + "\n", + "### Model-independent Upper Limits\n", + "\n", + "Model-independent upper limits (UL) at the 95% confidence level (CL) on the number of beyond the SM (BSM) events for each signal region are derived using the $\\mathrm{CL}_\\mathrm{s}$ prescription (DOI: [10.1088/0954-3899/28/10/313](https://doi.org/10.1088/0954-3899/28/10/313)) and neglecting any possible contamination in the control regions.\n", + "\n", + "To obtain the 95% CL model-independent UL / discovery $p_0$-value, the fit in the signal region proceeds in the same way as the background-only fit, except that an additional parameter for the non-SM signal strength, constrained to be non-negative, is fit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57fda65c", + "metadata": {}, + "outputs": [], + "source": [ + "pyhf.set_backend(\"numpy\", \"scipy\")\n", + "\n", + "mu_tests = np.linspace(15, 25, 10)\n", + "(\n", + " obs_limit,\n", + " exp_limit_and_bands,\n", + " (poi_tests, tests),\n", + ") = pyhf.infer.intervals.upper_limits.upper_limit(\n", + " data, model, scan=mu_tests, level=0.05, return_results=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4190909", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Observed UL: {obs_limit:0.2f}\")\n", + "print(\n", + " f\"Expected UL: {exp_limit_and_bands[2]:0.2f} [{exp_limit_and_bands[1]-exp_limit_and_bands[2]:0.2f}, +{exp_limit_and_bands[3]-exp_limit_and_bands[2]:0.2f}]\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "27c5e954", + "metadata": {}, + "source": [ + "When calculating $S^{95}_\\mathrm{obs}$ (also for expected) it is typical to inject a _test_ signal (all expected event rates equal to unity) which means for this signal, we're assuming $A\\times\\varepsilon\\times\\sigma\\times\\mathcal{L}=1$. As $S^{95}_\\mathrm{obs}$ is an upper limit on $\\mu$ for this _test_ signal, normalizing this by the integrated luminosity of the data sample allows us to interpret as upper limits on the **visible (BSM) cross-section**, $\\langle A\\varepsilon\\sigma\\rangle_\\mathrm{obs}^{95}$. This is defined as the product of acceptance, reconstruction efficiency and production cross-section.\n", + "\n", + "Remember that we often call this the **visible cross-section**, where cross-section is $\\sigma$ and the visible cross-section is $A\\times\\varepsilon\\times\\sigma$. This is called a _model independent_ limit as we do not have a specific model in mind (hence the _test_ signal). We don't have specific values for selection acceptance $A$, detector efficiency $\\varepsilon$, or signal model cross-section $\\sigma$, so we can only say something about the product. If we have a specific signal model with $A\\varepsilon\\sigma$ that gives us a cross-section higher than the limit on the visible cross-section, we should be able to see that signal in the analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f5b5e3a", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Limit on visible cross-section: {obs_limit / 140:0.2f} ifb\") # 140 ifb" + ] + }, + { + "cell_type": "markdown", + "id": "13e055f6", + "metadata": {}, + "source": [ + "## Discovery $p$-values\n", + "\n", + "The significance of an excess can be quantified by the probability ($p_0$) that a background-only experiment is more signal-like than observed. The last two columns indicate the $\\mathrm{CL}_\\mathrm{b}$ value and the discovery $p$-value ($p(s)=0$) with the corresponding gaussian significance ($Z$$\\,$).\n", + "\n", + "These two numbers provide information about the compatibility of the observed data with a background-only hypothesis, albeit with different test statistics $q$. The $q_\\mu$ (or $\\tilde{q}_\\mu$) test statistic is optimized for hypothesis-testing the signal+background hypothesis. The $q_0$ test statistic is optimized for hypothesis-testing the background-only hypothesis.\n", + "\n", + "From Equation 52 in [arXiv:1503.07622](https://arxiv.org/abs/1503.07622):\n", + "- for $p_\\mu$: the null hypothesis is signal at a rate $\\mu' < \\mu$ and the alternative hypothesis is signal at a rate of $\\mu$\n", + "- for $p_0$: the null hypothesis is background-only (b) and alternative hypothesis is signal+background (s+b)\n", + "\n", + "For these hypotheses, we find that the $p$-values are\n", + "\n", + "$$\n", + "1 - p_\\mathrm{b} = P(q \\geq q_\\mathrm{obs} | \\mathrm{b}) = \\int_{q_\\mathrm{obs}}^\\infty f(q | \\mathrm{b} )\\ \\mathrm{d}q\n", + "$$\n", + "\n", + "and\n", + "\n", + "$$\n", + "p_\\mathrm{s+b} = P(q \\geq q_\\mathrm{obs} | \\mathrm{s+b}) = \\int_{q_\\mathrm{obs}}^\\infty f(q | \\mathrm{s+b} )\\ \\mathrm{d}q\n", + "$$\n", + "\n", + "Note that the same $q_\\mathrm{obs}$ value is used to determine $p_\\mu$ and $p_b$." + ] + }, + { + "cell_type": "markdown", + "id": "b092d468", + "metadata": {}, + "source": [ + "### $p_\\mu$ (and $\\mathrm{CL}_\\mathrm{b}$)\n", + "\n", + "$\\mathrm{CL}_\\mathrm{b}$ provides a measure of compatibility of the observed data with the 95% CL signal strength hypothesis relative to fluctuations of the background. $\\mathrm{CL}_\\mathrm{b}$ is evaluating $p_\\mu$ in the case where $\\mu'=0$ and $\\mu=\\mu_{95}$ (the fitted $\\mu$ at the upper limit $S^{95}_\\mathrm{obs}$). This is saying that the tested $\\mu$ is the 95% CL signal strength ($\\mu_{95}$) but the assumed true $\\mu$ is the background-only hypothesis ($\\mu'=0$):\n", + "\n", + "$$\n", + "\\mathrm{CL}_\\mathrm{b} \\equiv 1 - p_b = \\int_{q_{\\mu_{95}}^{\\ \\mathrm{obs}}}^\\infty f(q_\\mu | \\mu' = 0)\\ \\mathrm{d} q_\\mu\n", + "$$\n", + "\n", + "Note that this calculation depends on the value of $\\mu$ at the fitted upper limit $S^{95}_\\mathrm{obs}$. You get a small $\\mathrm{CL}_\\mathrm{b}$ if you have a downward fluctuation in observed data relative to your signal+background hypothesis, preventing you from excluding your signal erroneously if you \"shouldn't be able to\" when using $\\mathrm{CL}_\\mathrm{s}$ (since you would be dividing by a small number). See the paragraph above plot on page two in [this document](https://twiki.cern.ch/twiki/pub/Sandbox/NotesOnStatistics/CLsInfo.pdf).\n", + "\n", + "### $p_0$\n", + "\n", + "$p(s)=0$ measures compatibility of the observed data with the background-only (zero signal strength) hypothesis relative to fluctuations of the background. Larger values indicate greater relative compatibility. $p(s)=0$ is not calculated in signal regions with a deficit with respect to the nominal background prediction.\n", + "\n", + "This is saying that the tested $\\mu$ is the background-only hypothesis ($\\mu=\\mu'=0$) and the assumed true $\\mu$ is the background-only hypothesis:\n", + "\n", + "$$\n", + "p_0 = P(q_0 \\geq q_0^\\mathrm{obs}) = \\int_{q_{\\mu_{95}}^{\\ \\mathrm{obs}}}^\\infty f(q_{\\mu=0} | \\mu'=0)\\ \\mathrm{d} q_{\\mu=0}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "afa48732", + "metadata": {}, + "source": [ + "### Relationship to $\\mathrm{CL}_\\mathrm{s}$\n", + "\n", + "This will be covered through a learn notebook, which will be added as part of [scikit-hep/pyhf#2274](https://github.com/scikit-hep/pyhf/issues/2274)." + ] + }, + { + "cell_type": "markdown", + "id": "005b6d1c", + "metadata": {}, + "source": [ + "### Calculating the $p$-values\n", + "\n", + "Armed with all of this knowledge, we can first calculate the discovery $p$-value evaluated at $\\mu=\\mu'=0$, as well as convert this to a Z-score assuming a \"standard\" Normal distribution:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11011192", + "metadata": {}, + "outputs": [], + "source": [ + "p0 = pyhf.infer.hypotest(0.0, data, model, test_stat=\"q0\")\n", + "p0_Z = scipy.stats.norm.isf(p0)\n", + "print(f\"p(s)=0 (Z): {p0:0.2f} ({p0_Z:0.2f})\")" + ] + }, + { + "cell_type": "markdown", + "id": "1f10a3ca", + "metadata": {}, + "source": [ + "And similarly, we can extract $\\mathrm{CL}_\\mathrm{b}$ by evaluating at $\\mu=\\mu_{95}^\\mathrm{obs}$ :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74072887", + "metadata": {}, + "outputs": [], + "source": [ + "_, (_, CLb) = pyhf.infer.hypotest(obs_limit, data, model, return_tail_probs=True)\n", + "print(f\"CLb: {CLb:0.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2586d091", + "metadata": {}, + "source": [ + "## Making the table\n", + "\n", + "Now that we have all of our numbers, we can make the table for the discovery region `DRInt_cuts`. We'll get the corresponding index of the channel so we can extract out the total background and uncertainty estimates from above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84be7d7b", + "metadata": {}, + "outputs": [], + "source": [ + "region_mask = model.config.channels.index(\"DRInt_cuts\")\n", + "bkg_count = model.expected_actualdata(pars_bonly)[region_mask]\n", + "bkg_uncrt = np.sqrt(error_sq)[region_mask]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d7ee562", + "metadata": {}, + "outputs": [], + "source": [ + "table = Table(\n", + " title=\"Model-independent upper limits on the observed visible cross-section in the five electroweak search discovery regions; derived using asymptotics, pyhf, and published likelihoods.\",\n", + " box=box.HORIZONTALS,\n", + ")\n", + "table.add_column(\"Signal Region\", justify=\"left\", style=\"cyan\", no_wrap=True)\n", + "table.add_column(\"Total Bkg.\", justify=\"center\")\n", + "table.add_column(\"Data\")\n", + "table.add_column(r\"XSec \\[fb]\")\n", + "table.add_column(\"S 95 Obs\")\n", + "table.add_column(\"S 95 Exp\")\n", + "table.add_column(\"CLb(S 95 Obs)\")\n", + "table.add_column(\"p(s=0) (Z)\")\n", + "\n", + "table.add_row(\n", + " model.config.channels[region_mask],\n", + " f\"{bkg_count:0.0f} ± {bkg_uncrt:0.0f}\",\n", + " f\"{data[region_mask]:0.0f}\",\n", + " f\"{obs_limit / 140:0.2f}\",\n", + " f\"{obs_limit:0.1f}\",\n", + " f\"{exp_limit_and_bands[2]:0.1f} + {exp_limit_and_bands[3] - exp_limit_and_bands[2]:0.1f} - {exp_limit_and_bands[2] - exp_limit_and_bands[1]:0.1f}\",\n", + " f\"{CLb:0.2f}\",\n", + " f\"{p0:0.2f} ({p0_Z:0.1f})\",\n", + ")\n", + "\n", + "console = Console()\n", + "console.print(table)" + ] + }, + { + "cell_type": "markdown", + "id": "334983cb", + "metadata": {}, + "source": [ + "
\n", + "
\n", + " \n", + "
Table 20: Model-independent upper limits on the observed visible cross-section in the five electroweak search discovery regions, derived using pseudo-experiments. Left to right: background-only model post-fit total expected background, with the combined statistical and systematic uncertainties; observed data; 95 CL upper limits on the visible cross-section ($\\langle A\\varepsilon\\sigma\\rangle_\\mathrm{obs}^{95}$) and on the number of signal events ($\\mathrm{S}_\\mathrm{obs}^{95}$). The sixth column ($\\mathrm{S}_\\mathrm{exp}^{95}$) shows the expected 95 CL upper limit on the number of signal events, given the expected number (and $\\pm1\\sigma$ excursions of the expectation) of background events. The last two columns indicate the confidence level of the background-only hypothesis ($\\mathrm{CL}_\\mathrm{b}$) and discovery $p$-value with the corresponding Gaussian significance ($Z(s=0)$). $\\mathrm{CL}_\\mathrm{b}$ provides a measure of compatibility of the observed data with the signal strength hypothesis at the 95 CL limit relative to fluctuations of the background, and $p(s=0)$ measures compatibility of the observed data with the background-only hypothesis relative to fluctuations of the background. The $p$-value is capped at 0.5.\n", + "
\n", + "
\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "8ca56370", + "metadata": {}, + "source": [ + "## Acknowledgements\n", + "\n", + "We acknowledge the help of ATLAS Statistics Forum including:\n", + " - Will Buttinger\n", + " - Jonathan Long\n", + " - Zach Marshall\n", + " - Alex Read\n", + " - Sarah Williams" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/book/_toc.yml b/book/_toc.yml index 64ece44..0fbc7cf 100644 --- a/book/_toc.yml +++ b/book/_toc.yml @@ -13,6 +13,7 @@ parts: - file: PullPlot - file: Toys - file: Combinations + - file: UpperLimitsTable - caption: More Information chapters: diff --git a/book/requirements.lock b/book/requirements.lock index f9a4d28..6b39496 100644 --- a/book/requirements.lock +++ b/book/requirements.lock @@ -536,9 +536,9 @@ ipympl==0.9.3 \ --hash=sha256:49bab75c05673a6881d1aaec5d8ac81d4624f73d292d154c5fb7096f10236a2b \ --hash=sha256:d113cd55891bafe9b27ef99b6dd111a87beb6bb2ae550c404292272103be8013 # via -r /home/feickert/Code/GitHub/pyhf-org/pyhf-tutorial/requirements.txt -ipython==8.14.0 \ - --hash=sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1 \ - --hash=sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf +ipython==8.12.2 \ + --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \ + --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc # via # ipykernel # ipympl @@ -753,6 +753,7 @@ markdown-it-py==2.2.0 \ # via # mdit-py-plugins # myst-parser + # rich markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ @@ -912,32 +913,35 @@ notebook-shim==0.2.3 \ # via # jupyterlab # notebook -numpy==1.25.1 \ - --hash=sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe \ - --hash=sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004 \ - --hash=sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19 \ - --hash=sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f \ - --hash=sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631 \ - --hash=sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd \ - --hash=sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c \ - --hash=sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009 \ - --hash=sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d \ - --hash=sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105 \ - --hash=sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09 \ - --hash=sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf \ - --hash=sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e \ - --hash=sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588 \ - --hash=sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa \ - --hash=sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb \ - --hash=sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf \ - --hash=sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57 \ - --hash=sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171 \ - --hash=sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625 \ - --hash=sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6 \ - --hash=sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b \ - --hash=sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800 \ - --hash=sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7 \ - --hash=sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503 +numpy==1.24.4 \ + --hash=sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f \ + --hash=sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61 \ + --hash=sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7 \ + --hash=sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400 \ + --hash=sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef \ + --hash=sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2 \ + --hash=sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d \ + --hash=sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc \ + --hash=sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835 \ + --hash=sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706 \ + --hash=sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5 \ + --hash=sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4 \ + --hash=sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6 \ + --hash=sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463 \ + --hash=sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a \ + --hash=sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f \ + --hash=sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e \ + --hash=sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e \ + --hash=sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694 \ + --hash=sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8 \ + --hash=sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64 \ + --hash=sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d \ + --hash=sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc \ + --hash=sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254 \ + --hash=sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2 \ + --hash=sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1 \ + --hash=sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810 \ + --hash=sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9 # via # altair # awkward @@ -1137,6 +1141,7 @@ pygments==2.15.1 \ # ipython # nbconvert # pydata-sphinx-theme + # rich # sphinx pyhf[contrib,minuit,xmlio]==0.7.3 \ --hash=sha256:11647c79a515be65c061780fb9e06a885195fba2774c5d4ccce6da1a30781064 \ @@ -1319,6 +1324,10 @@ rfc3986-validator==0.1.1 \ # via # jsonschema # jupyter-events +rich==13.5.2 \ + --hash=sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808 \ + --hash=sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39 + # via -r /home/feickert/Code/GitHub/pyhf-org/pyhf-tutorial/requirements.txt rpds-py==0.9.2 \ --hash=sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f \ --hash=sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238 \ @@ -1420,26 +1429,28 @@ rpds-py==0.9.2 \ # via # jsonschema # referencing -scipy==1.11.1 \ - --hash=sha256:08d957ca82d3535b3b9ba6c8ff355d78fe975271874e2af267cb5add5bd78625 \ - --hash=sha256:249cfa465c379c9bb2c20123001e151ff5e29b351cbb7f9c91587260602c58d0 \ - --hash=sha256:366a6a937110d80dca4f63b3f5b00cc89d36f678b2d124a01067b154e692bab1 \ - --hash=sha256:39154437654260a52871dfde852adf1b93b1d1bc5dc0ffa70068f16ec0be2624 \ - --hash=sha256:396fae3f8c12ad14c5f3eb40499fd06a6fef8393a6baa352a652ecd51e74e029 \ - --hash=sha256:3b9963798df1d8a52db41a6fc0e6fa65b1c60e85d73da27ae8bb754de4792481 \ - --hash=sha256:3e8eb42db36526b130dfbc417609498a6192381abc1975b91e3eb238e0b41c1a \ - --hash=sha256:512fdc18c65f76dadaca139348e525646d440220d8d05f6d21965b8d4466bccd \ - --hash=sha256:aec8c62fbe52914f9cf28d846cf0401dd80ab80788bbab909434eb336ed07c04 \ - --hash=sha256:b41a0f322b4eb51b078cb3441e950ad661ede490c3aca66edef66f4b37ab1877 \ - --hash=sha256:b4bb943010203465ac81efa392e4645265077b4d9e99b66cf3ed33ae12254173 \ - --hash=sha256:b588311875c58d1acd4ef17c983b9f1ab5391755a47c3d70b6bd503a45bfaf71 \ - --hash=sha256:ba94eeef3c9caa4cea7b402a35bb02a5714ee1ee77eb98aca1eed4543beb0f4c \ - --hash=sha256:be8c962a821957fdde8c4044efdab7a140c13294997a407eaee777acf63cbf0c \ - --hash=sha256:cce154372f0ebe88556ed06d7b196e9c2e0c13080ecb58d0f35062dc7cc28b47 \ - --hash=sha256:d51565560565a0307ed06fa0ec4c6f21ff094947d4844d6068ed04400c72d0c3 \ - --hash=sha256:e866514bc2d660608447b6ba95c8900d591f2865c07cca0aa4f7ff3c4ca70f30 \ - --hash=sha256:fb5b492fa035334fd249f0973cc79ecad8b09c604b42a127a677b45a9a3d4289 \ - --hash=sha256:ffb28e3fa31b9c376d0fb1f74c1f13911c8c154a760312fbee87a21eb21efe31 +scipy==1.10.1 \ + --hash=sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415 \ + --hash=sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f \ + --hash=sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd \ + --hash=sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f \ + --hash=sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d \ + --hash=sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601 \ + --hash=sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5 \ + --hash=sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88 \ + --hash=sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f \ + --hash=sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e \ + --hash=sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2 \ + --hash=sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353 \ + --hash=sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35 \ + --hash=sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6 \ + --hash=sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea \ + --hash=sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35 \ + --hash=sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1 \ + --hash=sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9 \ + --hash=sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5 \ + --hash=sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019 \ + --hash=sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1 # via pyhf send2trash==1.8.2 \ --hash=sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679 \