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.GetXaxis().SetTitle(firstPOI.GetName())
229histOfThresholds.GetYaxis().SetTitle("Threshold")
230
231# loop through the points that were tested and ask confidence belt
232# what the upper/lower thresholds were.
233# For FeldmanCousins, the lower cut off is always 0
234for i in range(parameterScan.numEntries()):
235 tmpPoint = parameterScan.get(i).clone("temp")
236 # print(get threshold")
237 arMax = belt.GetAcceptanceRegionMax(tmpPoint)
238 poiVal = tmpPoint.getRealValue(firstPOI.GetName())
239 histOfThresholds.Fill(poiVal, arMax)
240
241c1 = ROOT.TCanvas()
242c1.Divide(2)
243c1.cd(1)
244histOfThresholds.SetMinimum(0)
245histOfThresholds.Draw()
246c1.cd(2)
247
248# -------------------------------------------------------
249# Now we generate the expected bands and power-constraint
250
251# First: find parameter point for mu=0, with conditional MLEs for nuisance parameters
252nll = mc.GetPdf().createNLL(data)
253profile = nll.createProfile(mc.GetParametersOfInterest())
254firstPOI.setVal(0.0)
255profile.getVal() # this will do fit and set nuisance parameters to profiled values
256poiAndNuisance = ROOT.RooArgSet()
257if mc.GetNuisanceParameters():
258 poiAndNuisance.add(mc.GetNuisanceParameters())
259poiAndNuisance.add(mc.GetParametersOfInterest())
260w.saveSnapshot("paramsToGenerateData", poiAndNuisance)
261paramsToGenerateData = poiAndNuisance.snapshot()
262print("\nWill use these parameter points to generate pseudo data for bkg only")
263paramsToGenerateData.Print("v")
264
265unconditionalObs = ROOT.RooArgSet()
266unconditionalObs.add(mc.GetObservables())
267unconditionalObs.add(mc.GetGlobalObservables()) # comment this out for the original conditional ensemble
268
269CLb = 0
270CLbinclusive = 0
271
272# Now we generate background only and find distribution of upper limits
273histOfUL = ROOT.TH1F("histOfUL", "", 100, 0, firstPOI.getMax())
274histOfUL.GetXaxis().SetTitle("Upper Limit (background only)")
275histOfUL.GetYaxis().SetTitle("Entries")
276for imc in range(nToyMC):
277
278 # set parameters back to values for generating pseudo data
279 # print("\n get current nuis, set vals, print again")
280 w.loadSnapshot("paramsToGenerateData")
281 # poiAndNuisance.Print("v")
282
283 # now generate a toy dataset for the main measurement
284 if not mc.GetPdf().canBeExtended():
285 if data.numEntries() == 1:
286 toyData = mc.GetPdf().generate(mc.GetObservables(), 1)
287 else:
288 print(f"Not sure what to do about this model")
289 else:
290 # print("generating extended dataset")
291 toyData = mc.GetPdf().generate(mc.GetObservables(), Extended=True)
292
293 # generate global observables
294 # need to be careful for simpdf.
295 # In ROOT 5.28 there is a problem with generating global observables
296 # with a simultaneous PDF. In 5.29 there is a solution with
297 # RooSimultaneous::generateSimGlobal, but this may change to
298 # the standard generate interface in 5.30.
299
300 simPdf = ROOT.RooSimultaneous(mc.GetPdf())
301 if not simPdf:
302 one = mc.GetPdf().generate(mc.GetGlobalObservables(), 1)
303 values = one.get()
304 allVars = mc.GetPdf().getVariables()
305 allVars.assign(values)
306 else:
307 one = simPdf.generateSimGlobal(mc.GetGlobalObservables(), 1)
308 values = one.get()
309 allVars = mc.GetPdf().getVariables()
310 allVars.assign(values)
311
312 # get test stat at observed UL in observed data
313 firstPOI.setVal(observedUL)
314 toyTSatObsUL = fc.GetTestStatSampler().EvaluateTestStatistic(toyData, tmpPOI)
315 # toyData.get().Print("v")
316 # print("obsTSatObsUL ", obsTSatObsUL, "toyTS ", toyTSatObsUL)
317 if obsTSatObsUL < toyTSatObsUL: # not sure about <= part yet
318 CLb += (1.0) / nToyMC
319 if obsTSatObsUL <= toyTSatObsUL: # not sure about <= part yet
320 CLbinclusive += (1.0) / nToyMC
321
322 # loop over points in belt to find upper limit for this toy data
323 thisUL = 0
324 for i in range(parameterScan.numEntries()):
325 tmpPoint = parameterScan.get(i).clone("temp")
326 arMax = belt.GetAcceptanceRegionMax(tmpPoint)
327 firstPOI.setVal(tmpPoint.getRealValue(firstPOI.GetName()))
328 # thisTS = profile.getVal()
329 thisTS = fc.GetTestStatSampler().EvaluateTestStatistic(toyData, tmpPOI)
330
331 # print(f"poi = {firstPOI.getVal()} max is {arMax} this profile = {thisTS}")
332 # print("thisTS = ", thisTS)
333 if thisTS <= arMax:
334 thisUL = firstPOI.getVal()
335 else:
336 break
337
338 histOfUL.Fill(thisUL)
339
340 # for few events, data is often the same, and UL is often the same
341 # print("thisUL = ", thisUL)
342
343histOfUL.Draw()
344c1.SaveAs("two-sided_upper_limit_output.pdf")
345
346# if you want to see a plot of the sampling distribution for a particular scan point:
347#
348# sampPlot = ROOT.RooStats.SamplingDistPlot()
349# indexInScan = 0
350# tmpPoint = parameterScan.get(indexInScan).clone("temp")
351# firstPOI.setVal( tmpPoint.getRealValue(firstPOI.GetName()) )
352# toymcsampler.SetParametersForTestStat(tmpPOI)
353# samp = toymcsampler.GetSamplingDistribution(tmpPoint)
354# sampPlot.AddSamplingDistribution(samp)
355# sampPlot.Draw()
356
357# Now find bands and power constraint
358bins = histOfUL.GetIntegral()
359cumulative = ROOT.TH1F()
360cumulative = histOfUL.Clone("cumulative")
361cumulative.SetContent(bins)
362band2sigDown = 0
363band1sigDown = 0
364bandMedian = 0
365band1sigUp = 0
366band2sigUp = 0
367for i in range(1, cumulative.GetNbinsX() + 1):
368 if bins[i] < ROOT.RooStats.SignificanceToPValue(2):
369 band2sigDown = cumulative.GetBinCenter(i)
370 if bins[i] < ROOT.RooStats.SignificanceToPValue(1):
371 band1sigDown = cumulative.GetBinCenter(i)
372 if bins[i] < 0.5:
373 bandMedian = cumulative.GetBinCenter(i)
374 if bins[i] < ROOT.RooStats.SignificanceToPValue(-1):
375 band1sigUp = cumulative.GetBinCenter(i)
376 if bins[i] < ROOT.RooStats.SignificanceToPValue(-2):
377 band2sigUp = cumulative.GetBinCenter(i)
378
379print(f"-2 sigma band {band2sigDown}")
380print(f"-1 sigma band {band1sigDown} [Power Constraint)]")
381print(f"median of band {bandMedian}")
382print(f"+1 sigma band {band1sigUp}")
383print(f"+2 sigma band {band2sigUp}")
384
385# print out the interval on the first Parameter of Interest
386print(f"\nobserved 95% upper-limit {interval.UpperLimit(firstPOI)}")
387print(f"CLb strict [P(toy>obs|0)] for observed 95% upper-limit {CLb}")
388print(f"CLb inclusive [P(toy>=obs|0)] for observed 95% upper-limit {CLbinclusive}")
void Print(GNN_Data &d, std::string txt="")