Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
TwoSidedFrequentistUpperLimitWithBands.py
Go to the documentation of this file.
1# \file
2# \ingroup tutorial_roostats
3# \notebook -js
4# TwoSidedFrequentistUpperLimitWithBands
5#
6#
7# This is a standard demo that can be used with any ROOT file
8# prepared in the standard way. You specify:
9# - name for input ROOT file
10# - name of workspace inside ROOT file that holds model and data
11# - name of ModelConfig that specifies details for calculator tools
12# - name of dataset
13#
14# With default parameters the macro will attempt to run the
15# standard hist2workspace example and read the ROOT file
16# that it produces.
17#
18# You may want to control:
19# ~~~{.cpp}
20# double confidenceLevel=0.95;
21# double additionalToysFac = 1.;
22# int nPointsToScan = 12;
23# int nToyMC = 200;
24# ~~~
25#
26# This uses a modified version of the profile likelihood ratio as
27# a test statistic for upper limits (eg. test stat = 0 if muhat>mu).
28#
29# Based on the observed data, one defines a set of parameter points
30# to be tested based on the value of the parameter of interest
31# and the conditional MLE (eg. profiled) values of the nuisance parameters.
32#
33# At each parameter point, pseudo-experiments are generated using this
34# fixed reference model and then the test statistic is evaluated.
35# The auxiliary measurements (global observables) associated with the
36# constraint terms in nuisance parameters are also fluctuated in the
37# process of generating the pseudo-experiments in a frequentist manner
38# forming an 'unconditional ensemble'. One could form a 'conditional'
39# ensemble in which these auxiliary measurements are fixed. Note that the
40# nuisance parameters are not randomized, which is a Bayesian procedure.
41# Note, the nuisance parameters are floating in the fits. For each point,
42# the threshold that defines the 95% acceptance region is found. This
43# forms a "Confidence Belt".
44#
45# After constructing the confidence belt, one can find the confidence
46# interval for any particular dataset by finding the intersection
47# of the observed test statistic and the confidence belt. First
48# this is done on the observed data to get an observed 1-sided upper limt.
49#
50# Finally, there expected limit and bands (from background-only) are
51# formed by generating background-only data and finding the upper limit.
52# The background-only is defined as such that the nuisance parameters are
53# fixed to their best fit value based on the data with the signal rate fixed to 0.
54# The bands are done by hand for now, will later be part of the RooStats tools.
55#
56# On a technical note, this technique IS the generalization of Feldman-Cousins
57# with nuisance parameters.
58#
59# Building the confidence belt can be computationally expensive.
60# Once it is built, one could save it to a file and use it in a separate step.
61#
62# We can use PROOF to speed things along in parallel, however,
63# the test statistic has to be installed on the workers
64# so either turn off PROOF or include the modified test statistic
65# in your $ROOTSYS/roofit/roostats/inc directory,
66# add the additional line to the LinkDef.h file,
67# and recompile root.
68#
69# Note, if you have a boundary on the parameter of interest (eg. cross-section)
70# the threshold on the two-sided test statistic starts off at moderate values and plateaus.
71#
72# [#0] PROGRESS:Generation -- generated toys: 500 / 999
73# NeymanConstruction: Prog: 12/50 total MC = 39 this test stat = 0
74# SigXsecOverSM=0.69 alpha_syst1=0.136515 alpha_syst3=0.425415 beta_syst2=1.08496 [-1e+30, 0.011215] in interval = 1
75#
76# this tells you the values of the parameters being used to generate the pseudo-experiments
77# and the threshold in this case is 0.011215. One would expect for 95% that the threshold
78# would be ~1.35 once the cross-section is far enough away from 0 that it is essentially
79# unaffected by the boundary. As one reaches the last points in the scan, the
80# threshold starts to get artificially high. This is because the range of the parameter in
81# the fit is the same as the range in the scan. In the future, these should be independently
82# controlled, but they are not now. As a result the ~50% of pseudo-experiments that have an
83# upward fluctuation end up with muhat = muMax. Because of this, the upper range of the
84# parameter should be well above the expected upper limit... but not too high or one will
85# need a very large value of nPointsToScan to resolve the relevant region. This can be
86# improved, but this is the first version of this script.
87#
88# Important note: when the model includes external constraint terms, like a Gaussian
89# constraint to a nuisance parameter centered around some nominal value there is
90# a subtlety. The asymptotic results are all based on the assumption that all the
91# measurements fluctuate... including the nominal values from auxiliary measurements.
92# If these do not fluctuate, this corresponds to an "conditional ensemble". The
93# result is that the distribution of the test statistic can become very non-chi^2.
94# This results in thresholds that become very large.
95#
96# \macro_image
97# \macro_output
98# \macro_code
99#
100# \authors Kyle Cranmer, contributions from Aaron Armbruster, Haoshuang Ji, Haichen Wang, Daniel Whiteson, and Jolly Chen (Python translation)
101
102import ROOT
103
104# -------------------------------------------------------
105
106
107# User configuration parameters
108infile = ""
109workspaceName = "combined"
110modelConfigName = "ModelConfig"
111dataName = "obsData"
112
113
114confidenceLevel = 0.95
115# degrade/improve number of pseudo-experiments used to define the confidence belt.
116# value of 1 corresponds to default number of toys in the tail, which is 50/(1-confidenceLevel)
117additionalToysFac = 0.5
118nPointsToScan = 20 # number of steps in the parameter of interest
119nToyMC = 200 # number of toys used to define the expected limit and band
120
121# -------------------------------------------------------
122# First part is just to access a user-defined file
123# or create the standard example file if it doesn't exist
124filename = ""
125if not infile:
126 filename = "results/example_combined_GaussExample_model.root"
127 fileExist = not ROOT.gSystem.AccessPathName(filename) # note opposite return code
128 # if file does not exists generate with histfactory
129 if not fileExist:
130 # Normally this would be run on the command line
131 print(f"will run standard hist2workspace example")
132 ROOT.gROOT.ProcessLine(".! prepareHistFactory .")
133 ROOT.gROOT.ProcessLine(".! hist2workspace config/example.xml")
134 print("\n\n---------------------")
135 print("Done creating example input")
136 print("---------------------\n\n")
137else:
138 filename = infile
139
140# Try to open the file
141inputFile = ROOT.TFile.Open(filename)
142
143# -------------------------------------------------------
144# Now get the data and workspace
145
146# get the workspace out of the file
147w = inputFile.Get(workspaceName)
148
149# get the modelConfig out of the file
150mc = w.obj(modelConfigName)
151
152# get the modelConfig out of the file
153data = w.data(dataName)
154
155print(f"Found data and ModelConfig:")
156mc.Print()
157
158# -------------------------------------------------------
159# Now get the POI for convenience
160# you may want to adjust the range of your POI
161firstPOI = mc.GetParametersOfInterest().first()
162# firstPOI.setMin(0)
163# firstPOI.setMax(10)
164
165# -------------------------------------------------------
166# create and use the FeldmanCousins tool
167# to find and plot the 95% confidence interval
168# on the parameter of interest as specified
169# in the model config
170# REMEMBER, we will change the test statistic
171# so this is NOT a Feldman-Cousins interval
172fc = ROOT.RooStats.FeldmanCousins(data, mc)
173fc.SetConfidenceLevel(confidenceLevel)
174fc.AdditionalNToysFactor(additionalToysFac) # improve sampling that defines confidence belt
175# fc.UseAdaptiveSampling(True) # speed it up a bit, but don't use for expected limits
176fc.SetNBins(nPointsToScan) # set how many points per parameter of interest to scan
177fc.CreateConfBelt(True) # save the information in the belt for plotting
178
179# -------------------------------------------------------
180# Feldman-Cousins is a unified limit by definition
181# but the tool takes care of a few things for us like which values
182# of the nuisance parameters should be used to generate toys.
183# so let's just change the test statistic and realize this is
184# no longer "Feldman-Cousins" but is a fully frequentist Neyman-Construction.
185# fc.GetTestStatSampler().SetTestStatistic(onesided)
186# fc.GetTestStatSampler().SetGenerateBinned(True)
187toymcsampler = fc.GetTestStatSampler()
188testStat = toymcsampler.GetTestStatistic()
189
190# Since this tool needs to throw toy MC the PDF needs to be
191# extended or the tool needs to know how many entries in a dataset
192# per pseudo experiment.
193# In the 'number counting form' where the entries in the dataset
194# are counts, and not values of discriminating variables, the
195# datasets typically only have one entry and the PDF is not
196# extended.
197if not mc.GetPdf().canBeExtended():
198 if data.numEntries() == 1:
199 fc.FluctuateNumDataEntries(False)
200 else:
201 print(f"Not sure what to do about this model")
202
203if mc.GetGlobalObservables():
204 print(f"will use global observables for unconditional ensemble")
205 mc.GetGlobalObservables().Print()
206 toymcsampler.SetGlobalObservables(mc.GetGlobalObservables())
207
208# Now get the interval
209interval = fc.GetInterval()
210belt = fc.GetConfidenceBelt()
211
212# print out the interval on the first Parameter of Interest
213print(
214 f"\n95% interval on {firstPOI.GetName()} is : [{interval.LowerLimit(firstPOI)}, {interval.UpperLimit(firstPOI)}] "
215)
216
217# get observed UL and value of test statistic evaluated there
218tmpPOI = ROOT.RooArgSet(firstPOI)
219observedUL = interval.UpperLimit(firstPOI)
220firstPOI.setVal(observedUL)
221obsTSatObsUL = fc.GetTestStatSampler().EvaluateTestStatistic(data, tmpPOI)
222
223# Ask the calculator which points were scanned
224parameterScan = fc.GetPointsToScan()
225
226# make a histogram of parameter vs. threshold
227histOfThresholds = ROOT.TH1F("histOfThresholds", "", parameterScan.numEntries(), firstPOI.getMin(), firstPOI.getMax())
228histOfThresholds.SetDirectory(ROOT.nullptr) # so th histogram doesn't get attached to the file with the workspace
229histOfThresholds.GetXaxis().SetTitle(firstPOI.GetName())
230histOfThresholds.GetYaxis().SetTitle("Threshold")
231
232# loop through the points that were tested and ask confidence belt
233# what the upper/lower thresholds were.
234# For FeldmanCousins, the lower cut off is always 0
235for i in range(parameterScan.numEntries()):
236 tmpPoint = parameterScan.get(i).clone("temp")
237 # print(get threshold")
238 arMax = belt.GetAcceptanceRegionMax(tmpPoint)
239 poiVal = tmpPoint.getRealValue(firstPOI.GetName())
240 histOfThresholds.Fill(poiVal, arMax)
241
242c1 = ROOT.TCanvas()
243c1.Divide(2)
244c1.cd(1)
245histOfThresholds.SetMinimum(0)
246histOfThresholds.Draw()
247c1.cd(2)
248
249# -------------------------------------------------------
250# Now we generate the expected bands and power-constraint
251
252# First: find parameter point for mu=0, with conditional MLEs for nuisance parameters
253nll = mc.GetPdf().createNLL(data)
254profile = nll.createProfile(mc.GetParametersOfInterest())
255firstPOI.setVal(0.0)
256profile.getVal() # this will do fit and set nuisance parameters to profiled values
257poiAndNuisance = ROOT.RooArgSet()
258if mc.GetNuisanceParameters():
259 poiAndNuisance.add(mc.GetNuisanceParameters())
260poiAndNuisance.add(mc.GetParametersOfInterest())
261w.saveSnapshot("paramsToGenerateData", poiAndNuisance)
262paramsToGenerateData = poiAndNuisance.snapshot()
263print("\nWill use these parameter points to generate pseudo data for bkg only")
264paramsToGenerateData.Print("v")
265
266unconditionalObs = ROOT.RooArgSet()
267unconditionalObs.add(mc.GetObservables())
268unconditionalObs.add(mc.GetGlobalObservables()) # comment this out for the original conditional ensemble
269
270CLb = 0
271CLbinclusive = 0
272
273# Now we generate background only and find distribution of upper limits
274histOfUL = ROOT.TH1F("histOfUL", "", 100, 0, firstPOI.getMax())
275histOfUL.SetDirectory(ROOT.nullptr) # make sure the histogram doesn't get attached to the file with the workspace
276histOfUL.GetXaxis().SetTitle("Upper Limit (background only)")
277histOfUL.GetYaxis().SetTitle("Entries")
278for imc in range(nToyMC):
279
280 # set parameters back to values for generating pseudo data
281 # print("\n get current nuis, set vals, print again")
282 w.loadSnapshot("paramsToGenerateData")
283 # poiAndNuisance.Print("v")
284
285 # now generate a toy dataset for the main measurement
286 if not mc.GetPdf().canBeExtended():
287 if data.numEntries() == 1:
288 toyData = mc.GetPdf().generate(mc.GetObservables(), 1)
289 else:
290 print(f"Not sure what to do about this model")
291 else:
292 # print("generating extended dataset")
293 toyData = mc.GetPdf().generate(mc.GetObservables(), Extended=True)
294
295 # generate global observables
296 # need to be careful for simpdf.
297 # In ROOT 5.28 there is a problem with generating global observables
298 # with a simultaneous PDF. In 5.29 there is a solution with
299 # RooSimultaneous::generateSimGlobal, but this may change to
300 # the standard generate interface in 5.30.
301
302 simPdf = ROOT.RooSimultaneous(mc.GetPdf())
303 if not simPdf:
304 one = mc.GetPdf().generate(mc.GetGlobalObservables(), 1)
305 values = one.get()
306 allVars = mc.GetPdf().getVariables()
307 allVars.assign(values)
308 else:
309 one = simPdf.generateSimGlobal(mc.GetGlobalObservables(), 1)
310 values = one.get()
311 allVars = mc.GetPdf().getVariables()
312 allVars.assign(values)
313
314 # get test stat at observed UL in observed data
315 firstPOI.setVal(observedUL)
316 toyTSatObsUL = fc.GetTestStatSampler().EvaluateTestStatistic(toyData, tmpPOI)
317 # toyData.get().Print("v")
318 # print("obsTSatObsUL ", obsTSatObsUL, "toyTS ", toyTSatObsUL)
319 if obsTSatObsUL < toyTSatObsUL: # not sure about <= part yet
320 CLb += (1.0) / nToyMC
321 if obsTSatObsUL <= toyTSatObsUL: # not sure about <= part yet
322 CLbinclusive += (1.0) / nToyMC
323
324 # loop over points in belt to find upper limit for this toy data
325 thisUL = 0
326 for i in range(parameterScan.numEntries()):
327 tmpPoint = parameterScan.get(i).clone("temp")
328 arMax = belt.GetAcceptanceRegionMax(tmpPoint)
329 firstPOI.setVal(tmpPoint.getRealValue(firstPOI.GetName()))
330 # thisTS = profile.getVal()
331 thisTS = fc.GetTestStatSampler().EvaluateTestStatistic(toyData, tmpPOI)
332
333 # print(f"poi = {firstPOI.getVal()} max is {arMax} this profile = {thisTS}")
334 # print("thisTS = ", thisTS)
335 if thisTS <= arMax:
336 thisUL = firstPOI.getVal()
337 else:
338 break
339
340 histOfUL.Fill(thisUL)
341
342 # for few events, data is often the same, and UL is often the same
343 # print("thisUL = ", thisUL)
344
345# At this point we can close the input file, since the RooWorkspace is not used
346# anymore.
347inputFile.Close()
348
349histOfUL.Draw()
350c1.SaveAs("two-sided_upper_limit_output.pdf")
351
352# if you want to see a plot of the sampling distribution for a particular scan point:
353#
354# sampPlot = ROOT.RooStats.SamplingDistPlot()
355# indexInScan = 0
356# tmpPoint = parameterScan.get(indexInScan).clone("temp")
357# firstPOI.setVal( tmpPoint.getRealValue(firstPOI.GetName()) )
358# toymcsampler.SetParametersForTestStat(tmpPOI)
359# samp = toymcsampler.GetSamplingDistribution(tmpPoint)
360# sampPlot.AddSamplingDistribution(samp)
361# sampPlot.Draw()
362
363# Now find bands and power constraint
364bins = histOfUL.GetIntegral()
365cumulative = histOfUL.Clone("cumulative")
366cumulative.SetContent(bins)
367band2sigDown = 0
368band1sigDown = 0
369bandMedian = 0
370band1sigUp = 0
371band2sigUp = 0
372for i in range(1, cumulative.GetNbinsX() + 1):
373 if bins[i] < ROOT.RooStats.SignificanceToPValue(2):
374 band2sigDown = cumulative.GetBinCenter(i)
375 if bins[i] < ROOT.RooStats.SignificanceToPValue(1):
376 band1sigDown = cumulative.GetBinCenter(i)
377 if bins[i] < 0.5:
378 bandMedian = cumulative.GetBinCenter(i)
379 if bins[i] < ROOT.RooStats.SignificanceToPValue(-1):
380 band1sigUp = cumulative.GetBinCenter(i)
381 if bins[i] < ROOT.RooStats.SignificanceToPValue(-2):
382 band2sigUp = cumulative.GetBinCenter(i)
383
384print(f"-2 sigma band {band2sigDown}")
385print(f"-1 sigma band {band1sigDown} [Power Constraint)]")
386print(f"median of band {bandMedian}")
387print(f"+1 sigma band {band1sigUp}")
388print(f"+2 sigma band {band2sigUp}")
389
390# print out the interval on the first Parameter of Interest
391print(f"\nobserved 95% upper-limit {interval.UpperLimit(firstPOI)}")
392print(f"CLb strict [P(toy>obs|0)] for observed 95% upper-limit {CLb}")
393print(f"CLb inclusive [P(toy>=obs|0)] for observed 95% upper-limit {CLbinclusive}")
void Print(GNN_Data &d, std::string txt="")