SurvivalEVAL 0.2.2__py3-none-any.whl
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.
- SurvivalEVAL/Evaluator.py +1087 -0
- SurvivalEVAL/__init__.py +25 -0
- SurvivalEVAL/version.py +4 -0
- SurvivalEVAL-0.2.2.dist-info/LICENSE +21 -0
- SurvivalEVAL-0.2.2.dist-info/METADATA +237 -0
- SurvivalEVAL-0.2.2.dist-info/RECORD +8 -0
- SurvivalEVAL-0.2.2.dist-info/WHEEL +5 -0
- SurvivalEVAL-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Union, Optional, Callable
|
|
5
|
+
from scipy.integrate import trapezoid
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
|
|
10
|
+
from SurvivalEVAL.Evaluations.custom_types import NumericArrayLike
|
|
11
|
+
from SurvivalEVAL.Evaluations.util import check_and_convert
|
|
12
|
+
from SurvivalEVAL.Evaluations.util import predict_mean_survival_time, predict_median_survival_time
|
|
13
|
+
from SurvivalEVAL.Evaluations.util import predict_prob_from_curve, predict_multi_probs_from_curve, quantile_to_survival
|
|
14
|
+
|
|
15
|
+
from SurvivalEVAL.Evaluations.Concordance import concordance
|
|
16
|
+
from SurvivalEVAL.Evaluations.AreaUnderCurve import auc
|
|
17
|
+
from SurvivalEVAL.Evaluations.BrierScore import single_brier_score, brier_multiple_points
|
|
18
|
+
from SurvivalEVAL.Evaluations.MeanError import mean_error
|
|
19
|
+
from SurvivalEVAL.Evaluations.OneCalibration import one_calibration
|
|
20
|
+
from SurvivalEVAL.Evaluations.D_Calibration import d_calibration
|
|
21
|
+
from SurvivalEVAL.Evaluations.KM_Calibration import km_calibration
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SurvivalEvaluator:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
predicted_survival_curves: NumericArrayLike,
|
|
28
|
+
time_coordinates: NumericArrayLike,
|
|
29
|
+
test_event_times: NumericArrayLike,
|
|
30
|
+
test_event_indicators: NumericArrayLike,
|
|
31
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
32
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
33
|
+
predict_time_method: str = "Median",
|
|
34
|
+
interpolation: str = "Linear"
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the Evaluator
|
|
38
|
+
param predicted_survival_curves: structured array, shape = (n_samples, n_time_points)
|
|
39
|
+
Predicted survival curves for the testing samples.
|
|
40
|
+
param time_coordinates: structured array, shape = (n_time_points, )
|
|
41
|
+
Time coordinates for the given curves.
|
|
42
|
+
param test_event_times: structured array, shape = (n_samples, )
|
|
43
|
+
Actual event/censor time for the testing samples.
|
|
44
|
+
param test_event_indicators: structured array, shape = (n_samples, )
|
|
45
|
+
Binary indicators of censoring for the testing samples
|
|
46
|
+
param train_event_times: structured array, shape = (n_train_samples, )
|
|
47
|
+
Actual event/censor time for the training samples.
|
|
48
|
+
param train_event_indicators: structured array, shape = (n_train_samples, )
|
|
49
|
+
Binary indicators of censoring for the training samples
|
|
50
|
+
param predict_time_method: str, default = "Median"
|
|
51
|
+
Method for calculating predicted survival time. Available options are "Median" and "Mean".
|
|
52
|
+
param interpolation: str, default = "Linear"
|
|
53
|
+
Method for interpolation. Available options are ['Linear', 'Pchip'].
|
|
54
|
+
"""
|
|
55
|
+
self._predicted_curves = check_and_convert(predicted_survival_curves)
|
|
56
|
+
self._time_coordinates = check_and_convert(time_coordinates)
|
|
57
|
+
|
|
58
|
+
test_event_times, test_event_indicators = check_and_convert(test_event_times, test_event_indicators)
|
|
59
|
+
self.event_times = test_event_times
|
|
60
|
+
self.event_indicators = test_event_indicators
|
|
61
|
+
|
|
62
|
+
if (train_event_times is not None) and (train_event_indicators is not None):
|
|
63
|
+
train_event_times, train_event_indicators = check_and_convert(train_event_times, train_event_indicators)
|
|
64
|
+
self.train_event_times = train_event_times
|
|
65
|
+
self.train_event_indicators = train_event_indicators
|
|
66
|
+
|
|
67
|
+
if predict_time_method == "Median":
|
|
68
|
+
self.predict_time_method = predict_median_survival_time
|
|
69
|
+
elif predict_time_method == "Mean":
|
|
70
|
+
self.predict_time_method = predict_mean_survival_time
|
|
71
|
+
else:
|
|
72
|
+
error = "Please enter one of 'Median' or 'Mean' for calculating predicted survival time."
|
|
73
|
+
raise TypeError(error)
|
|
74
|
+
|
|
75
|
+
self.interpolation = interpolation
|
|
76
|
+
|
|
77
|
+
def _error_trainset(self, method_name: str):
|
|
78
|
+
if (self.train_event_times is None) or (self.train_event_indicators is None):
|
|
79
|
+
raise TypeError("Train set information is missing. "
|
|
80
|
+
"Evaluator cannot perform {} evaluation.".format(method_name))
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def predicted_curves(self):
|
|
84
|
+
return self._predicted_curves
|
|
85
|
+
|
|
86
|
+
@predicted_curves.setter
|
|
87
|
+
def predicted_curves(self, val: NumericArrayLike):
|
|
88
|
+
print("Setter called. Resetting predicted curves for this evaluator.")
|
|
89
|
+
self._predicted_curves = check_and_convert(val)
|
|
90
|
+
self._clear_cache()
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def time_coordinates(self):
|
|
94
|
+
return self._time_coordinates
|
|
95
|
+
|
|
96
|
+
@time_coordinates.setter
|
|
97
|
+
def time_coordinates(self, val: NumericArrayLike):
|
|
98
|
+
print("Setter called. Resetting time coordinates for this evaluator.")
|
|
99
|
+
self._time_coordinates = check_and_convert(val)
|
|
100
|
+
self._clear_cache()
|
|
101
|
+
|
|
102
|
+
@cached_property
|
|
103
|
+
def predicted_event_times(self):
|
|
104
|
+
return self.predict_time_from_curve(self.predict_time_method)
|
|
105
|
+
|
|
106
|
+
def _clear_cache(self):
|
|
107
|
+
# See how to clear cache in functools:
|
|
108
|
+
# https://docs.python.org/3/library/functools.html#functools.cached_property
|
|
109
|
+
# https://stackoverflow.com/questions/62662564/how-do-i-clear-the-cache-from-cached-property-decorator
|
|
110
|
+
self.__dict__.pop('predicted_event_times', None)
|
|
111
|
+
|
|
112
|
+
def predict_time_from_curve(
|
|
113
|
+
self,
|
|
114
|
+
predict_method: Callable,
|
|
115
|
+
) -> np.ndarray:
|
|
116
|
+
"""
|
|
117
|
+
Predict survival time from survival curves.
|
|
118
|
+
param predict_method: Callable
|
|
119
|
+
A function that takes in a survival curve and returns a predicted survival time.
|
|
120
|
+
There are two build-in methods: 'predict_median_survival_time' and 'predict_mean_survival_time'.
|
|
121
|
+
'predict_median_survival_time' uses the median of the survival curve as the predicted survival time.
|
|
122
|
+
'predict_mean_survival_time' uses the expected time of the survival curve as the predicted survival time.
|
|
123
|
+
:return: np.ndarray
|
|
124
|
+
Predicted survival time for each sample.
|
|
125
|
+
"""
|
|
126
|
+
if (predict_method is not predict_mean_survival_time) and (predict_method is not predict_median_survival_time):
|
|
127
|
+
error = "Prediction method must be 'predict_mean_survival_time' or 'predict_median_survival_time', " \
|
|
128
|
+
"got '{}' instead".format(predict_method.__name__)
|
|
129
|
+
raise TypeError(error)
|
|
130
|
+
|
|
131
|
+
predicted_times = []
|
|
132
|
+
for i in range(self.predicted_curves.shape[0]):
|
|
133
|
+
predicted_time = predict_method(self.predicted_curves[i, :], self.time_coordinates, self.interpolation)
|
|
134
|
+
predicted_times.append(predicted_time)
|
|
135
|
+
predicted_times = np.array(predicted_times)
|
|
136
|
+
return predicted_times
|
|
137
|
+
|
|
138
|
+
def predict_probability_from_curve(
|
|
139
|
+
self,
|
|
140
|
+
target_time: Union[float, int, np.ndarray],
|
|
141
|
+
) -> np.ndarray:
|
|
142
|
+
"""
|
|
143
|
+
Predict a probability of event at a given time point from a predicted curve. Each predicted curve will only
|
|
144
|
+
have one corresponding probability. Note that this method is different from the
|
|
145
|
+
'predict_multi_probabilities_from_curve' method, which predicts the multiple probabilities at multiple time
|
|
146
|
+
points from a predicted curve.
|
|
147
|
+
param target_time: float, int, or array-like, shape = (n_samples, )
|
|
148
|
+
Time point(s) at which the probability of event is to be predicted. If float or int, the same time point is
|
|
149
|
+
used for all samples. If array-like, each sample will have it own target time. The length of the array must
|
|
150
|
+
be the same as the number of samples.
|
|
151
|
+
:return: array-like, shape = (n_samples, )
|
|
152
|
+
Predicted probabilities of event at the target time point(s).
|
|
153
|
+
"""
|
|
154
|
+
if isinstance(target_time, (float, int)):
|
|
155
|
+
target_time = target_time * np.ones_like(self.event_times)
|
|
156
|
+
elif isinstance(target_time, np.ndarray):
|
|
157
|
+
assert target_time.ndim == 1, "Target time must be a 1D array"
|
|
158
|
+
assert target_time.shape[0] == self.predicted_curves.shape[0], "Target time must have the same length as " \
|
|
159
|
+
"the number of samples"
|
|
160
|
+
else:
|
|
161
|
+
error = "Target time must be a float, int, or 1D array, got '{}' instead".format(type(target_time))
|
|
162
|
+
raise TypeError(error)
|
|
163
|
+
|
|
164
|
+
predict_probs = []
|
|
165
|
+
for i in range(self.predicted_curves.shape[0]):
|
|
166
|
+
predict_prob = predict_prob_from_curve(self.predicted_curves[i, :], self.time_coordinates,
|
|
167
|
+
target_time[i], self.interpolation)
|
|
168
|
+
predict_probs.append(predict_prob)
|
|
169
|
+
predict_probs = np.array(predict_probs)
|
|
170
|
+
return predict_probs
|
|
171
|
+
|
|
172
|
+
def predict_multi_probabilities_from_curve(
|
|
173
|
+
self,
|
|
174
|
+
target_times: np.ndarray
|
|
175
|
+
) -> np.ndarray:
|
|
176
|
+
"""
|
|
177
|
+
Predict the probability of event at multiple time points from the predicted curve.
|
|
178
|
+
param target_times: array-like, shape = (n_target_times)
|
|
179
|
+
Time points at which the probability of event is to be predicted.
|
|
180
|
+
:return: array-like, shape = (n_samples, n_target_times)
|
|
181
|
+
Predicted probabilities of event at the target time points.
|
|
182
|
+
"""
|
|
183
|
+
predict_probs_mat = []
|
|
184
|
+
for i in range(self.predicted_curves.shape[0]):
|
|
185
|
+
predict_probs = predict_multi_probs_from_curve(self.predicted_curves[i, :], self.time_coordinates,
|
|
186
|
+
target_times, self.interpolation).tolist()
|
|
187
|
+
predict_probs_mat.append(predict_probs)
|
|
188
|
+
predict_probs_mat = np.array(predict_probs_mat)
|
|
189
|
+
return predict_probs_mat
|
|
190
|
+
|
|
191
|
+
def plot_survival_curves(
|
|
192
|
+
self,
|
|
193
|
+
curve_indices,
|
|
194
|
+
color=None,
|
|
195
|
+
x_lim: tuple = None,
|
|
196
|
+
y_lim: tuple = None,
|
|
197
|
+
x_label: str = 'Time',
|
|
198
|
+
y_label: str = 'Survival probability'
|
|
199
|
+
):
|
|
200
|
+
"""Plot survival curves."""
|
|
201
|
+
fig, ax = plt.subplots()
|
|
202
|
+
ax.plot(self.time_coordinates, self.predicted_curves[curve_indices, :].T, color=color, label=curve_indices)
|
|
203
|
+
if y_lim is None:
|
|
204
|
+
ax.set_ylim(0, 1.02)
|
|
205
|
+
else:
|
|
206
|
+
ax.set_ylim(y_lim)
|
|
207
|
+
|
|
208
|
+
if x_lim is not None:
|
|
209
|
+
ax.set_xlim(x_lim)
|
|
210
|
+
ax.set_xlabel(x_label)
|
|
211
|
+
ax.set_ylabel(y_label)
|
|
212
|
+
ax.legend()
|
|
213
|
+
return fig, ax
|
|
214
|
+
|
|
215
|
+
def concordance(
|
|
216
|
+
self,
|
|
217
|
+
ties: str = "None",
|
|
218
|
+
pair_method: str = "Comparable"
|
|
219
|
+
) -> (float, float, int):
|
|
220
|
+
"""
|
|
221
|
+
Calculate the concordance index between the predicted survival times and the true survival times.
|
|
222
|
+
param ties: str, default = "None"
|
|
223
|
+
A string indicating the way ties should be handled.
|
|
224
|
+
Options: "None" (default), "Time", "Risk", or "All"
|
|
225
|
+
"None" will throw out all ties in true survival time and all ties in predict survival times (risk scores).
|
|
226
|
+
"Time" includes ties in true survival time but removes ties in predict survival times (risk scores).
|
|
227
|
+
"Risk" includes ties in predict survival times (risk scores) but not in true survival time.
|
|
228
|
+
"All" includes all ties.
|
|
229
|
+
Note the concordance calculation is given by
|
|
230
|
+
(Concordant Pairs + (Number of Ties/2))/(Concordant Pairs + Discordant Pairs + Number of Ties).
|
|
231
|
+
param pair_method: str, default = "Comparable"
|
|
232
|
+
A string indicating the method for constructing the pairs of samples.
|
|
233
|
+
Options: "Comparable" (default) or "Margin"
|
|
234
|
+
"Comparable": the pairs are constructed by comparing the predicted survival time of each sample with the
|
|
235
|
+
event time of all other samples. The pairs are only constructed between samples with comparable
|
|
236
|
+
event times. For example, if sample i has a censor time of 10, then the pairs are constructed by
|
|
237
|
+
comparing the predicted survival time of sample i with the event time of all samples with event
|
|
238
|
+
time of 10 or less.
|
|
239
|
+
"Margin": the pairs are constructed between all samples. A best-guess time for the censored samples
|
|
240
|
+
will be calculated and used to construct the pairs.
|
|
241
|
+
:return: (float, float, int)
|
|
242
|
+
The concordance index, the number of concordant pairs, and the number of total pairs.
|
|
243
|
+
"""
|
|
244
|
+
# Choose prediction method based on the input argument
|
|
245
|
+
if pair_method == "Margin" and (self.train_event_times is None or self.train_event_indicators is None):
|
|
246
|
+
self._error_trainset("margin concordance")
|
|
247
|
+
|
|
248
|
+
return concordance(self.predicted_event_times, self.event_times, self.event_indicators, self.train_event_times,
|
|
249
|
+
self.train_event_indicators, pair_method, ties)
|
|
250
|
+
|
|
251
|
+
def auc(
|
|
252
|
+
self,
|
|
253
|
+
target_time: Optional[Union[int, float]] = None
|
|
254
|
+
) -> float:
|
|
255
|
+
"""
|
|
256
|
+
Calculate the area under the ROC curve (AUC) score at a given time point from the predicted survival curve.
|
|
257
|
+
param target_time: float, int, or None, default = None
|
|
258
|
+
Time point at which the AUC score is to be calculated. If None, the AUC score is calculated at the
|
|
259
|
+
median time of all the event/censor times from the training and test sets.
|
|
260
|
+
:return: float
|
|
261
|
+
The Brier score at the target time point.
|
|
262
|
+
"""
|
|
263
|
+
event_times = np.concatenate((self.event_times, self.train_event_times)) \
|
|
264
|
+
if self.train_event_times is not None else self.event_times
|
|
265
|
+
|
|
266
|
+
if target_time is None:
|
|
267
|
+
target_time = np.quantile(event_times, 0.5)
|
|
268
|
+
|
|
269
|
+
predict_probs = self.predict_probability_from_curve(target_time)
|
|
270
|
+
|
|
271
|
+
return auc(predict_probs, self.event_times, self.event_indicators, target_time)
|
|
272
|
+
|
|
273
|
+
def brier_score(
|
|
274
|
+
self,
|
|
275
|
+
target_time: Optional[Union[int, float]] = None,
|
|
276
|
+
IPCW_weighted: bool = True
|
|
277
|
+
) -> float:
|
|
278
|
+
"""
|
|
279
|
+
Calculate the Brier score at a given time point from the predicted survival curve.
|
|
280
|
+
param target_time: float, int, or None, default = None
|
|
281
|
+
Time point at which the Brier score is to be calculated. If None, the Brier score is calculated at the
|
|
282
|
+
median time of all the event/censor times from the training and test sets.
|
|
283
|
+
param IPCW_weighted: bool, default = True
|
|
284
|
+
Whether to use IPCW weighting for the Brier score.
|
|
285
|
+
:return: float
|
|
286
|
+
The Brier score at the target time point.
|
|
287
|
+
"""
|
|
288
|
+
if IPCW_weighted:
|
|
289
|
+
self._error_trainset("IPCW-weighted Brier score (BS)")
|
|
290
|
+
|
|
291
|
+
if target_time is None:
|
|
292
|
+
target_time = np.quantile(np.concatenate((self.event_times, self.train_event_times)), 0.5)
|
|
293
|
+
|
|
294
|
+
predict_probs = self.predict_probability_from_curve(target_time)
|
|
295
|
+
|
|
296
|
+
return single_brier_score(predict_probs, self.event_times, self.event_indicators, self.train_event_times,
|
|
297
|
+
self.train_event_indicators, target_time, IPCW_weighted)
|
|
298
|
+
|
|
299
|
+
def brier_score_multiple_points(
|
|
300
|
+
self,
|
|
301
|
+
target_times: np.ndarray,
|
|
302
|
+
IPCW_weighted: bool = True
|
|
303
|
+
) -> np.ndarray:
|
|
304
|
+
"""
|
|
305
|
+
Calculate multiple Brier scores at multiple specific times.
|
|
306
|
+
param target_times: float, default: None
|
|
307
|
+
The specific time points for which to estimate the Brier scores.
|
|
308
|
+
param IPCW_weighted: bool, default = True
|
|
309
|
+
Whether to use IPCW weighting for the Brier score.
|
|
310
|
+
:return:
|
|
311
|
+
Values of multiple Brier scores.
|
|
312
|
+
"""
|
|
313
|
+
if IPCW_weighted:
|
|
314
|
+
self._error_trainset("IPCW-weighted Brier score (BS)")
|
|
315
|
+
|
|
316
|
+
predict_probs_mat = self.predict_multi_probabilities_from_curve(target_times)
|
|
317
|
+
|
|
318
|
+
return brier_multiple_points(predict_probs_mat, self.event_times, self.event_indicators, self.train_event_times,
|
|
319
|
+
self.train_event_indicators, target_times, IPCW_weighted)
|
|
320
|
+
|
|
321
|
+
def integrated_brier_score(
|
|
322
|
+
self,
|
|
323
|
+
num_points: int = None,
|
|
324
|
+
IPCW_weighted: bool = True,
|
|
325
|
+
draw_figure: bool = False
|
|
326
|
+
) -> float:
|
|
327
|
+
"""
|
|
328
|
+
Calculate the integrated Brier score (IBS) from the predicted survival curve.
|
|
329
|
+
param num_points: int, default = None
|
|
330
|
+
Number of points at which the Brier score is to be calculated. If None, the number of points is set to
|
|
331
|
+
the number of event/censor times from the training and test sets.
|
|
332
|
+
param IPCW_weighted: bool, default = True
|
|
333
|
+
Whether to use IPCW weighting for the Brier score.
|
|
334
|
+
param draw_figure: bool, default = False
|
|
335
|
+
Whether to draw the figure of the IBS.
|
|
336
|
+
:return: float
|
|
337
|
+
The integrated Brier score.
|
|
338
|
+
"""
|
|
339
|
+
if IPCW_weighted:
|
|
340
|
+
self._error_trainset("IPCW-weighted Integrated Brier Score (IBS)")
|
|
341
|
+
|
|
342
|
+
max_target_time = np.max(np.concatenate((self.event_times, self.train_event_times))) if self.train_event_times \
|
|
343
|
+
is not None else np.max(self.event_times)
|
|
344
|
+
|
|
345
|
+
# If number of target time is not indicated, then we use the censored times obtained from test set
|
|
346
|
+
if num_points is None:
|
|
347
|
+
censored_times = self.event_times[self.event_indicators == 0]
|
|
348
|
+
time_points = np.unique(censored_times)
|
|
349
|
+
if time_points.size == 0:
|
|
350
|
+
raise ValueError("You don't have censor data in the testset, "
|
|
351
|
+
"please provide \"num_points\" for calculating IBS")
|
|
352
|
+
else:
|
|
353
|
+
time_range = np.max(time_points) - np.min(time_points)
|
|
354
|
+
else:
|
|
355
|
+
time_points = np.linspace(0, max_target_time, num_points)
|
|
356
|
+
time_range = max_target_time
|
|
357
|
+
|
|
358
|
+
# Get single brier score from multiple target times, and use trapezoidal integral to calculate ISB.
|
|
359
|
+
#########################
|
|
360
|
+
# Solution 1, implemented using metrics multiplication, this is geometrically faster than solution 2
|
|
361
|
+
b_scores = self.brier_score_multiple_points(time_points, IPCW_weighted)
|
|
362
|
+
if np.isnan(b_scores).any():
|
|
363
|
+
warnings.warn("Time-dependent Brier Score contains nan")
|
|
364
|
+
bs_dict = {}
|
|
365
|
+
for time_point, b_score in zip(time_points, b_scores):
|
|
366
|
+
bs_dict[time_point] = b_score
|
|
367
|
+
print("Brier scores for multiple time points are".format(bs_dict))
|
|
368
|
+
integral_value = trapezoid(b_scores, time_points)
|
|
369
|
+
ibs_score = integral_value / time_range
|
|
370
|
+
##########################
|
|
371
|
+
# (Deprecated)
|
|
372
|
+
# Solution 2, implemented by iteratively calling self.brier_score(),
|
|
373
|
+
# this solution is much slower than solution 1
|
|
374
|
+
# b_scores = []
|
|
375
|
+
# for i in range(len(time_points)):
|
|
376
|
+
# b_score = self.brier_score(time_points[i])
|
|
377
|
+
# b_scores.append(b_score)
|
|
378
|
+
# b_scores = np.array(b_scores)
|
|
379
|
+
# integral_value = trapezoid(b_scores, time_points)
|
|
380
|
+
# ibs_score = integral_value / time_range
|
|
381
|
+
|
|
382
|
+
# Draw the Brier score graph
|
|
383
|
+
if draw_figure:
|
|
384
|
+
plt.plot(time_points, b_scores, 'bo-')
|
|
385
|
+
score_text = r'IBS$= {:.3f}$'.format(ibs_score)
|
|
386
|
+
plt.plot([], [], ' ', label=score_text)
|
|
387
|
+
plt.legend()
|
|
388
|
+
# plt.text(500, 0.05, r'IBS$= {:.3f}$'.format(ibs_score), verticalalignment='top',
|
|
389
|
+
# horizontalalignment='left', fontsize=12, color='Black')
|
|
390
|
+
plt.xlabel('Time')
|
|
391
|
+
plt.ylabel('Brier Score')
|
|
392
|
+
plt.show()
|
|
393
|
+
return ibs_score
|
|
394
|
+
|
|
395
|
+
def mae(
|
|
396
|
+
self,
|
|
397
|
+
method: str = "Hinge",
|
|
398
|
+
weighted: bool = False,
|
|
399
|
+
log_scale: bool = False,
|
|
400
|
+
verbose: bool = False
|
|
401
|
+
) -> float:
|
|
402
|
+
"""
|
|
403
|
+
Calculate the MAE score for the test set.
|
|
404
|
+
param method: string, default: "Hinge"
|
|
405
|
+
The method used to calculate the MAE score.
|
|
406
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
407
|
+
param weighted: bool, default: True
|
|
408
|
+
Whether to use weighting scheme for MAE.
|
|
409
|
+
param log_scale: boolean, default: False
|
|
410
|
+
Whether to use log scale for the time axis.
|
|
411
|
+
param verbose: boolean, default: False
|
|
412
|
+
Whether to show the progress bar.
|
|
413
|
+
:return: float
|
|
414
|
+
The MAE score for the test set.
|
|
415
|
+
"""
|
|
416
|
+
return mean_error(
|
|
417
|
+
predicted_times=self.predicted_event_times,
|
|
418
|
+
event_times=self.event_times,
|
|
419
|
+
event_indicators=self.event_indicators,
|
|
420
|
+
train_event_times=self.train_event_times,
|
|
421
|
+
train_event_indicators=self.train_event_indicators,
|
|
422
|
+
error_type="absolute",
|
|
423
|
+
method=method,
|
|
424
|
+
weighted=weighted,
|
|
425
|
+
log_scale=log_scale,
|
|
426
|
+
verbose=verbose
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def mse(
|
|
430
|
+
self,
|
|
431
|
+
method: str = "Hinge",
|
|
432
|
+
weighted: bool = True,
|
|
433
|
+
log_scale: bool = False,
|
|
434
|
+
verbose: bool = False
|
|
435
|
+
) -> float:
|
|
436
|
+
"""
|
|
437
|
+
Calculate the MAE score for the test set.
|
|
438
|
+
param method: string, default: "Hinge"
|
|
439
|
+
The method used to calculate the MAE score.
|
|
440
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
441
|
+
param weighted: bool, default: True
|
|
442
|
+
Whether to use weighting scheme for MAE.
|
|
443
|
+
param log_scale: boolean, default: False
|
|
444
|
+
Whether to use log scale for the time axis.
|
|
445
|
+
param verbose: boolean, default: False
|
|
446
|
+
Whether to show the progress bar.
|
|
447
|
+
:return: float
|
|
448
|
+
The MAE score for the test set.
|
|
449
|
+
"""
|
|
450
|
+
return mean_error(
|
|
451
|
+
predicted_times=self.predicted_event_times,
|
|
452
|
+
event_times=self.event_times,
|
|
453
|
+
event_indicators=self.event_indicators,
|
|
454
|
+
train_event_times=self.train_event_times,
|
|
455
|
+
train_event_indicators=self.train_event_indicators,
|
|
456
|
+
error_type="squared",
|
|
457
|
+
method=method,
|
|
458
|
+
weighted=weighted,
|
|
459
|
+
log_scale=log_scale,
|
|
460
|
+
verbose=verbose
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def rmse(
|
|
464
|
+
self,
|
|
465
|
+
method: str = "Hinge",
|
|
466
|
+
weighted: bool = True,
|
|
467
|
+
log_scale: bool = False,
|
|
468
|
+
verbose: bool = False
|
|
469
|
+
) -> float:
|
|
470
|
+
"""
|
|
471
|
+
Calculate the root mean squared error (RMSE) score for the test set.
|
|
472
|
+
param method: string, default: "Hinge"
|
|
473
|
+
The method used to calculate the MAE score.
|
|
474
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
475
|
+
param weighted: bool, default: True
|
|
476
|
+
Whether to use weighting scheme for MAE.
|
|
477
|
+
param log_scale: boolean, default: False
|
|
478
|
+
Whether to use log scale for the time axis.
|
|
479
|
+
param verbose: boolean, default: False
|
|
480
|
+
Whether to show the progress bar.
|
|
481
|
+
:return: float
|
|
482
|
+
The MAE score for the test set.
|
|
483
|
+
"""
|
|
484
|
+
return self.mse(method, weighted, log_scale, verbose) ** 0.5
|
|
485
|
+
|
|
486
|
+
def one_calibration(
|
|
487
|
+
self,
|
|
488
|
+
target_time: Union[float, int],
|
|
489
|
+
num_bins: int = 10,
|
|
490
|
+
method: str = "DN"
|
|
491
|
+
) -> (float, list, list):
|
|
492
|
+
"""
|
|
493
|
+
Calculate the one calibration score at a given time point from the predicted survival curve.
|
|
494
|
+
param target_time: float, int
|
|
495
|
+
Time point at which the one calibration score is to be calculated.
|
|
496
|
+
param num_bins: int, default: 10
|
|
497
|
+
Number of bins used to calculate the one calibration score.
|
|
498
|
+
param method: string, default: "DN"
|
|
499
|
+
The method used to calculate the one calibration score.
|
|
500
|
+
Options: "Uncensored", or "DN" (default)
|
|
501
|
+
:return: float, list, list
|
|
502
|
+
(p-value, observed probabilities, expected probabilities)
|
|
503
|
+
"""
|
|
504
|
+
predict_probs = self.predict_probability_from_curve(target_time)
|
|
505
|
+
return one_calibration(predict_probs, self.event_times, self.event_indicators, target_time, num_bins, method)
|
|
506
|
+
|
|
507
|
+
def d_calibration(
|
|
508
|
+
self,
|
|
509
|
+
num_bins: int = 10
|
|
510
|
+
) -> (float, np.ndarray):
|
|
511
|
+
"""
|
|
512
|
+
Calculate the D calibration score from the predicted survival curve.
|
|
513
|
+
param num_bins: int, default: 10
|
|
514
|
+
Number of bins used to calculate the D calibration score.
|
|
515
|
+
:return: float, np.ndarray
|
|
516
|
+
(p-value, counts in bins)
|
|
517
|
+
"""
|
|
518
|
+
predict_probs = self.predict_probability_from_curve(self.event_times)
|
|
519
|
+
return d_calibration(predict_probs, self.event_indicators, num_bins)
|
|
520
|
+
|
|
521
|
+
def x_calibration(
|
|
522
|
+
self,
|
|
523
|
+
num_bins: int = 10
|
|
524
|
+
) -> float:
|
|
525
|
+
"""
|
|
526
|
+
Calculate the X calibration score from the predicted survival curve.
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
num_bins
|
|
530
|
+
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
|
|
534
|
+
"""
|
|
535
|
+
_, bin_hist = self.d_calibration(num_bins)
|
|
536
|
+
n_bins = bin_hist.shape[0]
|
|
537
|
+
# normalize the histogram
|
|
538
|
+
d_cal_pdf = bin_hist / bin_hist.sum()
|
|
539
|
+
# compute the x-calibration score
|
|
540
|
+
optimal = np.ones_like(d_cal_pdf) / n_bins
|
|
541
|
+
x_cal = np.sum(np.square(d_cal_pdf - optimal))
|
|
542
|
+
return x_cal
|
|
543
|
+
|
|
544
|
+
def km_calibration(self):
|
|
545
|
+
"""
|
|
546
|
+
Calculate the KM calibration score from the predicted survival curve.
|
|
547
|
+
:return: float
|
|
548
|
+
KL divergence between the average predicted survival distribution and the Kaplan-Meier distribution.
|
|
549
|
+
"""
|
|
550
|
+
average_survival_curve = np.mean(self._predicted_curves, axis=0)
|
|
551
|
+
return km_calibration(average_survival_curve, self.time_coordinates, self.event_times, self.event_indicators)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class PycoxEvaluator(SurvivalEvaluator, ABC):
|
|
555
|
+
def __init__(
|
|
556
|
+
self,
|
|
557
|
+
surv: pd.DataFrame,
|
|
558
|
+
test_event_times: NumericArrayLike,
|
|
559
|
+
test_event_indicators: NumericArrayLike,
|
|
560
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
561
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
562
|
+
predict_time_method: str = "Median",
|
|
563
|
+
interpolation: str = "Linear"
|
|
564
|
+
):
|
|
565
|
+
"""
|
|
566
|
+
Evaluator for survival models in PyCox packages.
|
|
567
|
+
param surv: pd.DataFrame, shape = (n_time_points, n_samples)
|
|
568
|
+
Predicted survival curves for the testing samples
|
|
569
|
+
DataFrame index represents the time coordinates for the given curves.
|
|
570
|
+
DataFrame value represents transpose of the survival probabilities.
|
|
571
|
+
param test_event_times: NumericArrayLike, shape = (n_samples,)
|
|
572
|
+
Event times for the testing samples.
|
|
573
|
+
param test_event_indicators: NumericArrayLike, shape = (n_samples,)
|
|
574
|
+
Event indicators for the testing samples.
|
|
575
|
+
param train_event_times: NumericArrayLike, shape = (n_samples,), optional
|
|
576
|
+
Event times for the training samples.
|
|
577
|
+
param train_event_indicators: NumericArrayLike, shape = (n_samples,), optional
|
|
578
|
+
Event indicators for the training samples.
|
|
579
|
+
param predict_time_method: string, default: "Median"
|
|
580
|
+
The method used to calculate the predicted event time. Options: "Median" (default), "Mean".
|
|
581
|
+
param interpolation: string, default: "Linear"
|
|
582
|
+
The interpolation method used to calculate the predicted event time.
|
|
583
|
+
Options: "Linear" (default), "Pchip".
|
|
584
|
+
"""
|
|
585
|
+
time_coordinates = surv.index.values
|
|
586
|
+
predicted_survival_curves = surv.values.T
|
|
587
|
+
# Pycox models can sometimes obtain -0 as survival probabilities. Need to convert that to 0.
|
|
588
|
+
predicted_survival_curves[predicted_survival_curves < 0] = 0
|
|
589
|
+
super(PycoxEvaluator, self).__init__(predicted_survival_curves, time_coordinates, test_event_times,
|
|
590
|
+
test_event_indicators, train_event_times, train_event_indicators,
|
|
591
|
+
predict_time_method, interpolation)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class LifelinesEvaluator(PycoxEvaluator, ABC):
|
|
595
|
+
def __init__(
|
|
596
|
+
self,
|
|
597
|
+
surv: pd.DataFrame,
|
|
598
|
+
test_event_times: NumericArrayLike,
|
|
599
|
+
test_event_indicators: NumericArrayLike,
|
|
600
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
601
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
602
|
+
predict_time_method: str = "Median",
|
|
603
|
+
interpolation: str = "Linear"
|
|
604
|
+
):
|
|
605
|
+
"""
|
|
606
|
+
Evaluator for survival models in Lifelines packages.
|
|
607
|
+
param surv: pd.DataFrame, shape = (n_time_points, n_samples)
|
|
608
|
+
Predicted survival curves for the testing samples
|
|
609
|
+
param test_event_times: NumericArrayLike, shape = (n_samples,)
|
|
610
|
+
Event times for the testing samples.
|
|
611
|
+
param test_event_indicators: NumericArrayLike, shape = (n_samples,)
|
|
612
|
+
Event indicators for the testing samples.
|
|
613
|
+
param train_event_times: NumericArrayLike, shape = (n_samples,), optional
|
|
614
|
+
Event times for the training samples.
|
|
615
|
+
param train_event_indicators: NumericArrayLike, shape = (n_samples,), optional
|
|
616
|
+
Event indicators for the training samples.
|
|
617
|
+
param predict_time_method: string, default: "Median"
|
|
618
|
+
The method used to calculate the predicted event time. Options: "Median" (default), "Mean".
|
|
619
|
+
param interpolation: string, default: "Linear"
|
|
620
|
+
The interpolation method used to calculate the predicted event time.
|
|
621
|
+
Options: "Linear" (default), "Pchip".
|
|
622
|
+
"""
|
|
623
|
+
super(LifelinesEvaluator, self).__init__(surv, test_event_times, test_event_indicators, train_event_times,
|
|
624
|
+
train_event_indicators, predict_time_method, interpolation)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class ScikitSurvivalEvaluator(SurvivalEvaluator, ABC):
|
|
628
|
+
def __init__(
|
|
629
|
+
self,
|
|
630
|
+
surv: np.ndarray,
|
|
631
|
+
test_event_times: NumericArrayLike,
|
|
632
|
+
test_event_indicators: NumericArrayLike,
|
|
633
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
634
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
635
|
+
predict_time_method: str = "Median",
|
|
636
|
+
interpolation: str = "Linear"
|
|
637
|
+
):
|
|
638
|
+
"""
|
|
639
|
+
Evaluator for survival models in scikit-survival packages.
|
|
640
|
+
param surv: np.ndarray, shape = (n_samples,)
|
|
641
|
+
Predicted survival curves for the testing samples. Each element is a scikit-survival customized object.
|
|
642
|
+
'.x' attribute is the time coordinates for the given curve. '.y' attribute is the survival probabilities.
|
|
643
|
+
param test_event_times: NumericArrayLike, shape = (n_samples,)
|
|
644
|
+
Event times for the testing samples.
|
|
645
|
+
param test_event_indicators: NumericArrayLike, shape = (n_samples,)
|
|
646
|
+
Event indicators for the testing samples.
|
|
647
|
+
param train_event_times: NumericArrayLike, shape = (n_samples,), optional
|
|
648
|
+
Event times for the training samples.
|
|
649
|
+
param train_event_indicators: NumericArrayLike, shape = (n_samples,), optional
|
|
650
|
+
Event indicators for the training samples.
|
|
651
|
+
param predict_time_method: string, default: "Median"
|
|
652
|
+
The method used to calculate the predicted event time. Options: "Median" (default), "Mean".
|
|
653
|
+
param interpolation: string, default: "Linear"
|
|
654
|
+
The interpolation method used to calculate the predicted event time.
|
|
655
|
+
Options: "Linear" (default), "Pchip".
|
|
656
|
+
"""
|
|
657
|
+
time_coordinates = surv[0].x
|
|
658
|
+
predict_curves = []
|
|
659
|
+
for i in range(len(surv)):
|
|
660
|
+
predict_curve = surv[i].y
|
|
661
|
+
if False in (time_coordinates == surv[i].x):
|
|
662
|
+
raise KeyError("{}-th survival curve does not have same time coordinates".format(i))
|
|
663
|
+
predict_curves.append(predict_curve)
|
|
664
|
+
predicted_curves = np.array(predict_curves)
|
|
665
|
+
if time_coordinates[0] != 0:
|
|
666
|
+
time_coordinates = np.concatenate([np.array([0]), time_coordinates], 0)
|
|
667
|
+
predicted_curves = np.concatenate([np.ones([len(predicted_curves), 1]), predicted_curves], 1)
|
|
668
|
+
# If some survival curves are all ones, we should do something.
|
|
669
|
+
if np.any(predicted_curves[:, len(time_coordinates) - 1] == 1):
|
|
670
|
+
idx_need_fix = predicted_curves[:, len(time_coordinates) - 1] == 1
|
|
671
|
+
max_prob_at_end = np.max(predicted_curves[~idx_need_fix,
|
|
672
|
+
len(time_coordinates) - 1])
|
|
673
|
+
# max_prob_at_end + (1 - max_prob_at_end) * 0.9
|
|
674
|
+
predicted_curves[idx_need_fix, len(time_coordinates) - 1] = max(0.1 * max_prob_at_end + 0.9, 0.99)
|
|
675
|
+
super(ScikitSurvivalEvaluator, self).__init__(predicted_curves, time_coordinates, test_event_times,
|
|
676
|
+
test_event_indicators, train_event_times, train_event_indicators,
|
|
677
|
+
predict_time_method, interpolation)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
DistributionEvaluator = SurvivalEvaluator # Alias for the SurvivalEvaluator
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class PointEvaluator:
|
|
684
|
+
def __init__(
|
|
685
|
+
self,
|
|
686
|
+
predicted_times: NumericArrayLike,
|
|
687
|
+
test_event_times: NumericArrayLike,
|
|
688
|
+
test_event_indicators: NumericArrayLike,
|
|
689
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
690
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
691
|
+
):
|
|
692
|
+
"""
|
|
693
|
+
Initialize the Evaluator
|
|
694
|
+
param predicted_times: structured array, shape = (n_samples, )
|
|
695
|
+
Predicted survival times for the testing samples.
|
|
696
|
+
param test_event_times: structured array, shape = (n_samples, )
|
|
697
|
+
Actual event/censor time for the testing samples.
|
|
698
|
+
param test_event_indicators: structured array, shape = (n_samples, )
|
|
699
|
+
Binary indicators of censoring for the testing samples
|
|
700
|
+
param train_event_times: structured array, shape = (n_train_samples, )
|
|
701
|
+
Actual event/censor time for the training samples.
|
|
702
|
+
param train_event_indicators: structured array, shape = (n_train_samples, )
|
|
703
|
+
Binary indicators of censoring for the training samples
|
|
704
|
+
"""
|
|
705
|
+
self._predicted_times = check_and_convert(predicted_times)
|
|
706
|
+
|
|
707
|
+
self.event_times, self.event_indicators = check_and_convert(test_event_times, test_event_indicators)
|
|
708
|
+
|
|
709
|
+
if (train_event_times is not None) and (train_event_indicators is not None):
|
|
710
|
+
train_event_times, train_event_indicators = check_and_convert(train_event_times, train_event_indicators)
|
|
711
|
+
self.train_event_times = train_event_times
|
|
712
|
+
self.train_event_indicators = train_event_indicators
|
|
713
|
+
|
|
714
|
+
def _error_trainset(self, method_name: str):
|
|
715
|
+
if (self.train_event_times is None) or (self.train_event_indicators is None):
|
|
716
|
+
raise TypeError("Train set information is missing. "
|
|
717
|
+
"Evaluator cannot perform {} evaluation.".format(method_name))
|
|
718
|
+
|
|
719
|
+
@property
|
|
720
|
+
def predicted_times(self):
|
|
721
|
+
return self._predicted_times
|
|
722
|
+
|
|
723
|
+
@predicted_times.setter
|
|
724
|
+
def predicted_times(self, predicted_times):
|
|
725
|
+
print("Setter called. Resetting predicted_times.")
|
|
726
|
+
self._predicted_times = predicted_times
|
|
727
|
+
|
|
728
|
+
def concordance(
|
|
729
|
+
self,
|
|
730
|
+
ties: str = "None",
|
|
731
|
+
pair_method: str = "Comparable"
|
|
732
|
+
) -> (float, float, int):
|
|
733
|
+
"""
|
|
734
|
+
Calculate the concordance index between the predicted survival times and the true survival times.
|
|
735
|
+
param ties: str, default = "None"
|
|
736
|
+
A string indicating the way ties should be handled.
|
|
737
|
+
Options: "None" (default), "Time", "Risk", or "All"
|
|
738
|
+
"None" will throw out all ties in true survival time and all ties in predict survival times (risk scores).
|
|
739
|
+
"Time" includes ties in true survival time but removes ties in predict survival times (risk scores).
|
|
740
|
+
"Risk" includes ties in predict survival times (risk scores) but not in true survival time.
|
|
741
|
+
"All" includes all ties.
|
|
742
|
+
Note the concordance calculation is given by
|
|
743
|
+
(Concordant Pairs + (Number of Ties/2))/(Concordant Pairs + Discordant Pairs + Number of Ties).
|
|
744
|
+
param pair_method: str, default = "Comparable"
|
|
745
|
+
A string indicating the method for constructing the pairs of samples.
|
|
746
|
+
Options: "Comparable" (default) or "Margin"
|
|
747
|
+
"Comparable": the pairs are constructed by comparing the predicted survival time of each sample with the
|
|
748
|
+
event time of all other samples. The pairs are only constructed between samples with comparable
|
|
749
|
+
event times. For example, if sample i has a censor time of 10, then the pairs are constructed by
|
|
750
|
+
comparing the predicted survival time of sample i with the event time of all samples with event
|
|
751
|
+
time of 10 or less.
|
|
752
|
+
"Margin": the pairs are constructed between all samples. A best-guess time for the censored samples
|
|
753
|
+
will be calculated and used to construct the pairs.
|
|
754
|
+
:return: (float, float, int)
|
|
755
|
+
The concordance index, the number of concordant pairs, and the number of total pairs.
|
|
756
|
+
"""
|
|
757
|
+
# Choose prediction method based on the input argument
|
|
758
|
+
if pair_method == "Margin" and (self.train_event_times is None or self.train_event_indicators is None):
|
|
759
|
+
self._error_trainset("margin concordance")
|
|
760
|
+
|
|
761
|
+
return concordance(self._predicted_times, self.event_times, self.event_indicators, self.train_event_times,
|
|
762
|
+
self.train_event_indicators, pair_method, ties)
|
|
763
|
+
|
|
764
|
+
def mae(
|
|
765
|
+
self,
|
|
766
|
+
method: str = "Hinge",
|
|
767
|
+
weighted: bool = False,
|
|
768
|
+
log_scale: bool = False
|
|
769
|
+
) -> float:
|
|
770
|
+
"""
|
|
771
|
+
Calculate the MAE score for the test set.
|
|
772
|
+
param method: string, default: "Hinge"
|
|
773
|
+
The method used to calculate the MAE score.
|
|
774
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
775
|
+
param weighted: bool, default: True
|
|
776
|
+
Whether to use weighting scheme for MAE.
|
|
777
|
+
param log_scale: boolean, default: False
|
|
778
|
+
Whether to use log scale for the time axis.
|
|
779
|
+
:return: float
|
|
780
|
+
The MAE score for the test set.
|
|
781
|
+
"""
|
|
782
|
+
return mean_error(
|
|
783
|
+
predicted_times=self._predicted_times,
|
|
784
|
+
event_times=self.event_times,
|
|
785
|
+
event_indicators=self.event_indicators,
|
|
786
|
+
train_event_times=self.train_event_times,
|
|
787
|
+
train_event_indicators=self.train_event_indicators,
|
|
788
|
+
error_type="absolute",
|
|
789
|
+
method=method,
|
|
790
|
+
weighted=weighted,
|
|
791
|
+
log_scale=log_scale
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
def mse(
|
|
795
|
+
self,
|
|
796
|
+
method: str = "Hinge",
|
|
797
|
+
weighted: bool = True,
|
|
798
|
+
log_scale: bool = False
|
|
799
|
+
) -> float:
|
|
800
|
+
"""
|
|
801
|
+
Calculate the MAE score for the test set.
|
|
802
|
+
param method: string, default: "Hinge"
|
|
803
|
+
The method used to calculate the MAE score.
|
|
804
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
805
|
+
param weighted: bool, default: True
|
|
806
|
+
Whether to use weighting scheme for MAE.
|
|
807
|
+
param log_scale: boolean, default: False
|
|
808
|
+
Whether to use log scale for the time axis.
|
|
809
|
+
:return: float
|
|
810
|
+
The MAE score for the test set.
|
|
811
|
+
"""
|
|
812
|
+
return mean_error(
|
|
813
|
+
predicted_times=self._predicted_times,
|
|
814
|
+
event_times=self.event_times,
|
|
815
|
+
event_indicators=self.event_indicators,
|
|
816
|
+
train_event_times=self.train_event_times,
|
|
817
|
+
train_event_indicators=self.train_event_indicators,
|
|
818
|
+
error_type="squared",
|
|
819
|
+
method=method,
|
|
820
|
+
weighted=weighted,
|
|
821
|
+
log_scale=log_scale
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
def rmse(
|
|
825
|
+
self,
|
|
826
|
+
method: str = "Hinge",
|
|
827
|
+
weighted: bool = True,
|
|
828
|
+
log_scale: bool = False
|
|
829
|
+
) -> float:
|
|
830
|
+
"""
|
|
831
|
+
Calculate the root mean squared error (RMSE) score for the test set.
|
|
832
|
+
param method: string, default: "Hinge"
|
|
833
|
+
The method used to calculate the MAE score.
|
|
834
|
+
Options: "Uncensored", "Hinge" (default), "Margin", "IPCW-v1", "IPCW-v2", or "Pseudo_obs"\
|
|
835
|
+
param weighted: bool, default = True
|
|
836
|
+
Whether to use weighting scheme for MAE.
|
|
837
|
+
param log_scale: boolean, default = False
|
|
838
|
+
Whether to use log scale for the time axis.
|
|
839
|
+
:return: float
|
|
840
|
+
The MAE score for the test set.
|
|
841
|
+
"""
|
|
842
|
+
return self.mse(method, weighted, log_scale) ** 0.5
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
class SingleTimeEvaluator:
|
|
846
|
+
def __init__(
|
|
847
|
+
self,
|
|
848
|
+
predicted_probs: NumericArrayLike,
|
|
849
|
+
test_event_times: NumericArrayLike,
|
|
850
|
+
test_event_indicators: NumericArrayLike,
|
|
851
|
+
target_time: Union[float, int] = None,
|
|
852
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
853
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
854
|
+
):
|
|
855
|
+
self._predicted_probs = check_and_convert(predicted_probs)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
self.event_times, self.event_indicators = check_and_convert(test_event_times, test_event_indicators)
|
|
859
|
+
|
|
860
|
+
if (train_event_times is not None) and (train_event_indicators is not None):
|
|
861
|
+
train_event_times, train_event_indicators = check_and_convert(train_event_times, train_event_indicators)
|
|
862
|
+
self.train_event_times = train_event_times
|
|
863
|
+
self.train_event_indicators = train_event_indicators
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
if target_time is None:
|
|
867
|
+
# set to the median time of all the event/censor times from the training and test sets
|
|
868
|
+
# if train set is not provided, use test set only
|
|
869
|
+
event_times = np.concatenate((self.event_times, self.train_event_times)) \
|
|
870
|
+
if self.train_event_times is not None else self.event_times
|
|
871
|
+
target_time = np.quantile(event_times, 0.5)
|
|
872
|
+
self.target_time = target_time
|
|
873
|
+
|
|
874
|
+
def _error_trainset(self, method_name: str):
|
|
875
|
+
if (self.train_event_times is None) or (self.train_event_indicators is None):
|
|
876
|
+
raise TypeError("Train set information is missing. "
|
|
877
|
+
"Evaluator cannot perform {} evaluation.".format(method_name))
|
|
878
|
+
|
|
879
|
+
@property
|
|
880
|
+
def predicted_probs(self):
|
|
881
|
+
return self._predicted_probs
|
|
882
|
+
|
|
883
|
+
@predicted_probs.setter
|
|
884
|
+
def predicted_probs(self, predicted_probs):
|
|
885
|
+
print("Setter called. Resetting predicted_probs.")
|
|
886
|
+
self._predicted_probs = predicted_probs
|
|
887
|
+
|
|
888
|
+
def auc(
|
|
889
|
+
self,
|
|
890
|
+
) -> float:
|
|
891
|
+
"""
|
|
892
|
+
Calculate the area under the ROC curve (AUC) score at a given time point from the predicted survival curve.
|
|
893
|
+
:return: float
|
|
894
|
+
The Brier score at the target time point.
|
|
895
|
+
"""
|
|
896
|
+
return auc(self._predicted_probs, self.event_times, self.event_indicators, self.target_time)
|
|
897
|
+
|
|
898
|
+
def brier_score(
|
|
899
|
+
self,
|
|
900
|
+
IPCW_weighted: bool = True
|
|
901
|
+
) -> float:
|
|
902
|
+
"""
|
|
903
|
+
Calculate the Brier score at a given time point from the predicted survival curve.
|
|
904
|
+
param IPCW_weighted: bool, default = True
|
|
905
|
+
Whether to use IPCW weighting for the Brier score.
|
|
906
|
+
:return: float
|
|
907
|
+
The Brier score at the target time point.
|
|
908
|
+
"""
|
|
909
|
+
if IPCW_weighted:
|
|
910
|
+
self._error_trainset("IPCW-weighted Brier score (BS)")
|
|
911
|
+
return single_brier_score(self._predicted_probs, self.event_times, self.event_indicators, self.train_event_times,
|
|
912
|
+
self.train_event_indicators, self.target_time, IPCW_weighted)
|
|
913
|
+
|
|
914
|
+
def one_calibration(
|
|
915
|
+
self,
|
|
916
|
+
num_bins: int = 10,
|
|
917
|
+
method: str = "DN"
|
|
918
|
+
) -> (float, list, list):
|
|
919
|
+
"""
|
|
920
|
+
Calculate the one calibration score at a given time point from the predicted survival curve.
|
|
921
|
+
param num_bins: int, default: 10
|
|
922
|
+
Number of bins used to calculate the one calibration score.
|
|
923
|
+
param method: string, default: "DN"
|
|
924
|
+
The method used to calculate the one calibration score.
|
|
925
|
+
Options: "Uncensored", or "DN" (default)
|
|
926
|
+
:return: float, list, list
|
|
927
|
+
(p-value, observed probabilities, expected probabilities)
|
|
928
|
+
"""
|
|
929
|
+
return one_calibration(self._predicted_probs, self.event_times, self.event_indicators,
|
|
930
|
+
self.target_time, num_bins, method)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
class QuantileRegEvaluator(SurvivalEvaluator):
|
|
934
|
+
def __init__(
|
|
935
|
+
self,
|
|
936
|
+
quantile_regression: NumericArrayLike,
|
|
937
|
+
quantile_levels: NumericArrayLike,
|
|
938
|
+
test_event_times: NumericArrayLike,
|
|
939
|
+
test_event_indicators: NumericArrayLike,
|
|
940
|
+
train_event_times: Optional[NumericArrayLike] = None,
|
|
941
|
+
train_event_indicators: Optional[NumericArrayLike] = None,
|
|
942
|
+
predict_time_method: str = "Median",
|
|
943
|
+
interpolation: str = "Linear"
|
|
944
|
+
):
|
|
945
|
+
"""
|
|
946
|
+
Initialize the quantile regression evaluator.
|
|
947
|
+
Parameters
|
|
948
|
+
----------
|
|
949
|
+
param quantile_regression: array-like, shape = (n_quantiles, n_samples)
|
|
950
|
+
Predicted quantile curves for the testing samples.
|
|
951
|
+
param quantile_levels: array-like, shape = (n_quantiles, )
|
|
952
|
+
Quantile levels for the quantile curves.
|
|
953
|
+
param test_event_times: structured array, shape = (n_samples, )
|
|
954
|
+
Actual event/censor time for the testing samples.
|
|
955
|
+
param test_event_indicators: structured array, shape = (n_samples, )
|
|
956
|
+
Binary indicators of censoring for the testing samples
|
|
957
|
+
param train_event_times: structured array, shape = (n_train_samples, )
|
|
958
|
+
Actual event/censor time for the training samples.
|
|
959
|
+
param train_event_indicators: structured array, shape = (n_train_samples, )
|
|
960
|
+
Binary indicators of censoring for the training samples
|
|
961
|
+
param predict_time_method: str, default = "Median"
|
|
962
|
+
Method for calculating predicted survival time. Available options are "Median" and "Mean".
|
|
963
|
+
param interpolation: str, default = "Linear"
|
|
964
|
+
Method for interpolation. Available options are ['Linear', 'Pchip'].
|
|
965
|
+
"""
|
|
966
|
+
survival_level = 1 - quantile_levels
|
|
967
|
+
super(QuantileRegEvaluator, self).__init__(survival_level, quantile_regression, test_event_times,
|
|
968
|
+
test_event_indicators, train_event_times, train_event_indicators,
|
|
969
|
+
predict_time_method, interpolation)
|
|
970
|
+
|
|
971
|
+
def predict_time_from_curve(
|
|
972
|
+
self,
|
|
973
|
+
predict_method: Callable,
|
|
974
|
+
) -> np.ndarray:
|
|
975
|
+
"""
|
|
976
|
+
Predict survival time from survival curves.
|
|
977
|
+
param predict_method: Callable
|
|
978
|
+
A function that takes in a survival curve and returns a predicted survival time.
|
|
979
|
+
There are two build-in methods: 'predict_median_survival_time' and 'predict_mean_survival_time'.
|
|
980
|
+
'predict_median_survival_time' uses the median of the survival curve as the predicted survival time.
|
|
981
|
+
'predict_mean_survival_time' uses the expected time of the survival curve as the predicted survival time.
|
|
982
|
+
:return: np.ndarray
|
|
983
|
+
Predicted survival time for each sample.
|
|
984
|
+
"""
|
|
985
|
+
if (predict_method is not predict_mean_survival_time) and (predict_method is not predict_median_survival_time):
|
|
986
|
+
error = "Prediction method must be 'predict_mean_survival_time' or 'predict_median_survival_time', " \
|
|
987
|
+
"got '{}' instead".format(predict_method.__name__)
|
|
988
|
+
raise TypeError(error)
|
|
989
|
+
|
|
990
|
+
predicted_times = []
|
|
991
|
+
for i in range(self.time_coordinates.shape[0]):
|
|
992
|
+
predicted_time = predict_method(self.predicted_curves, self.time_coordinates[i, :], self.interpolation)
|
|
993
|
+
predicted_times.append(predicted_time)
|
|
994
|
+
predicted_times = np.array(predicted_times)
|
|
995
|
+
return predicted_times
|
|
996
|
+
|
|
997
|
+
def predict_probability_from_curve(
|
|
998
|
+
self,
|
|
999
|
+
target_time: Union[float, int, np.ndarray],
|
|
1000
|
+
) -> np.ndarray:
|
|
1001
|
+
"""
|
|
1002
|
+
Predict a probability of event at a given time point from a predicted curve. Each predicted curve will only
|
|
1003
|
+
have one corresponding probability. Note that this method is different from the
|
|
1004
|
+
'predict_multi_probabilities_from_curve' method, which predicts the multiple probabilities at multiple time
|
|
1005
|
+
points from a predicted curve.
|
|
1006
|
+
param target_time: float, int, or array-like, shape = (n_samples, )
|
|
1007
|
+
Time point(s) at which the probability of event is to be predicted. If float or int, the same time point is
|
|
1008
|
+
used for all samples. If array-like, each sample will have it own target time. The length of the array must
|
|
1009
|
+
be the same as the number of samples.
|
|
1010
|
+
:return: array-like, shape = (n_samples, )
|
|
1011
|
+
Predicted probabilities of event at the target time point(s).
|
|
1012
|
+
"""
|
|
1013
|
+
if isinstance(target_time, (float, int)):
|
|
1014
|
+
target_time = target_time * np.ones_like(self.event_times)
|
|
1015
|
+
elif isinstance(target_time, np.ndarray):
|
|
1016
|
+
assert target_time.ndim == 1, "Target time must be a 1D array"
|
|
1017
|
+
assert target_time.shape[0] == self.time_coordinates.shape[0], "Target time must have the same length as " \
|
|
1018
|
+
"the number of samples"
|
|
1019
|
+
else:
|
|
1020
|
+
error = "Target time must be a float, int, or 1D array, got '{}' instead".format(type(target_time))
|
|
1021
|
+
raise TypeError(error)
|
|
1022
|
+
|
|
1023
|
+
predict_probs = []
|
|
1024
|
+
for i in range(self.time_coordinates.shape[0]):
|
|
1025
|
+
predict_prob = predict_prob_from_curve(self.predicted_curves, self.time_coordinates[i, :],
|
|
1026
|
+
target_time[i], self.interpolation)
|
|
1027
|
+
predict_probs.append(predict_prob)
|
|
1028
|
+
predict_probs = np.array(predict_probs)
|
|
1029
|
+
return predict_probs
|
|
1030
|
+
|
|
1031
|
+
def predict_multi_probabilities_from_curve(
|
|
1032
|
+
self,
|
|
1033
|
+
target_times: np.ndarray
|
|
1034
|
+
) -> np.ndarray:
|
|
1035
|
+
"""
|
|
1036
|
+
Predict the probability of event at multiple time points from the predicted curve.
|
|
1037
|
+
param target_times: array-like, shape = (n_target_times)
|
|
1038
|
+
Time points at which the probability of event is to be predicted.
|
|
1039
|
+
:return: array-like, shape = (n_samples, n_target_times)
|
|
1040
|
+
Predicted probabilities of event at the target time points.
|
|
1041
|
+
"""
|
|
1042
|
+
predict_probs_mat = []
|
|
1043
|
+
for i in range(self.time_coordinates.shape[0]):
|
|
1044
|
+
predict_probs = predict_multi_probs_from_curve(self.predicted_curves, self.time_coordinates[i, :],
|
|
1045
|
+
target_times, self.interpolation).tolist()
|
|
1046
|
+
predict_probs_mat.append(predict_probs)
|
|
1047
|
+
predict_probs_mat = np.array(predict_probs_mat)
|
|
1048
|
+
return predict_probs_mat
|
|
1049
|
+
|
|
1050
|
+
def plot_survival_curves(
|
|
1051
|
+
self,
|
|
1052
|
+
curve_indices,
|
|
1053
|
+
color=None,
|
|
1054
|
+
x_lim: tuple = None,
|
|
1055
|
+
y_lim: tuple = None,
|
|
1056
|
+
x_label: str = 'Time',
|
|
1057
|
+
y_label: str = 'Survival probability'
|
|
1058
|
+
):
|
|
1059
|
+
"""Plot survival curves."""
|
|
1060
|
+
fig, ax = plt.subplots()
|
|
1061
|
+
ax.plot(self.time_coordinates[curve_indices, :].T, self.predicted_curves, color=color, label=curve_indices)
|
|
1062
|
+
if y_lim is None:
|
|
1063
|
+
ax.set_ylim(0, 1.02)
|
|
1064
|
+
else:
|
|
1065
|
+
ax.set_ylim(y_lim)
|
|
1066
|
+
|
|
1067
|
+
if x_lim is not None:
|
|
1068
|
+
ax.set_xlim(x_lim)
|
|
1069
|
+
ax.set_xlabel(x_label)
|
|
1070
|
+
ax.set_ylabel(y_label)
|
|
1071
|
+
ax.legend()
|
|
1072
|
+
return fig, ax
|
|
1073
|
+
|
|
1074
|
+
def km_calibration(self, draw_figure: bool = False):
|
|
1075
|
+
"""
|
|
1076
|
+
Calculate the KM calibration score from the predicted survival curve.
|
|
1077
|
+
:return: float
|
|
1078
|
+
KL divergence between the average predicted survival distribution and the Kaplan-Meier distribution.
|
|
1079
|
+
"""
|
|
1080
|
+
unique_times = np.unique(self.event_times[self.event_indicators == 1])
|
|
1081
|
+
survival_curves = quantile_to_survival(1 - self.predicted_curves, self.time_coordinates,
|
|
1082
|
+
unique_times, interpolate=self.interpolation)
|
|
1083
|
+
avg_surv = np.mean(survival_curves, axis=0)
|
|
1084
|
+
|
|
1085
|
+
return km_calibration(avg_surv, np.unique(self.event_times[self.event_indicators == 1]),
|
|
1086
|
+
self.event_times, self.event_indicators,
|
|
1087
|
+
interpolation_method=self.interpolation, draw_figure=draw_figure)
|