Reporting Cumulative Incidence with gtsummary

A cumulative incidence table

The diabetes.complications dataset is the anonymized cohort used in the package overview. Follow-up is measured in years; event 1 is diabetic retinopathy and event 2 is a macrovascular complication. This example reports retinopathy probabilities by fruit-intake group.

library(cifmodeling)
library(gtsummary)
data(diabetes.complications)
report_data <- diabetes.complications
report_data$fruitq1 <- factor(report_data$fruitq1, levels = c(0, 1),
                            labels = c("Other intake", "Low intake"))

curve <- cifcurve(Event(t, epsilon) ~ fruitq1, data = report_data,
                  outcome.type = "competing-risk")
cif_table <- tbl_survfit(curve, times = c(4, 8), type = "risk",
                         label_header = "**{time}-year CIF**")
cif_table
#> Found litedown! Enabling r-universe template
Characteristic 4-year CIF 8-year CIF
fruitq1

    Other intake 18% (15%, 21%) 26% (23%, 30%)
    Low intake 26% (20%, 32%) 35% (28%, 42%)

For competing risks, the survfit-compatible object stores surv = 1 - CIF. The type = "risk" argument displays the CIF and reverses the saved interval limits: the reported limits are 1 - upper and 1 - lower. The table does not re-fit the curve or construct a different confidence interval.

Selecting confidence intervals

Choose inference when fitting the curve. Here IF-based standard errors, Student critical values and a log-log transformation are requested. For the stored 1 - CIF, this transformation is the complementary log-log transformation of the CIF.

student_curve <- cifcurve(Event(t, epsilon) ~ fruitq1, data = report_data,
  outcome.type = "competing-risk", error = "if", conf.dist = "t",
  conf.type = "log-log")
student_table <- tbl_survfit(student_curve, times = c(4, 8), type = "risk",
  label_header = "**{time}-year CIF**") |>
  modify_footnote_header(columns = all_stat_cols(),
    footnote = "95% pointwise IF-t intervals; log-log transformation of 1 - CIF; deletion allocation for degrees of freedom.")
student_table
Characteristic 4-year CIF1 8-year CIF1
fruitq1

    Other intake 18% (15%, 21%) 26% (23%, 30%)
    Low intake 26% (21%, 32%) 35% (30%, 42%)
1 95% pointwise IF-t intervals; log-log transformation of 1 - CIF; deletion allocation for degrees of freedom.

error = "fay" selects the Fay–Graubard-type variance correction, and df.method = "equal" is an alternative allocation for IF-t. Supplied weights are treated as fixed. Details of the inference methods are in ?cifcurve. Degrees of freedom vary by group and time; they are available in a summary without adding diagnostic columns to the publication table.

s <- summary(student_curve, times = c(4, 8), extend = TRUE)
data.frame(group = s$strata, time = s$time, df = s$df,
           interval_status = s$conf.status)
#>                  group time       df interval_status
#> 1 fruitq1=Other intake    4 659.7447              ok
#> 2 fruitq1=Other intake    8 624.5755              ok
#> 3   fruitq1=Low intake    4 227.4970              ok
#> 4   fruitq1=Low intake    8 212.0966              ok

An unavailable interval remains missing. In particular, a valid interval is not carried beyond the last positive-weight follow-up. Exact structural boundaries retain the fitted singleton interval. These are pointwise intervals, not simultaneous confidence bands.

Comparing curves through a specified horizon

The following tests compare the CIF curves through 8 years. They are not separate tests of the probabilities in each column of the preceding table. The score tests emphasize earlier or later differences through their Fleming–Harrington weights. The method labels are taken from the saved fits.

tests <- list(
  gray = ciftest(Event(t, epsilon) ~ fruitq1, report_data,
                 test = "gray", tau = 8),
  early = ciftest(Event(t, epsilon) ~ fruitq1, report_data,
                  test = "score", rho = 1, gamma = 0, tau = 8),
  late = ciftest(Event(t, epsilon) ~ fruitq1, report_data,
                 test = "score", rho = 0, gamma = 1, tau = 8)
)
#> Detected >2 status levels; outcome.type set to 'competing-risk'.
#> Detected >2 status levels; outcome.type set to 'competing-risk'.
#> Detected >2 status levels; outcome.type set to 'competing-risk'.
test_results <- do.call(rbind, lapply(tests, function(fit) {
  x <- generics::tidy(fit)
  data.frame(method = paste(x$method, x$weight), p.value = x$p.value)
}))
test_table <- as_gtsummary(test_results) |>
  modify_header(method = "**Curve comparison through 8 years**",
                p.value = "**p-value**") |>
  modify_fmt_fun(p.value = label_style_pvalue(digits = 3))
test_table
Curve comparison through 8 years p-value
Gray's test unweighted 0.004
Fine-Gray score test custom 0.006
Fine-Gray score test custom 0.038

This recipe reports the selected ciftest() results directly. The usual add_p() default for a survival table is a log-rank test and does not select these CIF tests. The ordinary Gray branch supports integer frequency weights; it does not treat fractional IPTW as frequency weights.

To include the statistic and degrees of freedom, select statistic and parameter from generics::tidy(fit). The latter is the effective rank of the test covariance, distinct from the time-specific Student degrees of freedom used for the curve intervals.

Labels and risk-set counts

Use gtsummary’s existing label, header and formatting arguments to customize the table. For a weighted fit, distinguish the number of observations, the sum of weights and the effective sample size. n.risk.type chooses the quantity exposed as n.risk; the legacy n.risk, n.event and n.censor fields are rounded upward, while n.risk.weighted, n.risk.unweighted and n.risk.ess retain their respective values on the native grid. An event count in this curve object includes both event causes, not only retinopathy.

Exporting the table

The table can be converted with standard gtsummary methods. The temporary paths below can be replaced with the desired output filenames.

html_file <- tempfile(fileext = ".html")
gt::gtsave(as_gt(student_table), filename = html_file)
word_file <- tempfile(fileext = ".docx")
flextable::save_as_docx(as_flex_table(student_table), path = word_file)