Skip to content

Commit 0f0da47

Browse files
committed
pylint
1 parent 05087fd commit 0f0da47

File tree

6 files changed

+390
-49
lines changed

6 files changed

+390
-49
lines changed

docs/02_rocpicker.tex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ \subsection{Discrete points}
7676
\xdot(t)&=\sum_n \mathscr{x}_n \delta(t-t_n) \\
7777
\ydot(t)&=\sum_r \mathscr{y}_r \delta(t-t_r) \\
7878
\end{aligned}
79+
\label{eq:roc-from-delta-functions}
7980
\end{align}
8081
The indices \(n\) and \(r\) index the non-responders and responders, \(\mathscr{X}_n\) and \(\mathscr{Y}_r\) are the numbers of non-responders and responders with \(t=t_n\) or \(t=t_r\), and \(\mathscr{x}_n\) and \(\mathscr{y}_r\) are the fractions of non-responders and responders with \(t=t_n\) or \(t=t_r\) in the true probability distribution.
8182

roc_picker/datacard.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def systematics_mc(self, *, id_start=0, flip_sign=False):
155155
count = ScipyDistribution(
156156
nominal=p["value"],
157157
scipydistribution=scipy.stats.poisson(mu=p["value"]),
158-
id=next(id_generator)
158+
unique_id=next(id_generator)
159159
)
160160
patient_distributions.append({
161161
"response": p["response"],
@@ -167,12 +167,12 @@ def systematics_mc(self, *, id_start=0, flip_sign=False):
167167
numerator = ScipyDistribution(
168168
nominal=p["numerator"],
169169
scipydistribution=scipy.stats.poisson(mu=p["numerator"]),
170-
id=next(id_generator)
170+
unique_id=next(id_generator)
171171
)
172172
denominator = ScipyDistribution(
173173
nominal=p["denominator"],
174174
scipydistribution=scipy.stats.poisson(mu=p["denominator"]),
175-
id=next(id_generator)
175+
unique_id=next(id_generator)
176176
)
177177
ratio = numerator / denominator
178178
patient_distributions.append({
@@ -186,7 +186,7 @@ def systematics_mc(self, *, id_start=0, flip_sign=False):
186186
log_norm_factor = ScipyDistribution(
187187
nominal=0,
188188
scipydistribution=scipy.stats.norm(),
189-
id=next(id_generator)
189+
unique_id=next(id_generator)
190190
)
191191
try:
192192
for patient, value in zip(patient_distributions, systematic["values"], strict=True):

roc_picker/delta_functions.py

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,67 @@
1-
import functools, numpy as np, scipy.optimize, warnings
1+
"""
2+
Optimize the discrete ROC curve using the Lagrangian method
3+
applied to delta functions. This is a sanity check and should
4+
be equivalent to the discrete method. See docs/02_rocpicker.tex
5+
for the math details and docs/03_examples.md for usage examples.
6+
"""
7+
8+
import functools, warnings
9+
import numpy as np, scipy.optimize
210
from .discrete_base import DiscreteROCBase
311

412
class DeltaFunctions(DiscreteROCBase):
13+
"""
14+
Optimize the discrete ROC curve using the Lagrangian method
15+
applied to delta functions. This is a sanity check and should
16+
be equivalent to the discrete method. See docs/02_rocpicker.tex
17+
for the math details and docs/03_examples.md for usage examples.
18+
19+
Parameters
20+
----------
21+
responders: array-like
22+
The parameter values for the responders.
23+
nonresponders: array-like
24+
The parameter values for the nonresponders.
25+
flip_sign: bool, optional
26+
If True, the sign of the parameter is flipped.
27+
Default is False.
28+
"""
529
@functools.cached_property
630
def sign(self):
7-
if self.flip_sign: return -1
8-
return 1
31+
"""
32+
The sign to multiply the parameter values by.
33+
"""
34+
if self.flip_sign:
35+
return -1
36+
else:
37+
return 1
938
@functools.cached_property
1039
def ts(self):
40+
"""
41+
The parameter values for all patients.
42+
"""
1143
return sorted(set(self.responders) | set(self.nonresponders) | {np.inf, -np.inf})
1244

1345
def X(self, t):
46+
"""
47+
The X coordinate of the nominal ROC curve at the parameter value t,
48+
which represents the number of nonresponders at or below t.
49+
"""
1450
return sum(1 for ni in self.nonresponders if ni*self.sign < t*self.sign)
1551
def Y(self, t):
52+
"""
53+
The Y coordinate of the nominal ROC curve at the parameter value t,
54+
which represents the number of responders at or below t.
55+
"""
1656
return sum(1 for ri in self.responders if ri*self.sign < t*self.sign)
1757

1858
def xy(self, c1, c5, Lambda):
59+
"""
60+
The x and y functions for the fitted ROC curve,
61+
given the parameters c1, c5, and Lambda.
62+
(The optimization is to determine those parameters.)
63+
c3 and c4 are trivial to calculate from the boundary conditions.
64+
"""
1965
c3 = c4 = 0
2066
if self.flip_sign:
2167
c3 = c4 = 1
@@ -53,6 +99,9 @@ def y(t):
5399
return x, y
54100

55101
def findparams(self, *, AUC, c1_guess, c5_guess, Lambda_guess):
102+
"""
103+
Find the parameters c1, c5, and Lambda that satisfy the boundary conditions.
104+
"""
56105
def bc(params):
57106
if not self.flip_sign:
58107
target_at_inf = 1
@@ -75,10 +124,37 @@ def bc(params):
75124
guess = [c1_guess, c5_guess, Lambda_guess]
76125
return scipy.optimize.fsolve(bc, guess)
77126

78-
def optimize(self, *, AUC, c1_guess=1, c5_guess=1, Lambda_guess=1):
127+
def optimize(self, *, AUC=None, c1_guess=1, c5_guess=1, Lambda_guess=1):
128+
"""
129+
Optimize the ROC curve to match the given AUC.
130+
131+
Parameters
132+
----------
133+
AUC: float
134+
The target AUC.
135+
c1_guess: float, optional
136+
The initial guess for the parameter c1.
137+
Default is 1.
138+
c5_guess: float, optional
139+
The initial guess for the parameter c5.
140+
Default is 1.
141+
Lambda_guess: float, optional
142+
The initial guess for the parameter Lambda.
143+
Default is 1.
144+
"""
79145
with warnings.catch_warnings():
80-
warnings.filterwarnings("ignore", "The number of calls to function has reached maxfev = |The iteration is not making good progress")
81-
c1, c5, Lambda = self.findparams(AUC=AUC, c1_guess=c1_guess, c5_guess=c5_guess, Lambda_guess=Lambda_guess)
146+
warnings.filterwarnings(
147+
"ignore",
148+
"The number of calls to function has reached maxfev = "
149+
"|"
150+
"The iteration is not making good progress"
151+
)
152+
c1, c5, Lambda = self.findparams(
153+
AUC=AUC,
154+
c1_guess=c1_guess,
155+
c5_guess=c5_guess,
156+
Lambda_guess=Lambda_guess
157+
)
82158

83159
x, y = self.xy(c1, c5, Lambda)
84160

roc_picker/discrete.py

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,104 @@
1+
"""
2+
Optimize the ROC curve using the discrete method.
3+
See docs/02_rocpicker.tex for the math details
4+
and docs/03_examples.md for usage examples.
5+
"""
6+
17
import collections, functools, numpy as np, scipy.optimize
28
from .discrete_base import DiscreteROCBase
39

410
class DiscreteROC(DiscreteROCBase):
11+
"""
12+
Optimize the ROC curve using the discrete method.
13+
See docs/02_rocpicker.tex for the math details
14+
and docs/03_examples.md for usage examples.
15+
16+
Parameters
17+
----------
18+
responders: array-like
19+
The parameter values for the responders.
20+
nonresponders: array-like
21+
The parameter values for the nonresponders.
22+
flip_sign: bool, optional
23+
If True, the sign of the parameter is flipped.
24+
Default is False.
25+
check_validity: bool, optional
26+
If True, run sanity checks at each step.
27+
This should not be necessary for normal use.
28+
Default is False.
29+
"""
530
def __init__(self, *args, check_validity=False, **kwargs):
631
self.__check_validity = check_validity
732
super().__init__(*args, **kwargs)
833

934
@functools.cached_property
1035
def ts(self):
36+
"""
37+
The parameter values for all patients.
38+
"""
1139
return np.array(sorted(set(self.responders) | set(self.nonresponders)))
1240
@functools.cached_property
1341
def Xscr(self):
42+
"""
43+
The number of nonresponders at each parameter value.
44+
"""
1445
counter = collections.Counter(self.nonresponders)
1546
return np.array([counter[t] for t in self.ts])
1647
@functools.cached_property
1748
def Yscr(self):
49+
"""
50+
The number of responders at each parameter value.
51+
"""
1852
counter = collections.Counter(self.responders)
1953
return np.array([counter[t] for t in self.ts])
2054

2155
@functools.cached_property
2256
def nonzeroXindices(self):
57+
"""
58+
The indices of parameter values for non-responders.
59+
"""
2360
return self.Xscr != 0
2461
@functools.cached_property
2562
def nonzeroYindices(self):
63+
"""
64+
The indices of parameter values for responders.
65+
"""
2666
return self.Yscr != 0
2767
@functools.cached_property
2868
def numnonzeroXindices(self):
69+
"""
70+
The number of distinct parameter values for non-responders.
71+
"""
2972
return np.count_nonzero(self.nonzeroXindices)
3073

3174
@functools.cached_property
3275
def nonzeroXscr(self):
76+
"""
77+
The number of nonresponders at each nonzero parameter value.
78+
"""
3379
return self.Xscr[self.nonzeroXindices]
3480
@functools.cached_property
3581
def nonzeroYscr(self):
82+
"""
83+
The number of responders at each nonzero parameter value.
84+
"""
3685
return self.Yscr[self.nonzeroYindices]
3786

3887
def checkvalidity(self, xscr, yscr):
39-
if not self.__check_validity: return
88+
"""
89+
Check whether a candidate ROC curve is valid.
90+
"""
91+
if not self.__check_validity:
92+
return
4093
np.testing.assert_array_equal(xscr[self.Xscr==0], 0)
4194
np.testing.assert_array_equal(yscr[self.Yscr==0], 0)
4295
np.testing.assert_allclose([xscr.sum(), yscr.sum()], 1)
4396

4497
def evalNLL(self, xscr, yscr):
98+
"""
99+
Evaluate the negative log-likelihood of a candidate ROC curve (xscr, yscr)
100+
given the data (self.Xscr, self.Yscr).
101+
"""
45102
self.checkvalidity(xscr, yscr)
46103

47104
nonzeroxscr = xscr[self.nonzeroXindices]
@@ -56,6 +113,10 @@ def evalNLL(self, xscr, yscr):
56113
return NLL
57114

58115
def buildroc(self, xscr, yscr):
116+
"""
117+
Build the ROC curve (x, y) from the delta function coordinates
118+
(xscr, yscr). (See eq.~\ref{eq:roc-from-delta-functions} in docs/02_rocpicker.tex.)
119+
"""
59120
self.checkvalidity(xscr, yscr)
60121
x = np.zeros(shape=len(self.ts)+1)
61122
y = np.zeros(shape=len(self.ts)+1)
@@ -73,10 +134,13 @@ def buildroc(self, xscr, yscr):
73134
return x, y
74135

75136
def evalAUC(self, xscr, yscr):
137+
"""
138+
Evaluate the area under the ROC curve constructed from (xscr, yscr).
139+
"""
76140
xx, yy = self.buildroc(xscr, yscr)
77141
return np.sum(0.5 * (xx[1:] - xx[:-1]) * (yy[1:] + yy[:-1]))
78142

79-
def optimize(self, AUC=None):
143+
def optimize(self, *, AUC=None):
80144
def unpackxy(xy):
81145
length = len(self.ts)
82146
xscr = np.zeros(length)
@@ -96,12 +160,12 @@ def NLL(xy):
96160

97161
guess = np.concatenate([self.Xscr[self.nonzeroXindices], self.Yscr[self.nonzeroYindices]])
98162

99-
def sumxscr(xy):
100-
xscr, yscr = unpackxy(xy)
101-
return sum(xscr) - 1
102-
def sumyscr(xy):
103-
xscr, yscr = unpackxy(xy)
104-
return sum(yscr) - 1
163+
#def sumxscr(xy):
164+
# xscr, yscr = unpackxy(xy)
165+
# return sum(xscr) - 1
166+
#def sumyscr(xy):
167+
# xscr, yscr = unpackxy(xy)
168+
# return sum(yscr) - 1
105169
constraints = [
106170
#{
107171
# "type": "eq",

roc_picker/discrete_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, responders, nonresponders, *, flip_sign=False):
2727
self.flip_sign = flip_sign
2828

2929
@abc.abstractmethod
30-
def optimize(self, AUC=None):
30+
def optimize(self, *, AUC=None):
3131
"""
3232
Optimize the ROC curve, either unconditionally or for a given AUC.
3333
"""

0 commit comments

Comments
 (0)