2828
2929# Statistics modules from SciPy
3030try :
31+ from scipy .stats import binom
3132 from scipy .stats import norm
3233 from scipy .stats import t as student
3334except ImportError :
3435 pass
3536
37+
38+ # Get orderered stats with confidence level
39+ def getidx_ordered_stats (p : float , cl : float , n : int ) -> float :
40+ r"""Get number of cases to exclude for ordered statistics w/ CL
41+
42+ :Call:
43+ >>> m = getidx_ordered_stats(p, cl, n)
44+ :Inputs:
45+ *p*: :class:`float` | :class:`np.ndarray`\ [:class:`float`]
46+ Coverage fraction, probability
47+ *cl*: :class:`float` | :class:`np.ndarray`\ [:class:`float`]
48+ Confidence interval, or array thereof
49+ *n*: :class:`int`
50+ Number of samples (must be scalar)
51+ :Versions:
52+ * 2025-09-24 ``@ddalle``: v1.0
53+ """
54+ # Check for lower bounds
55+ if isinstance (p , (float , np .floating )) and (p < 0.5 ):
56+ return n - getidx_ordered_stats (1 - p , cl , n )
57+ # Array of failure counts
58+ m_excl = np .arange (n + 1 )
59+ m_keep = m_excl [::- 1 ]
60+ # Use binomial distribution to get p(exactly k failures)
61+ p_excl = binom .pmf (m_keep , n , p )
62+ # Cumulative probability
63+ total_p_excl = np .cumsum (p_excl )
64+ total_confidence = 1 - total_p_excl
65+ # Get unique values to avoid issues with confidence < machine prec.
66+ tmp , idx = np .unique (total_confidence [::- 1 ], return_index = True )
67+ # Interpolate to requested confidnece level
68+ return np .interp (cl , tmp , m_excl [idx ])
69+
70+
3671# Get ordered stats
3772def get_ordered_stats (V , cov = None , onesided = False , ** kw ):
3873 r"""Calculate coverage using ordered statistics
@@ -61,7 +96,7 @@ def get_ordered_stats(V, cov=None, onesided=False, **kw):
6196 *vlim*: :class:`float`
6297 Upper limit of one-sided coverage interval
6398 :Versions:
64- * 2021-09-30 ``@ddalle``: Version 1 .0
99+ * 2021-09-30 ``@ddalle``: v1 .0
65100 """
66101 # Get standard deviation counts
67102 ksig = kw .get ("ksig" )
@@ -119,7 +154,7 @@ def get_ordered_lower(V, cov):
119154 than or equal to *v*; may be interpolated between sorted
120155 values of *V*
121156 :Versions:
122- * 2021-09-30 ``@ddalle``: Version 1 .0
157+ * 2021-09-30 ``@ddalle``: v1 .0
123158 """
124159 # Get size
125160 n = len (V )
@@ -164,7 +199,7 @@ def get_ordered_upper(V, cov):
164199 or equal to *v*; may be interpolated between sorted values
165200 of *V*
166201 :Versions:
167- * 2021-09-30 ``@ddalle``: Version 1 .0
202+ * 2021-09-30 ``@ddalle``: v1 .0
168203 """
169204 # Get size
170205 n = len (V )
@@ -190,16 +225,16 @@ def get_ordered_upper(V, cov):
190225 cova = ia / float (n )
191226 # Interpolate ... n = 1 / (covb-cova)
192227 return va + n * (cov - cova )* (vb - va )
193-
228+
194229
195230# Calculate range
196231def get_range (R , cov = None , ** kw ):
197232 r"""Calculate Student's t-distribution confidence range
198-
233+
199234 If the nominal application of the Student's t-distribution fails to
200235 cover a high enough fraction of the data, the bounds are extended
201236 until the data is covered.
202-
237+
203238 :Call:
204239 >>> width = get_range(R, cov, **kw)
205240 :Inputs:
@@ -218,8 +253,8 @@ def get_range(R, cov=None, **kw):
218253 *width*: :class:`float`
219254 Half-width of confidence region
220255 :Versions:
221- * 2018-09-28 ``@ddalle``: Version 1 .0
222- * 2021-09-20 ``@ddalle``: Version 1 .1
256+ * 2018-09-28 ``@ddalle``: v1 .0
257+ * 2021-09-20 ``@ddalle``: v1 .1
223258 - use :func:`_parse_options`
224259 - allow 100% coverage
225260 - remove confusing *kcov* vs *ksig* scaling
@@ -273,11 +308,11 @@ def get_range(R, cov=None, **kw):
273308# Calculate interval
274309def get_coverage (dx , cov = None , ** kw ):
275310 r"""Calculate Student's *t*\ -distribution confidence range
276-
311+
277312 If the nominal application of the Student's t-distribution fails to
278313 cover a high enough fraction of the data, the bounds are extended
279314 until *cov* (user-defined fraction) of the data is covered.
280-
315+
281316 :Call:
282317 >>> width = get_coverage(dx, cov, **kw)
283318 :Inputs:
@@ -296,8 +331,8 @@ def get_coverage(dx, cov=None, **kw):
296331 *width*: :class:`float`
297332 Half-width of confidence region
298333 :Versions:
299- * 2019-02-04 ``@ddalle``: Version 1 .0
300- * 2021-09-20 ``@ddalle``: Version 1 .1
334+ * 2019-02-04 ``@ddalle``: v1 .0
335+ * 2021-09-20 ``@ddalle``: v1 .1
301336 - use :func:`_parse_options`
302337 - allow 100% coverage
303338 - remove confusing *kcov* vs *ksig* scaling
@@ -311,11 +346,11 @@ def get_coverage(dx, cov=None, **kw):
311346# Calculate interval
312347def get_cov_interval (dx , cov = None , ** kw ):
313348 r"""Calculate Student's *t*\ -distribution confidence range
314-
349+
315350 If the nominal application of the Student's t-distribution fails to
316351 cover a high enough fraction of the data, the bounds are extended
317352 until *cov* (user-defined fraction) of the data is covered.
318-
353+
319354 :Call:
320355 >>> a, b = get_cov_interval(dx, cov, **kw)
321356 :Inputs:
@@ -336,8 +371,8 @@ def get_cov_interval(dx, cov=None, **kw):
336371 *b*: :class:`float`
337372 Upper bound of coverage interval
338373 :Versions:
339- * 2019-02-04 ``@ddalle``: Version 1 .0
340- * 2021-09-20 ``@ddalle``: Version 1 .1
374+ * 2019-02-04 ``@ddalle``: v1 .0
375+ * 2021-09-20 ``@ddalle``: v1 .1
341376 - use :func:`_parse_options`
342377 - allow 100% coverage
343378 - remove confusing *kcov* vs *ksig* scaling
@@ -372,7 +407,7 @@ def get_cov_interval(dx, cov=None, **kw):
372407 b = vmu + width
373408 # --- Coverage Check ---
374409 # Filter cases that are outside bounds
375- J = np .logical_and (a <= dx , dx <= b )
410+ J = np .logical_and (a <= dx , dx <= b )
376411 # Count cases outside the bounds
377412 ncov = np .count_nonzero (J )
378413 # Check coverage
@@ -396,7 +431,7 @@ def get_cov_interval(dx, cov=None, **kw):
396431# Filter outliers
397432def check_outliers_range (R , cov = None , ** kw ):
398433 r"""Find outliers in an array of ranges
399-
434+
400435 :Call:
401436 >>> I = check_outliers_range(R, cov, **kw)
402437 :Inputs:
@@ -415,7 +450,7 @@ def check_outliers_range(R, cov=None, **kw):
415450 *I*: :class:`np.ndarray`\ [:class:`bool`]
416451 Flags for non-outlier cases, ``False`` if case is an outlier
417452 :Versions:
418- * 2021-02-20 ``@ddalle``: Version 1 .0
453+ * 2021-02-20 ``@ddalle``: v1 .0
419454 """
420455 # --- Setup ---
421456 # Enforce array (copy to preserve original data)
@@ -452,8 +487,8 @@ def check_outliers_range(R, cov=None, **kw):
452487 # Update degrees of freedom
453488 df = N - n1
454489 # Recalculate statistics
455- vmu = np .mean (dx [I ])
456- vstd = np .std (dx [I ])
490+ vmu = np .mean (R [I ])
491+ vstd = np .std (R [I ])
457492 # Find outliers
458493 I = R / vstd <= osig
459494 J = np .logical_not (I )
@@ -466,7 +501,7 @@ def check_outliers_range(R, cov=None, **kw):
466501# Filter outliers
467502def check_outliers (dx , cov = None , ** kw ):
468503 r"""Find outliers in a data set
469-
504+
470505 :Call:
471506 >>> I = check_outliers(dx, cov, **kw)
472507 :Inputs:
@@ -485,8 +520,8 @@ def check_outliers(dx, cov=None, **kw):
485520 *I*: :class:`np.ndarray`\ [:class:`bool`]
486521 Flags for non-outlier cases, ``False`` if case is an outlier
487522 :Versions:
488- * 2019-02-04 ``@ddalle``: Version 1 .0
489- * 2021-09-20 ``@ddalle``: Version 1 .1
523+ * 2019-02-04 ``@ddalle``: v1 .0
524+ * 2021-09-20 ``@ddalle``: v1 .1
490525 - use :func:`_parse_options`
491526 - allow 100% coverage
492527 """
0 commit comments