-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathisic_helper.py
125 lines (100 loc) · 4.3 KB
/
isic_helper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# %% [code]
# %% [code]
from typing import Any
from argparse import Namespace
import typing
import numpy as np
from sklearn.metrics import roc_curve, auc
from sklearn.metrics import roc_auc_score as compute_auc
def time_to_str(duration, unit='min'):
if unit == "min":
minutes = int(duration // 60)
seconds = duration % 60
return f"{minutes} min {seconds:.2f} sec"
else:
return f"{duration:.2f} sec"
def compute_pauc(y_true, y_pred, min_tpr: float = 0.80) -> float:
"""
2024 ISIC Challenge metric: pAUC
Given a solution file and submission file, this function returns the
partial area under the receiver operating characteristic (pAUC)
above a given true positive rate (TPR) = 0.80.
https://en.wikipedia.org/wiki/Partial_Area_Under_the_ROC_Curve.
(c) 2024 Nicholas R Kurtansky, MSKCC
Args:
min_tpr: minimum true positive rate
y_true: ground truth of 1s and 0s
y_pred: predictions of scores ranging [0, 1]
Returns:
Float value range [0, max_fpr]
"""
# rescale the target. set 0s to 1s and 1s to 0s (since sklearn only has max_fpr)
v_gt = abs(y_true - 1)
# flip the submissions to their compliments
v_pred = -1.0 * y_pred
max_fpr = abs(1 - min_tpr)
# using sklearn.metric functions: (1) roc_curve and (2) auc
fpr, tpr, _ = roc_curve(v_gt, v_pred, sample_weight=None)
if max_fpr is None or max_fpr == 1:
return auc(fpr, tpr)
if max_fpr <= 0 or max_fpr > 1:
raise ValueError("Expected min_tpr in range [0, 1), got: %r" % min_tpr)
# Add a single point at max_fpr by linear interpolation
stop = np.searchsorted(fpr, max_fpr, "right")
x_interp = [fpr[stop - 1], fpr[stop]]
y_interp = [tpr[stop - 1], tpr[stop]]
tpr = np.append(tpr[:stop], np.interp(max_fpr, x_interp, y_interp))
fpr = np.append(fpr[:stop], max_fpr)
partial_auc = auc(fpr, tpr)
# # Equivalent code that uses sklearn's roc_auc_score
# v_gt = abs(np.asarray(solution.values)-1)
# v_pred = np.array([1.0 - x for x in submission.values])
# max_fpr = abs(1-min_tpr)
# partial_auc_scaled = roc_auc_score(v_gt, v_pred, max_fpr=max_fpr)
# # change scale from [0.5, 1.0] to [0.5 * max_fpr**2, max_fpr]
# # https://math.stackexchange.com/questions/914823/shift-numbers-into-a-different-range
# partial_auc = 0.5 * max_fpr**2 + (max_fpr - 0.5 * max_fpr**2) / (1.0 - 0.5) * (partial_auc_scaled - 0.5)
return partial_auc
class DotDict(Namespace):
"""A simple class that builds upon `argparse.Namespace`
in order to make chained attributes possible."""
def __init__(self, temp=False, key=None, parent=None, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._temp = temp
self._key = key
self._parent = parent
def __eq__(self, other):
if not isinstance(other, DotDict):
return NotImplemented
return vars(self) == vars(other)
def __getattr__(self, __name: str) -> Any:
if __name not in self.__dict__ and not self._temp:
self.__dict__[__name] = DotDict(temp=True, key=__name, parent=self)
else:
del self._parent.__dict__[self._key]
raise AttributeError("No attribute '%s'" % __name)
return self.__dict__[__name]
def __repr__(self) -> str:
item_keys = [k for k in self.__dict__ if not k.startswith("_")]
if len(item_keys) == 0:
return "DotDict()"
elif len(item_keys) == 1:
key = item_keys[0]
val = self.__dict__[key]
return "DotDict(%s=%s)" % (key, repr(val))
else:
return "DotDict(%s)" % ", ".join(
"%s=%s" % (key, repr(val)) for key, val in self.__dict__.items()
)
@classmethod
def from_dict(cls, original: typing.Mapping[str, any]) -> "DotDict":
"""Create a DotDict from a (possibly nested) dict `original`.
Warning: this method should not be used on very deeply nested inputs,
since it's recursively traversing the nested dictionary values.
"""
dd = DotDict()
for key, value in original.items():
if isinstance(value, typing.Mapping):
value = cls.from_dict(value)
setattr(dd, key, value)
return dd