# k-Sample tests for left-truncated and right-censored survival data

# Quick example

    ltrc.data <- data.frame(
      ltrc.sim(n = 500, rtrunc = rnorm, trunc.args = list(4, 1),
               rtarget = rnorm, target.args = list(4, 1),
               rcens = rexp, cens.args = list(1), seed = 1212),
      group = rep(1:2, each = 250)
    )

    test <- ltrc.test(ltrc.data, tests = "ks", weights = rep(1/2, 2), B = 200, plot.curves = TRUE)

    test$statistic
    #> $ks
    #> [1] 3.557636
    test$p.value
    #> $ks
    #> [1] 0.1940299

# Introduction

This package implements tests for the \(k\)-sample problem under left truncation and/or right censoring. It provides

- Omnibus tests for the \(k\)-sample problem for data of that type.
- Classical and weighted log-rank-type tests.
- Maximum likelihood estimators of the survival (and distribution) functions.
- Simulation tools for left-truncated and/or right-censored data.

In this vignette, we focus on the case of left-truncated and right-censored (LTRC) data. The functions for only left truncation or only right censoring follow the same structure and are not shown separately.

The methodology implemented here is described in detail in Lago, de Uña-Alvarez and Pardo-Fernandez (2025) and Lago, Pardo-Fernandez and de Uña-Alvarez (2026); see DESCRIPTION for the full references.

# Notation and data structure

The package uses the following variables:

- `entry`: truncation time \(U\)
- `time`: observed time \(Y = \min(X, C)\)
- `status`: event indicator \(\Delta\)
- `group`: group label

Data must be provided as a data frame (or matrix) with these columns, in this order. Functions that take a single combined data frame (such as `ltrc.test`) identify columns by position rather than by name; the output of `ltrc.sim` follows the same convention but labels the event indicator column `delta` rather than `status` — both refer to the same quantity \(\Delta\).

# Simulating LTRC data

The user only needs to specify the sample size of the simulated sample and the distributions of the truncation, target, and censoring variables through the corresponding standard R generators (`rnorm`, `rweibull`, `runif`, etc.), along with their parameters supplied as a list. The only requirement is that the truncation, target, and censoring variables be strictly positive.

    ltrc.data <- ksamplesLTRC::ltrc.sim(n = 500, rtrunc = rnorm, trunc.args = list(4, 1),
                     rtarget = rnorm, target.args = list(4, 1),
                     rcens = rexp, cens.args = list(1), seed = 12)
    head(ltrc.data)
    #>          u        y delta
    #> 1 3.043256 3.714960     0
    #> 2 3.079995 4.490035     1
    #> 3 2.002358 2.500780     0
    #> 4 3.893536 4.713094     0
    #> 5 4.428015 4.504474     0
    #> 6 3.222280 3.475242     0
    mean(ltrc.data[, 1] <= ltrc.data[, 2])
    #> [1] 1
    table(ltrc.data$delta)
    #>
    #>   0   1
    #> 291 209

# Estimating the survival function

Despite testing being the main goal of this package, we also include the estimators used internally by the testing procedures.

    ltrc.data <- ksamplesLTRC::ltrc.sim(1000, rnorm, trunc.args = list(4, 1),
                                         rnorm, target.args = list(4, 1),
                                         rexp, cens.args = list(1))

    est <- ltrc.estimator(o.sample = ltrc.data, conf.int = TRUE, conf.type = 'loglog')
    plot(est, col = 'forestgreen', xlab = 't', ylab = 'Estimated survival probability')
    curve(1 - pnorm(x, 4, 1), add = TRUE)

    str(est)
    #> List of 6
    #>  $ fail.time    : num [1:428] 2.04 2.24 2.34 2.44 2.51 ...
    #>  $ estimation   : num [1:428] 0.969 0.948 0.933 0.919 0.908 ...
    #>  $ prob         : num [1:428] 0.0312 0.0211 0.0146 0.0137 0.0111 ...
    #>  $ conf.limits  : num [1:428, 1:2] 0.798 0.803 0.799 0.79 0.783 ...
    #>   ..- attr(*, "dimnames")=List of 2
    #>   .. ..$ : NULL
    #>   .. ..$ : chr [1:2] "var.i.l" "var.i.u"
    #>  $ conf.limits.F: num [1:428, 1:2] 0.00446 0.01305 0.02109 0.02956 0.0371 ...
    #>   ..- attr(*, "dimnames")=List of 2
    #>   .. ..$ : NULL
    #>   .. ..$ : chr [1:2] "varF.l" "varF.u"
    #>  $ hole         : NULL
    #>  - attr(*, "class")= chr "estimators"

1. `$fail.time`: the ordered, distinct observed event times
2. `$estimation`: the estimated survival probability at each observed event time
3. `$prob`: the jumps of the estimator at each observed event time
4. `$conf.limits`: the lower and upper values of the confidence band for the survival function
5. `$conf.limits.F`: the same limits for the distribution function

# Handling holes

The functions of this package also contain an input to deal with holes when data are subject to left truncation (with or without censoring). Holes are event times with a unitary risk set, which would make the Lynden-Bell-type estimator degenerate. There are three possibilities:

- `"holes"` (default): no correction; a warning is issued.
- `"add"`: increases the risk set by 1 at the hole.
- `"conditional"`: discards observations and conditions the sample on event times larger than the largest hole.

For illustration, we use the well-known Channing House dataset. This dataset presents a hole at time \(t = 781\) in the male group. Below is the resulting estimated survival function when `holes = 'holes'` (red), `holes = 'add'` (green), and `holes = 'conditional'` (blue).

    data("channing", package = "KMsurv")
    o.sample <- data.frame(entry = channing$ageentry, time = channing$age,
                            status = channing$death, group = channing$gender)

    o.sample <- subset(o.sample, group == 1)
    head(o.sample)
    #>     entry time status group
    #> 366   782  909      1     1
    #> 367  1020 1128      1     1
    #> 368   856  969      1     1
    #> 369   915  957      1     1
    #> 370   863  983      1     1
    #> 371   906 1012      1     1
    plot(ksamplesLTRC::ltrc.estimator(o.sample, holes = 'holes'), col = 2,
         xlab = 't', ylab = 'Estimated survival probability')
    lines(ksamplesLTRC::ltrc.estimator(o.sample, holes = 'add'), col = 3)
    lines(ksamplesLTRC::ltrc.estimator(o.sample, holes = 'conditional'), col = 4)

When data are subject to left truncation or right censoring only, the matrix or data frame must contain only two columns: truncation and event times in the former case, and event times and censoring indicators in the latter. The survival function estimator for left-truncated data also offers the hole-handling options described above; the right-censored-only estimator does not, since that phenomenon does not occur in that framework.

# \(k\)-sample tests

We aim to test whether the \(k\) target variables follow the same distribution across the \(k\) populations or not. The tests are based on comparing each group-specific estimator \(F_{n_j}\) against a pooled estimator

\[ F_n(t) = \sum_{j=1}^k \varphi_j F_{n_j}(t), \]

where \(\varphi_1,\dots,\varphi_k\) are user-specified weights satisfying \(\varphi_j > 0\) and \(\sum_{j=1}^k \varphi_j = 1\).

There are two options, as the user may perform the tests with either of the two main functions of this package.

1. The user may perform the tests via `ltrc.test`. Columns must appear in this order: `entry`, `time`, `status`, `group`. This function requires the weights \(w_1,\dots,w_k\) used in \(F_n\) above, supplied as a vector with strictly positive components summing to 1. The user also chooses one or more of `tests = c("ks", "cvm", "logrank")`. If `ks` and/or `cvm` are requested, the bootstrap p-value is approximated using `B` replications (default `B = 500`); this argument need not be set explicitly unless a different number of replications is desired. Weighted log-rank-type (Fleming and Harrington) tests are also available: the choice \(p = 0 = q\) returns the classical log-rank test, while other choices of \(p\) and \(q\) (scalars or vectors) assign more weight to discrepancies in early, mid, or late event times. This function also supports the `holes` argument described above.

   When `holes = 'conditional'` is used with more than one group, all \(k\) groups are conditioned jointly on event times larger than the largest hole found across all \(k\) samples, and not on each group’s own largest hole. This guarantees that the KS and CvM statistics are computed on a common, hole-free time window across groups; conditioning each group separately would compare estimators defined on different supports.

    data("channing", package = "KMsurv")
    data.channing <- data.frame(entry = channing$ageentry, time = channing$age,
                                 status = channing$death, group = channing$gender)
    aa <- ksamplesLTRC::ltrc.test(data.channing, weights = rep(1/2, 2), holes = 'conditional',
                                   tests = c('ks', 'cvm', 'logrank'), plot.curves = TRUE,
                                   p = c(0, 1, 0.5, 0), q = c(0, 0, 0.5, 1)) # equal weights for two groups

    aa$p.value
    #> $ks
    #> [1] 0.5708583
    #>
    #> $cvm
    #> [1] 0.6047904
    aa$logrank
    #>              statistic    p.value
    #> p=0, q=0      2.285075 0.13062379
    #> p=1, q=0      2.814105 0.09343901
    #> p=0.5, q=0.5  1.890058 0.16919541
    #> p=0, q=1      1.177968 0.27776998

2. The user may perform the tests via `ksample.test`. This function differs from `ltrc.test` in only one aspect: instead of passing a single matrix or data frame, the user passes `time`, `entry` (when applicable), `status` (when applicable), and `group` as separate vectors. The function detects which type of data (left-truncated, right-censored, or both) the user has, and performs the tests accordingly. The same global-conditioning behavior under `holes = 'conditional'` applies here.

    data("channing", package = "KMsurv")
    bb <- ksamplesLTRC::ksample.test(time = channing$age, entry = channing$ageentry,
                                      status = channing$death, group = channing$gender,
                                      weights = rep(1/2, 2), B = 500, holes = 'conditional',
                                      tests = c('ks', 'cvm', 'logrank'), plot.curves = TRUE,
                                      p = c(0, 1, 0.5, 0), q = c(0, 0, 0.5, 1)) # equal weights for two groups

    bb$p.value
    #> $ks
    #> [1] 0.5988024
    #>
    #> $cvm
    #> [1] 0.6347305
    bb$logrank
    #>              statistic    p.value
    #> p=0, q=0      2.285075 0.13062379
    #> p=1, q=0      2.814105 0.09343901
    #> p=0.5, q=0.5  1.890058 0.16919541
    #> p=0, q=1      1.177968 0.27776998

## Single-test shortcuts

If only the KS or only the CvM test is of interest, `ltrc.pv.ks` and `ltrc.pv.cvm` compute the statistic and its bootstrap p-value directly, without running the other tests:

    ksamplesLTRC::ltrc.pv.ks(data.channing, weights = rep(1/2, 2), B = 200, holes = 'conditional')$pvalue
    #> [1] 0.5870647

These are equivalent to calling `ltrc.test` with `tests = "ks"` (respectively `"cvm"`), and are provided for convenience when the other tests are not needed.

# Left truncation and right censoring special cases

Although the discussion above focused on left-truncated and right-censored data, the package includes functions for only left-truncated or only right-censored data. The structure of the inputs is the same in all three cases, except for the trivial fact that holes do not arise under right censoring. The functions follow the same naming convention as those for LTRC data, with different prefixes: for instance, `ltrc.estimator` returns the MLE of the survival function under left truncation and right censoring, `lt.estimator` returns the analogous estimator under left truncation only, and `rc.estimator` does so for right-censored-only data. The same convention applies to the testing and simulation functions.

# Practical recommendations

Following the results in the literature, we recommend the following:

1. Use the log-rank test when the proportional hazards assumption is guaranteed to hold.
2. Use weighted log-rank tests when looking for discrepancies in specific parts of the support of the target variables.
3. Use Kolmogorov-Smirnov and Cramér-von Mises tests when no information is available about the alternative hypothesis.

# Summary

This package provides tools for testing equality of survival distributions under left truncation and/or right censoring, including log-rank, weighted log-rank, and omnibus (Kolmogorov–Smirnov and Cramér–von Mises) tests.