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.
- {zedstat-0.0.149/zedstat.egg-info → zedstat-0.0.151}/PKG-INFO +2 -2
- zedstat-0.0.151/version.py +1 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/zedstat.py +687 -24
- {zedstat-0.0.149 → zedstat-0.0.151/zedstat.egg-info}/PKG-INFO +2 -2
- zedstat-0.0.149/version.py +0 -1
- {zedstat-0.0.149 → zedstat-0.0.151}/LICENSE +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/MANIFEST.in +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/README.rst +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/setup.cfg +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/setup.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/Untitled-Copy1-checkpoint.ipynb +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/example3-checkpoint.ipynb +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/__init__.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/calibration.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/ci.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/example_combine.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/mwauc.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/rrci.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/textable.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat/zcombine.py +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/SOURCES.txt +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/dependency_links.txt +0 -0
- {zedstat-0.0.149 → zedstat-0.0.151}/zedstat.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
237
|
-
|
|
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[
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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.
|
|
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.
|
|
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
|
zedstat-0.0.149/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.0.149'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{zedstat-0.0.149 → zedstat-0.0.151}/zedstat/.ipynb_checkpoints/Untitled-Copy1-checkpoint.ipynb
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|