Skip to content

Commit 704b1e7

Browse files
committed
Revert "[RF] Replace asymptotic Poisson interval with exact Garwood intervals"
This reverts commit 770f648.
1 parent a2bea54 commit 704b1e7

3 files changed

Lines changed: 70 additions & 60 deletions

File tree

README/ReleaseNotes/v640/index.md

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -205,28 +205,6 @@ As part of this migration, the following build options are deprecated. From ROOT
205205
overhead in `RooAbsRealLValue::inRange()`, which is visible in many-parameter
206206
fits. Therefore, these functions are removed.
207207

208-
### New implementation of `RooHistError::getPoissonInterval`
209-
210-
**RooHistError::getPoissonInterval** was reimplemented to use an exact chi-square–based construction (Garwood interval) instead of asymptotic approximations and lookup tables.
211-
212-
Previously:
213-
214-
* For `n > 100`, a hard-coded asymptotic formula was used, which was not explained in the documentation.
215-
* That approximation corresponds to inverting the Poisson score test and was only correct for `nSigma = 1`.
216-
* The behavior for `n > 100` was not statistically consistent because of the hard transition between exact formula and approximation, resulting in a discrete and unexpected jump in approximation bias.
217-
* For small `n`, numerical root finding and a lookup table were used.
218-
219-
Now:
220-
221-
* The interval is computed directly using chi-square quantiles for all `n`.
222-
* The construction provides exact frequentist coverage.
223-
* The implementation works consistently for arbitrary `nSigma`.
224-
* The hard-coded `n > 100` threshold, lookup table, and numerical root finding were removed.
225-
* For `n = 0`, a one-sided upper limit is used (with lower bound fixed at 0), consistent with the physical constraint `mu ≥ 0`.
226-
227-
Results may differ from previous ROOT versions for `n > 100` or `nSigma != 1`.
228-
The new implementation is statistically consistent and recommended.
229-
230208
## RDataFrame
231209

232210
- The message shown in ROOT 6.38 to inform users about change of default compression setting used by Snapshot (was 101 before 6.38, became 505 in 6.38) is now removed.

roofit/roofitcore/inc/RooHistError.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ class RooHistError {
3737
static RooAbsFunc *createBinomialSum(Int_t n, Int_t m, bool eff) ;
3838

3939
private:
40+
41+
42+
bool getPoissonIntervalCalc(Int_t n, double &mu1, double &mu2, double nSigma= 1) const;
43+
double _poissonLoLUT[1000] ;
44+
double _poissonHiLUT[1000] ;
45+
46+
RooHistError();
4047
double seek(const RooAbsFunc &f, double startAt, double step, double value) const;
4148

4249
// -----------------------------------------------------------

roofit/roofitcore/src/RooHistError.cxx

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,10 @@ a specified area of a Poisson or Binomail error distribution.
2727
#include "RooHistError.h"
2828
#include "RooBrentRootFinder.h"
2929
#include "RooMsgService.h"
30-
31-
#include "Math/QuantFuncMathCore.h" // ROOT::Math::chisquared_quantile, chisquared_quantile_c
30+
#include "TMath.h"
3231

3332
#include "Riostream.h"
34-
3533
#include <cassert>
36-
#include <cmath>
3734

3835
using std::endl;
3936

@@ -50,48 +47,76 @@ const RooHistError &RooHistError::instance()
5047
}
5148

5249

50+
////////////////////////////////////////////////////////////////////////////////
51+
/// Construct our singleton object.
52+
53+
RooHistError::RooHistError()
54+
{
55+
// Initialize lookup table ;
56+
Int_t i ;
57+
for (i=0 ; i<1000 ; i++) {
58+
getPoissonIntervalCalc(i,_poissonLoLUT[i],_poissonHiLUT[i],1.) ;
59+
}
60+
61+
}
62+
63+
64+
65+
////////////////////////////////////////////////////////////////////////////////
66+
/// Return a confidence interval for the expected number of events given n
67+
/// observed (unweighted) events. The interval will contain the same probability
68+
/// as nSigma of a Gaussian. Uses a central interval unless this does not enclose
69+
/// the point estimate n (ie, for small n) in which case the interval is adjusted
70+
/// to start at n. This method uses a lookup table to return precalculated results
71+
/// for n<1000
72+
73+
bool RooHistError::getPoissonInterval(Int_t n, double &mu1, double &mu2, double nSigma) const
74+
{
75+
// Use lookup table for most common cases
76+
if (n<1000 && nSigma==1.) {
77+
mu1=_poissonLoLUT[n] ;
78+
mu2=_poissonHiLUT[n] ;
79+
return true ;
80+
}
81+
82+
// Forward to calculation method
83+
bool ret = getPoissonIntervalCalc(n,mu1,mu2,nSigma) ;
84+
return ret ;
85+
}
86+
87+
88+
5389
////////////////////////////////////////////////////////////////////////////////
5490
/// Calculate a confidence interval for the expected number of events given n
5591
/// observed (unweighted) events. The interval will contain the same probability
5692
/// as nSigma of a Gaussian. Uses a central interval unless this does not enclose
5793
/// the point estimate n (ie, for small n) in which case the interval is adjusted
5894
/// to start at n.
5995

60-
bool RooHistError::getPoissonInterval(Int_t n, double &mu1, double &mu2, double nSigma) const
96+
bool RooHistError::getPoissonIntervalCalc(Int_t n, double &mu1, double &mu2, double nSigma) const
6197
{
62-
// sanity checks
63-
if (n < 0) {
64-
oocoutE(nullptr, Plotting) << "RooHistError::getPoissonInterval: cannot calculate interval for n = " << n
65-
<< std::endl;
66-
return false;
67-
}
68-
if (!(nSigma > 0.0)) {
69-
oocoutE(nullptr, Plotting) << "RooHistError::getPoissonInterval: nSigma must be > 0, got " << nSigma << std::endl;
70-
return false;
71-
}
72-
73-
// Convert "number of sigmas" to central Gaussian probability beta, and
74-
// corresponding two-sided tail probability alpha.
75-
const double beta = std::erf(nSigma / std::sqrt(2.0));
76-
const double alpha = 1.0 - beta;
77-
78-
// Special case n = 0 (boundary at mu >= 0).
79-
// Use a one-sided interval including the MLE mu=0:
80-
// mu1 = 0
81-
// P(N <= 0 | mu2) = alpha,
82-
// with alpha = 1 - erf(nSigma / sqrt(2)).
83-
if (n == 0) {
84-
mu1 = 0.0;
85-
mu2 = 0.5 * ROOT::Math::chisquared_quantile(1.0 - alpha, 2.0 * (n + 1));
86-
return true;
87-
}
88-
89-
// For n>0, use the central (equal-tailed) Garwood interval, which
90-
// corresponds to allocating alpha/2 in each tail.
91-
const double a2 = 0.5 * alpha;
92-
mu1 = 0.5 * ROOT::Math::chisquared_quantile(a2, 2.0 * n);
93-
mu2 = 0.5 * ROOT::Math::chisquared_quantile_c(a2, 2.0 * (n + 1));
94-
return true;
98+
// sanity checks
99+
if(n < 0) {
100+
oocoutE(nullptr,Plotting) << "RooHistError::getPoissonInterval: cannot calculate interval for n = " << n << std::endl;
101+
return false;
102+
}
103+
104+
// use asymptotic error if possible
105+
if(n > 100) {
106+
mu1= n - sqrt(n+0.25) + 0.5;
107+
mu2= n + sqrt(n+0.25) + 0.5;
108+
return true;
109+
}
110+
111+
// create a function object to use
112+
PoissonSum upper(n);
113+
if(n > 0) {
114+
PoissonSum lower(n-1);
115+
return getInterval(&upper,&lower,(double)n,1.0,mu1,mu2,nSigma);
116+
}
117+
118+
// Backup solution for negative numbers
119+
return getInterval(&upper,nullptr,(double)n,1.0,mu1,mu2,nSigma);
95120
}
96121

97122

0 commit comments

Comments
 (0)