statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
survival.hpp
Go to the documentation of this file.
1
9#pragma once
10
11#include <algorithm>
12#include <cmath>
13#include <cstddef>
14#include <limits>
15#include <numeric>
16#include <stdexcept>
17#include <vector>
18
20
21namespace statcpp {
22
23// ============================================================================
24// Kaplan-Meier Estimator
25// ============================================================================
26
33 std::vector<double> times;
34 std::vector<double> survival;
35 std::vector<double> se;
36 std::vector<double> ci_lower;
37 std::vector<double> ci_upper;
38 std::vector<std::size_t> n_at_risk;
39 std::vector<std::size_t> n_events;
40 std::vector<std::size_t> n_censored;
41};
42
55 const std::vector<double>& times,
56 const std::vector<bool>& events)
57{
58 if (times.size() != events.size()) {
59 throw std::invalid_argument("statcpp::kaplan_meier: times and events must have same length");
60 }
61 if (times.empty()) {
62 throw std::invalid_argument("statcpp::kaplan_meier: empty data");
63 }
64
65 std::size_t n = times.size();
66
67 // Sort indices by time
68 std::vector<std::size_t> indices(n);
69 std::iota(indices.begin(), indices.end(), 0);
70 std::sort(indices.begin(), indices.end(),
71 [&times](std::size_t i, std::size_t j) {
72 return times[i] < times[j];
73 });
74
76 result.times.push_back(0.0);
77 result.survival.push_back(1.0);
78 result.se.push_back(0.0);
79 result.n_at_risk.push_back(n);
80 result.n_events.push_back(0);
81 result.n_censored.push_back(0);
82
83 double S = 1.0; // Cumulative survival probability
84 double var_sum = 0.0; // Cumulative sum of Greenwood variance terms
85 std::size_t n_risk = n; // Risk set size
86
87 std::size_t i = 0;
88 while (i < n) {
89 double t = times[indices[i]];
90
91 // Count events and censored at this time point
92 std::size_t d = 0; // Number of events
93 std::size_t c = 0; // Number of censored
94
95 while (i < n && times[indices[i]] == t) {
96 if (events[indices[i]]) {
97 d++;
98 } else {
99 c++;
100 }
101 i++;
102 }
103
104 // Update survival probability only if there were events
105 if (d > 0) {
106 double q = static_cast<double>(d) / static_cast<double>(n_risk);
107 S *= (1.0 - q);
108
109 // Greenwood variance
110 if (n_risk > d) {
111 var_sum += static_cast<double>(d) /
112 (static_cast<double>(n_risk) * static_cast<double>(n_risk - d));
113 }
114
115 double se = S * std::sqrt(var_sum);
116
117 // 95% confidence interval (log transformation)
118 double z = 1.96;
119 double ci_lower, ci_upper;
120 if (S > 0 && S < 1) {
121 double log_S = std::log(S);
122 double se_log = se / S;
123 ci_lower = std::exp(log_S - z * se_log);
124 ci_upper = std::exp(log_S + z * se_log);
125 ci_lower = std::max(0.0, ci_lower);
126 ci_upper = std::min(1.0, ci_upper);
127 } else {
128 ci_lower = S;
129 ci_upper = S;
130 }
131
132 result.times.push_back(t);
133 result.survival.push_back(S);
134 result.se.push_back(se);
135 result.ci_lower.push_back(ci_lower);
136 result.ci_upper.push_back(ci_upper);
137 result.n_at_risk.push_back(n_risk);
138 result.n_events.push_back(d);
139 result.n_censored.push_back(c);
140 }
141
142 n_risk -= (d + c);
143 }
144
145 // Set confidence interval for the first point
146 result.ci_lower.insert(result.ci_lower.begin(), 1.0);
147 result.ci_upper.insert(result.ci_upper.begin(), 1.0);
148
149 return result;
150}
151
152// ============================================================================
153// Log-rank Test
154// ============================================================================
155
162 double statistic;
163 double p_value;
164 std::size_t df;
165 double expected1;
166 double expected2;
167 std::size_t observed1;
168 std::size_t observed2;
169};
170
185 const std::vector<double>& times1,
186 const std::vector<bool>& events1,
187 const std::vector<double>& times2,
188 const std::vector<bool>& events2)
189{
190 if (times1.size() != events1.size() || times2.size() != events2.size()) {
191 throw std::invalid_argument("statcpp::logrank_test: times and events must have same length");
192 }
193 if (times1.empty() || times2.empty()) {
194 throw std::invalid_argument("statcpp::logrank_test: empty data");
195 }
196
197 // Get all unique times and sort
198 std::vector<double> all_times;
199 all_times.reserve(times1.size() + times2.size());
200 for (double t : times1) all_times.push_back(t);
201 for (double t : times2) all_times.push_back(t);
202 std::sort(all_times.begin(), all_times.end());
203 all_times.erase(std::unique(all_times.begin(), all_times.end()), all_times.end());
204
205 // Calculate risk set and event count for each time point
206 std::size_t O1 = 0; // Observed events in group 1
207 std::size_t O2 = 0; // Observed events in group 2
208 double E1 = 0.0; // Expected events in group 1
209 double var = 0.0; // Variance
210
211 // Check if events occurred at each time
212 for (double t : all_times) {
213 // Number of observations >= this time = risk set
214 std::size_t n1_risk = 0;
215 std::size_t n2_risk = 0;
216 std::size_t d1 = 0; // Events in group 1
217 std::size_t d2 = 0; // Events in group 2
218
219 for (std::size_t i = 0; i < times1.size(); ++i) {
220 if (times1[i] >= t) {
221 n1_risk++;
222 }
223 if (times1[i] == t && events1[i]) {
224 d1++;
225 }
226 }
227
228 for (std::size_t i = 0; i < times2.size(); ++i) {
229 if (times2[i] >= t) {
230 n2_risk++;
231 }
232 if (times2[i] == t && events2[i]) {
233 d2++;
234 }
235 }
236
237 std::size_t d = d1 + d2; // Total events
238 std::size_t n_risk = n1_risk + n2_risk; // Total at risk
239
240 if (d > 0 && n_risk > 0) {
241 O1 += d1;
242 O2 += d2;
243
244 double e1 = static_cast<double>(n1_risk) * static_cast<double>(d) /
245 static_cast<double>(n_risk);
246 E1 += e1;
247
248 // Variance (hypergeometric distribution variance)
249 if (n_risk > 1) {
250 var += static_cast<double>(n1_risk) * static_cast<double>(n2_risk) *
251 static_cast<double>(d) * static_cast<double>(n_risk - d) /
252 (static_cast<double>(n_risk) * static_cast<double>(n_risk) *
253 static_cast<double>(n_risk - 1));
254 }
255 }
256 }
257
258 double E2 = static_cast<double>(O1 + O2) - E1;
259
260 // Test statistic
261 double stat = 0.0;
262 if (var > 0) {
263 double diff = static_cast<double>(O1) - E1;
264 stat = (diff * diff) / var;
265 }
266
267 // p-value (chi-square distribution, df=1)
268 double p_value = 1.0 - statcpp::chisq_cdf(stat, 1.0);
269
270 return {stat, p_value, 1, E1, E2, O1, O2};
271}
272
273// ============================================================================
274// Median Survival Time
275// ============================================================================
276
287{
288 // Find the first time where S(t) becomes <= 0.5
289 for (std::size_t i = 0; i < km.survival.size(); ++i) {
290 if (km.survival[i] <= 0.5) {
291 return km.times[i];
292 }
293 }
294 // Return NaN if 50% is not reached
295 return std::numeric_limits<double>::quiet_NaN();
296}
297
298// ============================================================================
299// Hazard Rate (Actuarial Method)
300// ============================================================================
301
308 std::vector<double> times;
309 std::vector<double> hazard;
310 std::vector<double> cumulative_hazard;
311};
312
338 const std::vector<double>& times,
339 const std::vector<bool>& events)
340{
341 if (times.size() != events.size()) {
342 throw std::invalid_argument("statcpp::nelson_aalen: times and events must have same length");
343 }
344 if (times.empty()) {
345 throw std::invalid_argument("statcpp::nelson_aalen: empty data");
346 }
347
348 std::size_t n = times.size();
349
350 // Sort indices by time
351 std::vector<std::size_t> indices(n);
352 std::iota(indices.begin(), indices.end(), 0);
353 std::sort(indices.begin(), indices.end(),
354 [&times](std::size_t i, std::size_t j) {
355 return times[i] < times[j];
356 });
357
358 hazard_rate_result result;
359 result.times.push_back(0.0);
360 result.hazard.push_back(0.0);
361 result.cumulative_hazard.push_back(0.0);
362
363 double H = 0.0; // Cumulative hazard
364 std::size_t n_risk = n;
365
366 std::size_t i = 0;
367 while (i < n) {
368 double t = times[indices[i]];
369 std::size_t d = 0;
370 std::size_t c = 0;
371
372 while (i < n && times[indices[i]] == t) {
373 if (events[indices[i]]) {
374 d++;
375 } else {
376 c++;
377 }
378 i++;
379 }
380
381 if (d > 0 && n_risk > 0) {
382 double h = static_cast<double>(d) / static_cast<double>(n_risk);
383 H += h;
384
385 result.times.push_back(t);
386 result.hazard.push_back(h);
387 result.cumulative_hazard.push_back(H);
388 }
389
390 n_risk -= (d + c);
391 }
392
393 return result;
394}
395
396} // namespace statcpp
Continuous distribution functions.
double median_survival_time(const kaplan_meier_result &km)
Calculate median survival time.
Definition survival.hpp:286
double chisq_cdf(double x, double df)
Chi-square distribution cumulative distribution function (CDF)
double var(Iterator first, Iterator last, std::size_t ddof=0)
Variance (ddof = Delta Degrees of Freedom)
logrank_result logrank_test(const std::vector< double > &times1, const std::vector< bool > &events1, const std::vector< double > &times2, const std::vector< bool > &events2)
Log-rank test (comparison of two survival curves)
Definition survival.hpp:184
kaplan_meier_result kaplan_meier(const std::vector< double > &times, const std::vector< bool > &events)
Estimate Kaplan-Meier survival curve.
Definition survival.hpp:54
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
hazard_rate_result nelson_aalen(const std::vector< double > &times, const std::vector< bool > &events)
Nelson-Aalen cumulative hazard estimation.
Definition survival.hpp:337
Hazard rate result.
Definition survival.hpp:307
std::vector< double > times
Interval start times.
Definition survival.hpp:308
std::vector< double > cumulative_hazard
Cumulative hazard.
Definition survival.hpp:310
std::vector< double > hazard
Hazard rates.
Definition survival.hpp:309
Kaplan-Meier estimation result.
Definition survival.hpp:32
std::vector< double > times
Event times.
Definition survival.hpp:33
std::vector< double > ci_upper
95% confidence interval upper bound
Definition survival.hpp:37
std::vector< double > ci_lower
95% confidence interval lower bound
Definition survival.hpp:36
std::vector< double > survival
Survival probabilities.
Definition survival.hpp:34
std::vector< std::size_t > n_censored
Number of censored at each time point.
Definition survival.hpp:40
std::vector< std::size_t > n_at_risk
Risk set size.
Definition survival.hpp:38
std::vector< std::size_t > n_events
Number of events at each time point.
Definition survival.hpp:39
std::vector< double > se
Standard errors (Greenwood's formula)
Definition survival.hpp:35
Log-rank test result.
Definition survival.hpp:161
double expected2
Expected number of events in group 2.
Definition survival.hpp:166
double p_value
p-value
Definition survival.hpp:163
double statistic
Test statistic (chi-square)
Definition survival.hpp:162
std::size_t df
Degrees of freedom.
Definition survival.hpp:164
std::size_t observed2
Observed number of events in group 2.
Definition survival.hpp:168
double expected1
Expected number of events in group 1.
Definition survival.hpp:165
std::size_t observed1
Observed number of events in group 1.
Definition survival.hpp:167