zedstat 0.0.149__tar.gz → 0.0.151__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {zedstat-0.0.149/zedstat.egg-info → zedstat-0.0.151}/PKG-INFO +2 -2
  2. zedstat-0.0.151/version.py +1 -0
  3. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/zedstat.py +687 -24
  4. {zedstat-0.0.149 → zedstat-0.0.151/zedstat.egg-info}/PKG-INFO +2 -2
  5. zedstat-0.0.149/version.py +0 -1
  6. {zedstat-0.0.149 → zedstat-0.0.151}/LICENSE +0 -0
  7. {zedstat-0.0.149 → zedstat-0.0.151}/MANIFEST.in +0 -0
  8. {zedstat-0.0.149 → zedstat-0.0.151}/README.rst +0 -0
  9. {zedstat-0.0.149 → zedstat-0.0.151}/setup.cfg +0 -0
  10. {zedstat-0.0.149 → zedstat-0.0.151}/setup.py +0 -0
  11. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/Untitled-Copy1-checkpoint.ipynb +0 -0
  12. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  13. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/example3-checkpoint.ipynb +0 -0
  14. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/__init__.py +0 -0
  15. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/calibration.py +0 -0
  16. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/ci.py +0 -0
  17. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/example_combine.py +0 -0
  18. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/mwauc.py +0 -0
  19. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/rrci.py +0 -0
  20. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/textable.py +0 -0
  21. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/zcombine.py +0 -0
  22. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/SOURCES.txt +0 -0
  23. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/dependency_links.txt +0 -0
  24. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/requires.txt +0 -0
  25. {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zedstat
3
- Version: 0.0.149
3
+ Version: 0.0.151
4
4
  Summary: Statistics tools for ML models and deployment
5
5
  Home-page: https://github.com/zeroknowledgediscovery/zedstat
6
- Download-URL: https://github.com/zeroknowledgediscovery/zedstat/archive/0.0.149.tar.gz
6
+ Download-URL: https://github.com/zeroknowledgediscovery/zedstat/archive/0.0.151.tar.gz
7
7
  Author: zed.uchicago.edu
8
8
  Author-email: ishanu@uchicago.edu
9
9
  License: LICENSE
@@ -0,0 +1 @@
1
+ __version__ = '0.0.151'
@@ -51,6 +51,118 @@ def _wilson_interval_from_proportion(p, n, alpha=0.05):
51
51
  return _clip01(center - radius), _clip01(center + radius)
52
52
 
53
53
 
54
+ def _wilson_interval_from_proportion_variable_n(p, n, alpha=0.05):
55
+ """Vectorized Wilson interval allowing a different effective n per point.
56
+
57
+ Parameters
58
+ ----------
59
+ p : array-like
60
+ Proportion estimate(s).
61
+ n : array-like
62
+ Effective denominator(s). Can be non-integer for prevalence-standardized
63
+ operating-characteristic displays.
64
+ alpha : float
65
+ Significance level.
66
+ """
67
+ p = _clip01(p)
68
+ n = np.asarray(n, dtype=float)
69
+ p, n = np.broadcast_arrays(p, n)
70
+
71
+ lo = np.full(p.shape, np.nan, dtype=float)
72
+ hi = np.full(p.shape, np.nan, dtype=float)
73
+
74
+ mask = np.isfinite(p) & np.isfinite(n) & (n > 0)
75
+ if not np.any(mask):
76
+ return lo, hi
77
+
78
+ z = norm.ppf(1 - alpha / 2.0)
79
+ z2 = z * z
80
+ denom = 1.0 + z2 / n[mask]
81
+ center = (p[mask] + z2 / (2.0 * n[mask])) / denom
82
+ radius = z * np.sqrt((p[mask] * (1.0 - p[mask]) / n[mask]) + (z2 / (4.0 * n[mask] * n[mask]))) / denom
83
+
84
+ lo[mask] = center - radius
85
+ hi[mask] = center + radius
86
+ return _clip01(lo), _clip01(hi)
87
+
88
+
89
+ def _direct_ppv_bounds_from_expected_flags(fpr, tpr, prevalence, total_samples, alpha=0.05, min_expected_flags=1.0):
90
+ """Direct Wilson interval for PPV among the expected flagged set.
91
+
92
+ The older propagated interval combines independent Wilson intervals for TPR
93
+ and specificity. At the degenerate origin, this can produce ppv_upper=1
94
+ even when the classifier flags no one. This function treats PPV directly as
95
+ the event fraction among flagged subjects under the target prevalence.
96
+
97
+ flagged fraction = prevalence * TPR + (1 - prevalence) * FPR
98
+ PPV = prevalence * TPR / flagged fraction
99
+
100
+ If total_samples is available, the effective denominator is
101
+ total_samples * flagged fraction. Rows with fewer than min_expected_flags
102
+ expected flags are undefined for PPV and are returned as NaN.
103
+ """
104
+ fpr = _clip01(fpr)
105
+ tpr = _clip01(tpr)
106
+ p = float(prevalence)
107
+ flagged_fraction = p * tpr + (1.0 - p) * fpr
108
+ ppv = _safe_divide(p * tpr, flagged_fraction)
109
+
110
+ if total_samples is None or not np.isfinite(total_samples) or total_samples <= 0:
111
+ nan = np.full_like(ppv, np.nan, dtype=float)
112
+ return nan, nan
113
+
114
+ n_eff = float(total_samples) * flagged_fraction
115
+ lo, hi = _wilson_interval_from_proportion_variable_n(ppv, n_eff, alpha=alpha)
116
+
117
+ invalid = (~np.isfinite(ppv)) | (~np.isfinite(n_eff)) | (n_eff < float(min_expected_flags)) | (flagged_fraction <= 0)
118
+ lo[invalid] = np.nan
119
+ hi[invalid] = np.nan
120
+ return lo, hi
121
+
122
+
123
+ def _enforce_bounds_around_nominal(nominal, lower, upper, cols=None, clip01_cols=('tpr', 'ppv', 'acc', 'npv')):
124
+ """Ensure lower <= nominal <= upper for displayed pointwise bounds.
125
+
126
+ This is a display/post-processing consistency step. It is especially useful
127
+ when the nominal curve is monotone-corrected but the pointwise analytic
128
+ bounds are computed before that correction.
129
+ """
130
+ nominal = nominal.copy()
131
+ lower = lower.copy()
132
+ upper = upper.copy()
133
+
134
+ if cols is None:
135
+ cols = [c for c in nominal.columns if c in lower.columns and c in upper.columns]
136
+
137
+ for col in cols:
138
+ n = pd.to_numeric(nominal[col], errors='coerce').to_numpy(dtype=float)
139
+ lo = pd.to_numeric(lower[col], errors='coerce').to_numpy(dtype=float)
140
+ hi = pd.to_numeric(upper[col], errors='coerce').to_numpy(dtype=float)
141
+
142
+ finite_n = np.isfinite(n)
143
+ finite_all = finite_n & np.isfinite(lo) & np.isfinite(hi)
144
+
145
+ lo2 = lo.copy()
146
+ hi2 = hi.copy()
147
+ lo2[finite_all] = np.minimum.reduce([lo[finite_all], hi[finite_all], n[finite_all]])
148
+ hi2[finite_all] = np.maximum.reduce([lo[finite_all], hi[finite_all], n[finite_all]])
149
+
150
+ # If the nominal value is undefined, the corresponding bounds should not
151
+ # be plotted or interpreted. This fixes rows such as fpr=0,tpr=0 where
152
+ # PPV is undefined but a propagated upper bound may otherwise be 1.
153
+ lo2[~finite_n] = np.nan
154
+ hi2[~finite_n] = np.nan
155
+
156
+ if col in clip01_cols:
157
+ lo2 = _clip01(lo2)
158
+ hi2 = _clip01(hi2)
159
+
160
+ lower[col] = lo2
161
+ upper[col] = hi2
162
+
163
+ return lower, upper
164
+
165
+
54
166
  def _hanley_mcneil_auc_ci(auc_value, n_pos, n_neg, alpha=0.05):
55
167
  if n_pos is None or n_neg is None or n_pos <= 0 or n_neg <= 0:
56
168
  return np.nan, np.nan
@@ -227,20 +339,43 @@ def _numeric_interp_frame_to_target_index(df, target_index, index_name='fpr'):
227
339
  return out
228
340
 
229
341
 
230
- def _apply_lr_floor(df, lr_fpr_floor, fprcol='fpr'):
342
+ def _apply_lr_floor(df, lr_fpr_floor, fprcol='fpr', lr_sp_floor=None):
343
+ """
344
+ Mask numerically unstable likelihood-ratio regions.
345
+
346
+ LR+ = TPR / FPR is unstable when FPR is close to zero.
347
+ LR- = (1 - TPR) / specificity is unstable when specificity = 1-FPR
348
+ is close to zero.
349
+
350
+ The old implementation masked both LR+ and LR- at low FPR. That created
351
+ odd LR- behavior and did not remove the high-FPR endpoint where LR- is
352
+ undefined or dominated by numerical extrapolation.
353
+ """
231
354
  work = df.copy()
355
+ if lr_sp_floor is None:
356
+ lr_sp_floor = lr_fpr_floor
357
+
232
358
  if work.index.name == fprcol:
233
359
  fpr_vals = work.index.to_numpy(dtype=float)
234
360
  else:
235
361
  fpr_vals = pd.to_numeric(work[fprcol], errors='coerce').to_numpy(dtype=float)
236
- bad = fpr_vals < float(lr_fpr_floor)
237
- for col in ['LR+', 'LR-', 'LR+_raw', 'LR-_raw']:
362
+
363
+ fpr_vals = np.asarray(fpr_vals, dtype=float)
364
+ sp_vals = 1.0 - fpr_vals
365
+
366
+ bad_lr_plus = (~np.isfinite(fpr_vals)) | (fpr_vals <= float(lr_fpr_floor))
367
+ bad_lr_minus = (~np.isfinite(sp_vals)) | (sp_vals <= float(lr_sp_floor))
368
+
369
+ for col in ['LR+', 'LR+_raw']:
370
+ if col in work.columns:
371
+ work.loc[bad_lr_plus, col] = np.nan
372
+ for col in ['LR-', 'LR-_raw']:
238
373
  if col in work.columns:
239
- work.loc[bad, col] = np.nan
374
+ work.loc[bad_lr_minus, col] = np.nan
240
375
  return work
241
376
 
242
377
 
243
- def _compute_measures_from_arrays(fpr, tpr, prevalence, lr_fpr_floor=0.001):
378
+ def _compute_measures_from_arrays(fpr, tpr, prevalence, lr_fpr_floor=0.001, lr_sp_floor=None):
244
379
  fpr = _clip01(fpr)
245
380
  tpr = _clip01(tpr)
246
381
  sp = 1.0 - fpr
@@ -262,11 +397,11 @@ def _compute_measures_from_arrays(fpr, tpr, prevalence, lr_fpr_floor=0.001):
262
397
  'LR-': lr_minus,
263
398
  })
264
399
  frame = frame.replace([np.inf, -np.inf], np.nan)
265
- frame = _apply_lr_floor(frame, lr_fpr_floor=lr_fpr_floor, fprcol='fpr')
400
+ frame = _apply_lr_floor(frame, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol='fpr')
266
401
  return frame
267
402
 
268
403
 
269
- def _compute_pointwise_measure_frames(df, prevalence, alpha, total_samples, positive_samples, thresholdcol='threshold', fprcol='fpr', tprcol='tpr', lr_fpr_floor=0.001):
404
+ def _compute_pointwise_measure_frames(df, prevalence, alpha, total_samples, positive_samples, thresholdcol='threshold', fprcol='fpr', tprcol='tpr', lr_fpr_floor=0.001, lr_sp_floor=None, ppv_ci_method='direct', min_expected_flags=1.0):
270
405
  work, thresholdcol = _prepare_roc_dataframe(df, fprcol=fprcol, tprcol=tprcol, thresholdcol=thresholdcol)
271
406
  work = _add_endpoints(work, fprcol=fprcol, tprcol=tprcol, thresholdcol=thresholdcol)
272
407
 
@@ -282,15 +417,34 @@ def _compute_pointwise_measure_frames(df, prevalence, alpha, total_samples, posi
282
417
  fpr_low = 1.0 - sp_high
283
418
  fpr_high = 1.0 - sp_low
284
419
 
285
- nominal = _compute_measures_from_arrays(fpr, tpr, prevalence=prevalence, lr_fpr_floor=lr_fpr_floor)
420
+ nominal = _compute_measures_from_arrays(fpr, tpr, prevalence=prevalence, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor)
286
421
  lower = pd.DataFrame({'fpr': fpr})
287
422
  upper = pd.DataFrame({'fpr': fpr})
288
423
 
289
424
  lower['tpr'] = tpr_low
290
425
  upper['tpr'] = tpr_high
291
426
 
292
- lower['ppv'] = _safe_divide(tpr_low * prevalence, tpr_low * prevalence + fpr_high * (1.0 - prevalence))
293
- upper['ppv'] = _safe_divide(tpr_high * prevalence, tpr_high * prevalence + fpr_low * (1.0 - prevalence))
427
+ if ppv_ci_method == 'direct':
428
+ # Direct PPV interval among the expected flagged set. This avoids the
429
+ # pathological ppv_upper=1 at thresholds where no one is flagged.
430
+ ppv_low, ppv_high = _direct_ppv_bounds_from_expected_flags(
431
+ fpr=fpr,
432
+ tpr=tpr,
433
+ prevalence=prevalence,
434
+ total_samples=total_samples,
435
+ alpha=alpha,
436
+ min_expected_flags=min_expected_flags,
437
+ )
438
+ lower['ppv'] = ppv_low
439
+ upper['ppv'] = ppv_high
440
+ elif ppv_ci_method == 'propagated':
441
+ # Conservative propagated interval from independent TPR and specificity
442
+ # Wilson intervals. This is retained for backward compatibility but can
443
+ # be visually unstable near fpr=0.
444
+ lower['ppv'] = _safe_divide(tpr_low * prevalence, tpr_low * prevalence + fpr_high * (1.0 - prevalence))
445
+ upper['ppv'] = _safe_divide(tpr_high * prevalence, tpr_high * prevalence + fpr_low * (1.0 - prevalence))
446
+ else:
447
+ raise ValueError("ppv_ci_method must be 'direct' or 'propagated'")
294
448
 
295
449
  lower['acc'] = prevalence * tpr_low + (1.0 - prevalence) * (1.0 - fpr_high)
296
450
  upper['acc'] = prevalence * tpr_high + (1.0 - prevalence) * (1.0 - fpr_low)
@@ -310,10 +464,18 @@ def _compute_pointwise_measure_frames(df, prevalence, alpha, total_samples, posi
310
464
  frame[thresholdcol] = work[thresholdcol].to_numpy(dtype=float)
311
465
  frame.index = work[fprcol].to_numpy(dtype=float)
312
466
  frame.index.name = fprcol
313
- frame = _apply_lr_floor(frame, lr_fpr_floor=lr_fpr_floor, fprcol=fprcol)
314
- nominal = _apply_lr_floor(nominal, lr_fpr_floor=lr_fpr_floor, fprcol=fprcol)
315
- lower = _apply_lr_floor(lower, lr_fpr_floor=lr_fpr_floor, fprcol=fprcol)
316
- upper = _apply_lr_floor(upper, lr_fpr_floor=lr_fpr_floor, fprcol=fprcol)
467
+
468
+ # Do this after the threshold/index is attached.
469
+ nominal = _apply_lr_floor(nominal, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
470
+ lower = _apply_lr_floor(lower, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
471
+ upper = _apply_lr_floor(upper, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
472
+
473
+ lower, upper = _enforce_bounds_around_nominal(
474
+ nominal,
475
+ lower,
476
+ upper,
477
+ cols=['tpr', 'ppv', 'acc', 'npv', 'LR+', 'LR-'],
478
+ )
317
479
  return nominal, lower, upper
318
480
 
319
481
 
@@ -381,6 +543,7 @@ class processRoc(object):
381
543
  self.positive_samples = positive_samples
382
544
  self.alpha = alpha
383
545
  self.lr_fpr_floor = float(lr_fpr_floor)
546
+ self.lr_sp_floor = float(lr_fpr_floor)
384
547
 
385
548
  work, thresholdcol = _prepare_roc_dataframe(df=df, fprcol=fprcol, tprcol=tprcol, thresholdcol=thresholdcol)
386
549
  self.thresholdcol = thresholdcol
@@ -398,6 +561,10 @@ class processRoc(object):
398
561
  self.lr_fpr_floor = float(lr_fpr_floor)
399
562
  return self
400
563
 
564
+ def set_lr_sp_floor(self, lr_sp_floor):
565
+ self.lr_sp_floor = float(lr_sp_floor)
566
+ return self
567
+
401
568
  def set_auc_bootstrap_data(self, df, score_col, label_col, lower_score_is_risk=False):
402
569
  work = df[[score_col, label_col]].dropna().copy()
403
570
  work[label_col] = _ensure_binary_labels(work[label_col])
@@ -595,7 +762,7 @@ class processRoc(object):
595
762
  self.df = self._compute_measures(work, prevalence=self.prevalence, apply_ppv_isotonic=True)
596
763
  return
597
764
 
598
- def usample(self, df=None, precision=3):
765
+ def usample(self, df=None, precision=3, recompute_measures=True):
599
766
  step = 10 ** (-precision)
600
767
  grid = np.round(np.arange(0.0, 1.0 + step, step), precision)
601
768
  grid[-1] = 1.0
@@ -603,6 +770,7 @@ class processRoc(object):
603
770
  if source.index.name == self.fprcol:
604
771
  source = source.reset_index()
605
772
  source = source.sort_values(self.fprcol).reset_index(drop=True)
773
+
606
774
  out = pd.DataFrame({self.fprcol: grid})
607
775
  x = pd.to_numeric(source[self.fprcol], errors='coerce').to_numpy(dtype=float)
608
776
  for col in source.columns:
@@ -616,17 +784,40 @@ class processRoc(object):
616
784
  out[col] = vals[mask][0]
617
785
  else:
618
786
  out[col] = np.nan
787
+
619
788
  if self.tprcol in out.columns:
620
789
  out[self.tprcol] = _clip01(np.maximum.accumulate(out[self.tprcol].values))
790
+
791
+ # Important: do not interpolate derived ratios such as LR+ and LR-.
792
+ # Interpolating them extrapolates finite values into endpoint rows
793
+ # where the ratio is undefined, producing the spurious LR curve branch.
794
+ # Instead, interpolate only the ROC coordinates and recompute all
795
+ # prevalence-derived measures from the resampled FPR/TPR curve.
796
+ if recompute_measures and self.prevalence is not None and self.tprcol in out.columns:
797
+ recomputed = _compute_measures_from_arrays(
798
+ out[self.fprcol].to_numpy(dtype=float),
799
+ out[self.tprcol].to_numpy(dtype=float),
800
+ prevalence=self.prevalence,
801
+ lr_fpr_floor=self.lr_fpr_floor,
802
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
803
+ )
804
+ for col in ['ppv', 'acc', 'npv', 'LR+', 'LR-']:
805
+ out[col] = recomputed[col].to_numpy(dtype=float)
806
+
621
807
  out = out.set_index(self.fprcol)
622
808
  if 'ppv' in out.columns:
623
809
  out = self.__correctPPV(out)
624
- out = _apply_lr_floor(out, lr_fpr_floor=self.lr_fpr_floor, fprcol=self.fprcol)
810
+ out = _apply_lr_floor(
811
+ out,
812
+ lr_fpr_floor=self.lr_fpr_floor,
813
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
814
+ fprcol=self.fprcol,
815
+ )
625
816
  if df is None:
626
817
  self.df = out
627
818
  return out
628
819
 
629
- def getBounds(self, total_samples=None, positive_samples=None, alpha=None, prevalence=None):
820
+ def getBounds(self, total_samples=None, positive_samples=None, alpha=None, prevalence=None, ppv_ci_method='direct', min_expected_flags=1.0, enforce_bounds=True):
630
821
  if total_samples is None:
631
822
  total_samples = self.total_samples
632
823
  if positive_samples is None:
@@ -649,21 +840,30 @@ class processRoc(object):
649
840
  fprcol=self.fprcol,
650
841
  tprcol=self.tprcol,
651
842
  lr_fpr_floor=self.lr_fpr_floor,
843
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
844
+ ppv_ci_method=ppv_ci_method,
845
+ min_expected_flags=min_expected_flags,
652
846
  )
847
+ # Only the displayed nominal PPV is monotone-corrected.
848
+ # Bounds are not isotonic-corrected independently; doing so can make
849
+ # the nominal curve and bounds inconsistent. Bounds are enforced below.
653
850
  nominal_emp = self.__correctPPV(nominal_emp)
654
- lower_emp = self.__correctPPV(lower_emp)
655
- upper_emp = self.__correctPPV(upper_emp)
656
851
 
657
852
  target_index = self.df.index.values.astype(float) if self.df.index.name == self.fprcol else self.df[self.fprcol].values.astype(float)
658
853
  nominal = _numeric_interp_frame_to_target_index(nominal_emp, target_index, index_name=self.fprcol)
659
854
  lower = _numeric_interp_frame_to_target_index(lower_emp, target_index, index_name=self.fprcol)
660
855
  upper = _numeric_interp_frame_to_target_index(upper_emp, target_index, index_name=self.fprcol)
661
856
  nominal = self.__correctPPV(nominal)
662
- lower = self.__correctPPV(lower)
663
- upper = self.__correctPPV(upper)
664
- nominal = _apply_lr_floor(nominal, lr_fpr_floor=self.lr_fpr_floor, fprcol=self.fprcol)
665
- lower = _apply_lr_floor(lower, lr_fpr_floor=self.lr_fpr_floor, fprcol=self.fprcol)
666
- upper = _apply_lr_floor(upper, lr_fpr_floor=self.lr_fpr_floor, fprcol=self.fprcol)
857
+ if enforce_bounds:
858
+ lower, upper = _enforce_bounds_around_nominal(
859
+ nominal,
860
+ lower,
861
+ upper,
862
+ cols=['tpr', 'ppv', 'acc', 'npv', 'LR+', 'LR-'],
863
+ )
864
+ nominal = _apply_lr_floor(nominal, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
865
+ lower = _apply_lr_floor(lower, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
866
+ upper = _apply_lr_floor(upper, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
667
867
 
668
868
  self.df_lim['L'] = lower
669
869
  self.df_lim['U'] = upper
@@ -675,6 +875,9 @@ class processRoc(object):
675
875
  'L_display': lower,
676
876
  'U_display': upper,
677
877
  'kind': 'analytic_pointwise',
878
+ 'ppv_ci_method': ppv_ci_method,
879
+ 'min_expected_flags': min_expected_flags,
880
+ 'enforce_bounds': enforce_bounds,
678
881
  'lr_fpr_floor': self.lr_fpr_floor,
679
882
  }
680
883
  return
@@ -1083,3 +1286,463 @@ def score_to_probability(scores, df, score_col, label_col, target_prevalence=Non
1083
1286
  return probs
1084
1287
  cal_df = calibration_curve_table(work, prob_col='calibrated_prob_oof', label_col=label_col, target_prevalence=target_prevalence, n_bins=n_bins)
1085
1288
  return probs, cal_df
1289
+
1290
+
1291
+ # =============================================================================
1292
+ # Additional fixed bounds/calculation layer
1293
+ # =============================================================================
1294
+ # This block intentionally overrides processRoc.usample and processRoc.getBounds
1295
+ # without changing the public construction/API pattern. It fixes three issues:
1296
+ # 1. derived ratios are recomputed from resampled FPR/TPR rather than
1297
+ # interpolated directly;
1298
+ # 2. PPV confidence bounds are direct Wilson bounds on the expected flagged
1299
+ # population, optionally monotone-smoothed and forced to bracket the point
1300
+ # estimate;
1301
+ # 3. likelihood-ratio quantities are masked in unstable/dominated endpoint
1302
+ # regions, with a helper that returns the nondominated LR frontier.
1303
+
1304
+
1305
+ def _isotonic_monotone_array(y, increasing=False, sample_weight=None, clip01=False):
1306
+ y = np.asarray(y, dtype=float)
1307
+ out = y.copy()
1308
+ valid = np.isfinite(y)
1309
+ if valid.sum() < 2:
1310
+ return _clip01(out) if clip01 else out
1311
+
1312
+ x = np.arange(valid.sum(), dtype=float)
1313
+ if sample_weight is None:
1314
+ w = np.ones(valid.sum(), dtype=float)
1315
+ else:
1316
+ w_all = np.asarray(sample_weight, dtype=float)
1317
+ w = w_all[valid]
1318
+ w = np.where(np.isfinite(w) & (w > 0), w, 1.0)
1319
+
1320
+ iso = IsotonicRegression(increasing=increasing, out_of_bounds='clip')
1321
+ out[valid] = iso.fit_transform(x, y[valid], sample_weight=w)
1322
+ return _clip01(out) if clip01 else out
1323
+
1324
+
1325
+ def _smooth_ppv_bounds_if_requested(lower, upper, nominal, prevalence=None, total_samples=None, smooth_bounds=True):
1326
+ if not smooth_bounds:
1327
+ return lower, upper
1328
+
1329
+ lower = lower.copy()
1330
+ upper = upper.copy()
1331
+ nominal = nominal.copy()
1332
+
1333
+ if 'ppv' not in lower.columns or 'ppv' not in upper.columns:
1334
+ return lower, upper
1335
+
1336
+ if lower.index.name == 'fpr':
1337
+ fpr = lower.index.to_numpy(dtype=float)
1338
+ elif 'fpr' in lower.columns:
1339
+ fpr = pd.to_numeric(lower['fpr'], errors='coerce').to_numpy(dtype=float)
1340
+ else:
1341
+ fpr = np.arange(len(lower), dtype=float)
1342
+
1343
+ if prevalence is not None and total_samples is not None and 'tpr' in nominal.columns:
1344
+ tpr = pd.to_numeric(nominal['tpr'], errors='coerce').to_numpy(dtype=float)
1345
+ ff = float(prevalence) * _clip01(tpr) + (1.0 - float(prevalence)) * _clip01(fpr)
1346
+ weights = np.maximum(float(total_samples) * ff, 1.0)
1347
+ else:
1348
+ weights = np.ones(len(lower), dtype=float)
1349
+
1350
+ lower['ppv'] = _isotonic_monotone_array(lower['ppv'].to_numpy(dtype=float), increasing=False, sample_weight=weights, clip01=True)
1351
+ upper['ppv'] = _isotonic_monotone_array(upper['ppv'].to_numpy(dtype=float), increasing=False, sample_weight=weights, clip01=True)
1352
+ return lower, upper
1353
+
1354
+
1355
+ def _lr_nondominated_mask_from_arrays(lr_minus, lr_plus, min_lrplus=1.0, max_lrminus=None, tol=1e-12):
1356
+ """Return mask for nondominated LR operating points.
1357
+
1358
+ In LR space, smaller LR- and larger LR+ are preferred. A point is dominated
1359
+ if another point has LR- <= current LR- and LR+ >= current LR+.
1360
+
1361
+ min_lrplus removes the no-rule-in branch near LR+ = 1. max_lrminus may be
1362
+ used to restrict to rule-out-relevant thresholds.
1363
+ """
1364
+ lr_minus = np.asarray(lr_minus, dtype=float)
1365
+ lr_plus = np.asarray(lr_plus, dtype=float)
1366
+ keep = np.zeros(lr_minus.shape, dtype=bool)
1367
+
1368
+ cand = np.isfinite(lr_minus) & np.isfinite(lr_plus) & (lr_minus > 0) & (lr_plus > float(min_lrplus))
1369
+ if max_lrminus is not None:
1370
+ cand &= lr_minus < float(max_lrminus)
1371
+ if not np.any(cand):
1372
+ return keep
1373
+
1374
+ idx = np.flatnonzero(cand)
1375
+ order = np.lexsort((-lr_plus[idx], lr_minus[idx]))
1376
+ idx = idx[order]
1377
+
1378
+ best_lrplus = -np.inf
1379
+ for i in idx:
1380
+ if lr_plus[i] > best_lrplus + tol:
1381
+ keep[i] = True
1382
+ best_lrplus = lr_plus[i]
1383
+ return keep
1384
+
1385
+
1386
+ def _apply_lr_frontier_mask_to_frames(nominal, lower, upper, min_lrplus=1.0, max_lrminus=None):
1387
+ nominal = nominal.copy()
1388
+ lower = lower.copy()
1389
+ upper = upper.copy()
1390
+
1391
+ if 'LR-' not in nominal.columns or 'LR+' not in nominal.columns:
1392
+ return nominal, lower, upper
1393
+
1394
+ mask = _lr_nondominated_mask_from_arrays(
1395
+ nominal['LR-'].to_numpy(dtype=float),
1396
+ nominal['LR+'].to_numpy(dtype=float),
1397
+ min_lrplus=min_lrplus,
1398
+ max_lrminus=max_lrminus,
1399
+ )
1400
+ bad = ~mask
1401
+ for frame in [nominal, lower, upper]:
1402
+ for col in ['LR+', 'LR-']:
1403
+ if col in frame.columns:
1404
+ frame.loc[bad, col] = np.nan
1405
+ return nominal, lower, upper
1406
+
1407
+
1408
+ def lr_nondominated_frontier(perf_df, min_lrplus=1.0, max_lrminus=None):
1409
+ """Return the nondominated LR frontier from a performance dataframe.
1410
+
1411
+ The input can be a zedstat output table with FPR as either the index or a
1412
+ column. The returned rows are thresholds for which no other threshold has
1413
+ both a lower/equal LR- and a higher/equal LR+.
1414
+ """
1415
+ pf = perf_df.reset_index().copy()
1416
+ if 'fpr' not in pf.columns:
1417
+ pf = pf.rename(columns={pf.columns[0]: 'fpr'})
1418
+ pf = pf.replace([np.inf, -np.inf], np.nan)
1419
+ if 'LR-' not in pf.columns or 'LR+' not in pf.columns:
1420
+ raise ValueError("perf_df must contain 'LR-' and 'LR+' columns")
1421
+ mask = _lr_nondominated_mask_from_arrays(
1422
+ pf['LR-'].to_numpy(dtype=float),
1423
+ pf['LR+'].to_numpy(dtype=float),
1424
+ min_lrplus=min_lrplus,
1425
+ max_lrminus=max_lrminus,
1426
+ )
1427
+ return pf.loc[mask].sort_values('LR-').reset_index(drop=True)
1428
+
1429
+
1430
+ def _compute_pointwise_measure_frames_fixed(
1431
+ df,
1432
+ prevalence,
1433
+ alpha,
1434
+ total_samples,
1435
+ positive_samples,
1436
+ thresholdcol='threshold',
1437
+ fprcol='fpr',
1438
+ tprcol='tpr',
1439
+ lr_fpr_floor=0.001,
1440
+ lr_sp_floor=None,
1441
+ ppv_ci_method='direct',
1442
+ min_expected_flags=10.0,
1443
+ min_expected_fp=5.0,
1444
+ min_expected_tn=5.0,
1445
+ ):
1446
+ work, thresholdcol = _prepare_roc_dataframe(df, fprcol=fprcol, tprcol=tprcol, thresholdcol=thresholdcol)
1447
+ work = _add_endpoints(work, fprcol=fprcol, tprcol=tprcol, thresholdcol=thresholdcol)
1448
+
1449
+ fpr = _clip01(work[fprcol].to_numpy(dtype=float))
1450
+ tpr = _clip01(work[tprcol].to_numpy(dtype=float))
1451
+ sp = 1.0 - fpr
1452
+
1453
+ n_pos = positive_samples
1454
+ n_neg = None if total_samples is None or positive_samples is None else int(total_samples - positive_samples)
1455
+
1456
+ tpr_low, tpr_high = _wilson_interval_from_proportion(tpr, n_pos, alpha=alpha)
1457
+ sp_low, sp_high = _wilson_interval_from_proportion(sp, n_neg, alpha=alpha)
1458
+ fpr_low = 1.0 - sp_high
1459
+ fpr_high = 1.0 - sp_low
1460
+
1461
+ nominal = _compute_measures_from_arrays(
1462
+ fpr,
1463
+ tpr,
1464
+ prevalence=prevalence,
1465
+ lr_fpr_floor=lr_fpr_floor,
1466
+ lr_sp_floor=lr_sp_floor,
1467
+ )
1468
+ lower = pd.DataFrame({'fpr': fpr})
1469
+ upper = pd.DataFrame({'fpr': fpr})
1470
+
1471
+ lower['tpr'] = tpr_low
1472
+ upper['tpr'] = tpr_high
1473
+
1474
+ if ppv_ci_method == 'direct':
1475
+ ppv_low, ppv_high = _direct_ppv_bounds_from_expected_flags(
1476
+ fpr=fpr,
1477
+ tpr=tpr,
1478
+ prevalence=prevalence,
1479
+ total_samples=total_samples,
1480
+ alpha=alpha,
1481
+ min_expected_flags=min_expected_flags,
1482
+ )
1483
+ lower['ppv'] = ppv_low
1484
+ upper['ppv'] = ppv_high
1485
+ elif ppv_ci_method == 'propagated':
1486
+ lower['ppv'] = _safe_divide(tpr_low * prevalence, tpr_low * prevalence + fpr_high * (1.0 - prevalence))
1487
+ upper['ppv'] = _safe_divide(tpr_high * prevalence, tpr_high * prevalence + fpr_low * (1.0 - prevalence))
1488
+ else:
1489
+ raise ValueError("ppv_ci_method must be 'direct' or 'propagated'")
1490
+
1491
+ lower['acc'] = prevalence * tpr_low + (1.0 - prevalence) * (1.0 - fpr_high)
1492
+ upper['acc'] = prevalence * tpr_high + (1.0 - prevalence) * (1.0 - fpr_low)
1493
+
1494
+ lower['npv'] = _safe_divide((1.0 - fpr_low) * (1.0 - prevalence), (1.0 - fpr_low) * (1.0 - prevalence) + (1.0 - tpr_high) * prevalence)
1495
+ upper['npv'] = _safe_divide((1.0 - fpr_high) * (1.0 - prevalence), (1.0 - fpr_high) * (1.0 - prevalence) + (1.0 - tpr_low) * prevalence)
1496
+
1497
+ lower['LR+'] = _safe_divide(tpr_low, fpr_high)
1498
+ upper['LR+'] = _safe_divide(tpr_high, fpr_low)
1499
+
1500
+ lower['LR-'] = _safe_divide(1.0 - tpr_high, 1.0 - fpr_low)
1501
+ upper['LR-'] = _safe_divide(1.0 - tpr_low, 1.0 - fpr_high)
1502
+
1503
+ # Expected-count masks for unstable LR regions.
1504
+ if total_samples is not None and n_neg is not None:
1505
+ expected_fp = float(n_neg) * fpr
1506
+ expected_tn = float(n_neg) * sp
1507
+ else:
1508
+ expected_fp = np.full_like(fpr, np.nan, dtype=float)
1509
+ expected_tn = np.full_like(fpr, np.nan, dtype=float)
1510
+
1511
+ lr_plus_bad = (
1512
+ (fpr <= float(lr_fpr_floor)) |
1513
+ (fpr_low <= 0) |
1514
+ (~np.isfinite(expected_fp)) |
1515
+ (expected_fp < float(min_expected_fp))
1516
+ )
1517
+ if lr_sp_floor is None:
1518
+ lr_sp_floor = lr_fpr_floor
1519
+ lr_minus_bad = (
1520
+ (sp <= float(lr_sp_floor)) |
1521
+ (sp_low <= 0) |
1522
+ (~np.isfinite(expected_tn)) |
1523
+ (expected_tn < float(min_expected_tn))
1524
+ )
1525
+
1526
+ for frame in [nominal, lower, upper]:
1527
+ frame.replace([np.inf, -np.inf], np.nan, inplace=True)
1528
+ if thresholdcol is not None and thresholdcol in work.columns:
1529
+ frame[thresholdcol] = work[thresholdcol].to_numpy(dtype=float)
1530
+ frame.index = work[fprcol].to_numpy(dtype=float)
1531
+ frame.index.name = fprcol
1532
+ frame['expected_fp'] = expected_fp
1533
+ frame['expected_tn'] = expected_tn
1534
+ p = float(prevalence)
1535
+ frame['expected_flags'] = float(total_samples) * (p * tpr + (1.0 - p) * fpr) if total_samples is not None else np.nan
1536
+ if 'LR+' in frame.columns:
1537
+ frame.loc[lr_plus_bad, 'LR+'] = np.nan
1538
+ if 'LR-' in frame.columns:
1539
+ frame.loc[lr_minus_bad, 'LR-'] = np.nan
1540
+
1541
+ nominal = _apply_lr_floor(nominal, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
1542
+ lower = _apply_lr_floor(lower, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
1543
+ upper = _apply_lr_floor(upper, lr_fpr_floor=lr_fpr_floor, lr_sp_floor=lr_sp_floor, fprcol=fprcol)
1544
+
1545
+ lower, upper = _enforce_bounds_around_nominal(
1546
+ nominal,
1547
+ lower,
1548
+ upper,
1549
+ cols=['tpr', 'ppv', 'acc', 'npv', 'LR+', 'LR-'],
1550
+ )
1551
+ return nominal, lower, upper
1552
+
1553
+
1554
+ def _processRoc_usample_fixed(self, df=None, precision=3, recompute_measures=True):
1555
+ step = 10 ** (-precision)
1556
+ grid = np.round(np.arange(0.0, 1.0 + step, step), precision)
1557
+ grid[-1] = 1.0
1558
+ source = self.df.copy() if df is None else df.copy()
1559
+ if source.index.name == self.fprcol:
1560
+ source = source.reset_index()
1561
+ source = source.sort_values(self.fprcol).reset_index(drop=True)
1562
+
1563
+ out = pd.DataFrame({self.fprcol: grid})
1564
+ x = pd.to_numeric(source[self.fprcol], errors='coerce').to_numpy(dtype=float)
1565
+
1566
+ # Interpolate only primary coordinates and threshold-like quantities.
1567
+ # Derived operating measures are recomputed below.
1568
+ skip_if_recompute = {'ppv', 'acc', 'npv', 'LR+', 'LR-'}
1569
+ for col in source.columns:
1570
+ if col == self.fprcol:
1571
+ continue
1572
+ if recompute_measures and col in skip_if_recompute:
1573
+ continue
1574
+ vals = pd.to_numeric(source[col], errors='coerce').to_numpy(dtype=float)
1575
+ mask = np.isfinite(x) & np.isfinite(vals)
1576
+ if mask.sum() >= 2:
1577
+ out[col] = np.interp(grid, x[mask], vals[mask])
1578
+ elif mask.sum() == 1:
1579
+ out[col] = vals[mask][0]
1580
+ else:
1581
+ out[col] = np.nan
1582
+
1583
+ if self.tprcol in out.columns:
1584
+ out[self.tprcol] = _clip01(np.maximum.accumulate(out[self.tprcol].values))
1585
+
1586
+ if recompute_measures and self.prevalence is not None and self.tprcol in out.columns:
1587
+ recomputed = _compute_measures_from_arrays(
1588
+ out[self.fprcol].to_numpy(dtype=float),
1589
+ out[self.tprcol].to_numpy(dtype=float),
1590
+ prevalence=self.prevalence,
1591
+ lr_fpr_floor=self.lr_fpr_floor,
1592
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
1593
+ )
1594
+ for col in ['ppv', 'acc', 'npv', 'LR+', 'LR-']:
1595
+ out[col] = recomputed[col].to_numpy(dtype=float)
1596
+
1597
+ out = out.set_index(self.fprcol)
1598
+ if 'ppv' in out.columns:
1599
+ out = self._processRoc__correctPPV(out)
1600
+ out = _apply_lr_floor(
1601
+ out,
1602
+ lr_fpr_floor=self.lr_fpr_floor,
1603
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
1604
+ fprcol=self.fprcol,
1605
+ )
1606
+ if df is None:
1607
+ self.df = out
1608
+ return out
1609
+
1610
+
1611
+ def _processRoc_getBounds_fixed(
1612
+ self,
1613
+ total_samples=None,
1614
+ positive_samples=None,
1615
+ alpha=None,
1616
+ prevalence=None,
1617
+ ppv_ci_method='direct',
1618
+ min_expected_flags=10.0,
1619
+ min_expected_fp=5.0,
1620
+ min_expected_tn=5.0,
1621
+ enforce_bounds=True,
1622
+ smooth_ppv_bounds=True,
1623
+ mask_dominated_lr=True,
1624
+ lr_min_plus=1.001,
1625
+ lr_max_minus=None,
1626
+ ):
1627
+ if total_samples is None:
1628
+ total_samples = self.total_samples
1629
+ if positive_samples is None:
1630
+ positive_samples = self.positive_samples
1631
+ if alpha is None:
1632
+ alpha = self.alpha
1633
+ if prevalence is None:
1634
+ prevalence = self.prevalence
1635
+ if prevalence is None:
1636
+ raise ValueError('prevalence undefined')
1637
+
1638
+ empirical = self.raw_df.reset_index().copy()
1639
+ nominal_emp, lower_emp, upper_emp = _compute_pointwise_measure_frames_fixed(
1640
+ empirical,
1641
+ prevalence=float(prevalence),
1642
+ alpha=alpha,
1643
+ total_samples=total_samples,
1644
+ positive_samples=positive_samples,
1645
+ thresholdcol=self.thresholdcol,
1646
+ fprcol=self.fprcol,
1647
+ tprcol=self.tprcol,
1648
+ lr_fpr_floor=self.lr_fpr_floor,
1649
+ lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
1650
+ ppv_ci_method=ppv_ci_method,
1651
+ min_expected_flags=min_expected_flags,
1652
+ min_expected_fp=min_expected_fp,
1653
+ min_expected_tn=min_expected_tn,
1654
+ )
1655
+
1656
+ nominal_emp = self._processRoc__correctPPV(nominal_emp)
1657
+ lower_emp, upper_emp = _smooth_ppv_bounds_if_requested(
1658
+ lower_emp,
1659
+ upper_emp,
1660
+ nominal_emp,
1661
+ prevalence=prevalence,
1662
+ total_samples=total_samples,
1663
+ smooth_bounds=smooth_ppv_bounds,
1664
+ )
1665
+ if enforce_bounds:
1666
+ lower_emp, upper_emp = _enforce_bounds_around_nominal(
1667
+ nominal_emp,
1668
+ lower_emp,
1669
+ upper_emp,
1670
+ cols=['tpr', 'ppv', 'acc', 'npv', 'LR+', 'LR-'],
1671
+ )
1672
+
1673
+ target_index = self.df.index.values.astype(float) if self.df.index.name == self.fprcol else self.df[self.fprcol].values.astype(float)
1674
+ nominal = _numeric_interp_frame_to_target_index(nominal_emp, target_index, index_name=self.fprcol)
1675
+ lower = _numeric_interp_frame_to_target_index(lower_emp, target_index, index_name=self.fprcol)
1676
+ upper = _numeric_interp_frame_to_target_index(upper_emp, target_index, index_name=self.fprcol)
1677
+
1678
+ nominal = self._processRoc__correctPPV(nominal)
1679
+ lower, upper = _smooth_ppv_bounds_if_requested(
1680
+ lower,
1681
+ upper,
1682
+ nominal,
1683
+ prevalence=prevalence,
1684
+ total_samples=total_samples,
1685
+ smooth_bounds=smooth_ppv_bounds,
1686
+ )
1687
+ if enforce_bounds:
1688
+ lower, upper = _enforce_bounds_around_nominal(
1689
+ nominal,
1690
+ lower,
1691
+ upper,
1692
+ cols=['tpr', 'ppv', 'acc', 'npv', 'LR+', 'LR-'],
1693
+ )
1694
+
1695
+ nominal = _apply_lr_floor(nominal, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
1696
+ lower = _apply_lr_floor(lower, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
1697
+ upper = _apply_lr_floor(upper, lr_fpr_floor=self.lr_fpr_floor, lr_sp_floor=getattr(self, 'lr_sp_floor', self.lr_fpr_floor), fprcol=self.fprcol)
1698
+
1699
+ if mask_dominated_lr:
1700
+ nominal, lower, upper = _apply_lr_frontier_mask_to_frames(
1701
+ nominal,
1702
+ lower,
1703
+ upper,
1704
+ min_lrplus=lr_min_plus,
1705
+ max_lrminus=lr_max_minus,
1706
+ )
1707
+
1708
+ # Ensure zt.get() returns the same repaired nominal calculations that are
1709
+ # used to define the displayed bounds.
1710
+ self.df = nominal.copy()
1711
+ self.df_lim['L'] = lower
1712
+ self.df_lim['U'] = upper
1713
+ self.df_measure_bounds_ = {
1714
+ 'nominal_empirical': nominal_emp,
1715
+ 'L_empirical': lower_emp,
1716
+ 'U_empirical': upper_emp,
1717
+ 'nominal_display': nominal,
1718
+ 'L_display': lower,
1719
+ 'U_display': upper,
1720
+ 'kind': 'analytic_pointwise_fixed',
1721
+ 'ppv_ci_method': ppv_ci_method,
1722
+ 'min_expected_flags': min_expected_flags,
1723
+ 'min_expected_fp': min_expected_fp,
1724
+ 'min_expected_tn': min_expected_tn,
1725
+ 'enforce_bounds': enforce_bounds,
1726
+ 'smooth_ppv_bounds': smooth_ppv_bounds,
1727
+ 'mask_dominated_lr': mask_dominated_lr,
1728
+ 'lr_min_plus': lr_min_plus,
1729
+ 'lr_max_minus': lr_max_minus,
1730
+ 'lr_fpr_floor': self.lr_fpr_floor,
1731
+ 'lr_sp_floor': getattr(self, 'lr_sp_floor', self.lr_fpr_floor),
1732
+ }
1733
+ return
1734
+
1735
+
1736
+ def _processRoc_lr_frontier(self, min_lrplus=1.0, max_lrminus=None):
1737
+ return lr_nondominated_frontier(self.get(), min_lrplus=min_lrplus, max_lrminus=max_lrminus)
1738
+
1739
+
1740
+ # Override processRoc methods with the fixed versions.
1741
+ processRoc.usample = _processRoc_usample_fixed
1742
+ processRoc.getBounds = _processRoc_getBounds_fixed
1743
+ processRoc.lr_frontier = _processRoc_lr_frontier
1744
+
1745
+ try:
1746
+ __all__.append('lr_nondominated_frontier')
1747
+ except Exception:
1748
+ pass
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zedstat
3
- Version: 0.0.149
3
+ Version: 0.0.151
4
4
  Summary: Statistics tools for ML models and deployment
5
5
  Home-page: https://github.com/zeroknowledgediscovery/zedstat
6
- Download-URL: https://github.com/zeroknowledgediscovery/zedstat/archive/0.0.149.tar.gz
6
+ Download-URL: https://github.com/zeroknowledgediscovery/zedstat/archive/0.0.151.tar.gz
7
7
  Author: zed.uchicago.edu
8
8
  Author-email: ishanu@uchicago.edu
9
9
  License: LICENSE
@@ -1 +0,0 @@
1
- __version__ = '0.0.149'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes