fuzzySurvival

Fuzzy survival analysis for data with vague event times and vague failure indicators.

Survival records are often imprecise: a failure is known to have happened “during the second quarter”, between two follow-up visits, or not at all - a subject lost to follow-up may or may not have failed. fuzzySurvival models that imprecision directly instead of discarding it or forcing an arbitrary choice, and reports the whole consequence on the estimation rather than a single number.

The package implements the alpha-cut extension of the Kaplan-Meier estimator, of the Cox proportional hazards model and of the log-rank test, together with the type-reduction (defuzzification) rules needed to turn a fuzzy result into a single interpretable quantity.

Installation

# from a local checkout
install.packages(".", repos = NULL, type = "source")

Why another package?

Survival analysis with fuzzy numbers is not covered by any CRAN package. FuzzyNumbers and friends provide the fuzzy-set machinery, but nothing connects it to censoring and to the partial likelihood; survival covers crisp data only. The two ingredients are trivially combined once the algebra is right, but the result has to satisfy properties that ordinary code breaks:

Both properties are covered by the test suite.

The two kinds of vagueness

Vague event times. The membership function of the event time says how plausible each value is. A failure observed between two examinations gives a flat fuzzy number on the interval between them (the value is entirely plausible anywhere in it); a rounded recollection gives a trapezoid.

library(fuzzySurvival)

# "the failure occurred in the second quarter" (months 4 to 6)
fuzzy_number_interval(4, 6)
#> <fuzzy_number> with 1 element
#> [1] "I[4, 6]"

# certainly between 3 and 9, plausibly concentrated in 5-7
fuzzy_number_trapezoid(3, 5, 7, 9)

Vague failure indicators. A status strictly between 0 and 1 is read as the degree of belief that the subject failed. Such a subject contributes a fractional number of events, and stays in the risk set until they are censored:

fuzzy_surv(c(2, 5, 7, 9), c(1, 0.5, 1, 1))

Fuzzy Kaplan-Meier

At level alpha the data compatible with the information are the box of times between the alpha-cuts. Replacing each time by the lower or the upper end of its cut makes the failures happen as early or as late as possible, so the two Kaplan-Meier curves bound the survival function:

data(fuzzy_dfs)

fit <- fkm(fuzzy_surv(ft, fs) ~ 1, data = fuzzy_dfs)
fit
#> Fuzzy Kaplan-Meier estimate
#>   n = 240, events = 81, uncertain = 40, alpha levels = 21
#>   mean width of the alpha-cuts at level 0: 0.675

summary(fit, times = c(6, 12, 24))[, c("time", "n.risk", "lower", "surv", "upper")]
#>   time n.risk   lower   surv  upper
#> 1    6    212 0.00000 0.9781 0.9781
#> 2   12    187 0.9360 0.9410 0.9460
#> 3   24    129 0.7668 0.7784 0.7900

lower and upper are the bounds at the requested level, surv the defuzzified curve. On precise data the two bounds coincide and the estimator reproduces survival::survfit() exactly.

Fuzzy Cox model

The same construction is applied to the partial likelihood: the alpha-cut of a coefficient spans every value obtained at a level at or below it, which is what makes the intervals nested. Hazard ratios keep their membership structure because the exponential is monotone.

fcox(fuzzy_surv(ft, fs) ~ age + arm, data = fuzzy_dfs)
#> Fuzzy Cox proportional hazards model
#>
#>        coef         p fuzzy.lower fuzzy.upper
#> age -0.0465 7.914e-05     -0.0696     -0.0234
#> arm  0.5211  0.005565      0.1527      0.8895

Fuzzy log-rank test

flogrank(fuzzy_surv(ft, fs) ~ arm, data = fuzzy_dfs)
#> Fuzzy log-rank test
#>   n = 240, groups: Control vs Treated
#>   observed / expected events in group 1:  49 / 65.66
#>   crisp chi-square (level 1):  9.3273
#>   crisp p-value: 0.002258

On crisp data this reproduces survival::survdiff().

Defuzzification

A fuzzy result carries a whole family of alpha-cuts; a single number is obtained by a type-reduction rule. Nine classical rules are available - midrange, centroid, gmirl, sgmrl, ms, msm, yager, min, max:

defuzz(fuzzy_number_trapezoid(3, 5, 7, 9), "centroid")
#> [1] 6

# the fuzzy survival probability itself, reduced at three levels
defuzz(fkm_fuzzy_surv(fit, times = c(6, 12, 24)), "midrange", level = 0.5)

Every rule of a symmetric fuzzy number returns its centre, which makes the rules easy to check:

defuzz(fuzzy_number_triangle(2, 5, 8), c("centroid", "gmirl", "sgmrl"))
#>      centroid        gmirl        sgmrl
#> [1,]        5            5            5

Simulating your own data

sim_fuzzy_surv() generates exact event times and then coarsens them the way a record does, so that a study can be validated against a known truth:

set.seed(1)
d <- sim_fuzzy_surv(n = 200, beta = c(age = 0.4, arm = -0.5))
head(d[, c("arm", "fs")])
fit <- fkm(fuzzy_surv(ft, fs) ~ arm, data = d)
plot(fit, type = "diff")

Documentation

vignette("fuzzy-survival", package = "fuzzySurvival") walks through the construction, the estimation, and the caveats. ?fkm, ?fcox, ?flogrank and ?fuzzy_number give the reference.

Caveats

License

MIT

mirror server hosted at Truenetwork, Russian Federation.