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.
@@ -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)