From 052792a159c571cc947814ab0b9ba11efcbe64d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 06:34:36 +0000 Subject: [PATCH 1/3] Add km_by_hand_table() and na_by_hand_table() functions Port the Kaplan-Meier and Nelson-Aalen by-hand table builders from ucdavis/epi204 (PR #272) into rme as exported package functions. - km_by_hand_table(): step-by-step KM table (S_prev x factor = S_new) - na_by_hand_table(): Nelson-Aalen table (H_prev + d/n = H_new, exp(-H)) Both return knitr_kable objects of LaTeX math strings. Move dplyr into Imports and add utils (both used by the new functions). https://claude.ai/code/session_011MedxXwDfmV4GnQVDDgzGv --- DESCRIPTION | 3 +- NAMESPACE | 2 + R/km_by_hand_table.R | 132 ++++++++++++++++++++++++++++++++++++++++ R/na_by_hand_table.R | 105 ++++++++++++++++++++++++++++++++ man/km_by_hand_table.Rd | 20 ++++++ man/na_by_hand_table.Rd | 20 ++++++ 6 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 R/km_by_hand_table.R create mode 100644 R/na_by_hand_table.R create mode 100644 man/km_by_hand_table.Rd create mode 100644 man/na_by_hand_table.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 6cffdb3d7d..a3a9165519 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,6 +16,7 @@ Imports: cardx (>= 0.2.4.9003), cvTools, dobson, + dplyr, forcats, fs, glmmTMB, @@ -35,12 +36,12 @@ Imports: stringr, table1, tibble, + utils, webshot2 Suggests: tidyverse, conflicted, dagitty, - dplyr, eha, ggfortify, ggdag, diff --git a/NAMESPACE b/NAMESPACE index 41bd850088..46f21cbd0f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,4 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(km_by_hand_table) +export(na_by_hand_table) importFrom(fs,path_package) importFrom(rlang,.data) diff --git a/R/km_by_hand_table.R b/R/km_by_hand_table.R new file mode 100644 index 0000000000..6d315556c0 --- /dev/null +++ b/R/km_by_hand_table.R @@ -0,0 +1,132 @@ +#' Build a Kaplan-Meier by-hand table +#' +#' Takes a data frame with `time` and `death` columns and returns a +#' `knitr_kable` table showing the time-point, risk set, contribution, and +#' survival estimate at each event or censoring time. +#' +#' @param data A data frame with columns `time` (numeric) and `death` +#' (0/1 integer or logical). +#' +#' @returns A `knitr_kable` object (LaTeX/HTML math strings, `escape = FALSE`). +#' @export +km_by_hand_table <- function(data) { + stopifnot( + "data must have a 'time' column" = "time" %in% names(data), + "data must have a 'death' column" = "death" %in% names(data) + ) + + time <- data$time + status <- data$death + n <- length(time) + + gcd_fn <- function(a, b) { + while (b != 0) { + tmp <- b + b <- a %% b + a <- tmp + } + a + } + + df <- dplyr::tibble(time = time, status = status) |> + dplyr::arrange(time) |> + dplyr::group_by(time) |> + dplyr::summarise( + deaths = sum(status), + censored = sum(1L - as.integer(status)), + .groups = "drop" + ) + + # Number at risk at each time = sample size minus everyone who left + # (via death or censoring) at an *earlier* time point. + left_before <- cumsum(c(0L, utils::head(df$deaths + df$censored, -1L))) + df$n_risk <- n - left_before + + surv_n <- 1L + surv_d <- 1L + prev_surv_str <- "1" + + tbl <- dplyr::tibble( + `Time-point` = integer(nrow(df)), + `Risk set` = integer(nrow(df)), + `Contribution` = character(nrow(df)), + `Survival Estimate` = character(nrow(df)) + ) + + for (i in seq_len(nrow(df))) { + ni <- as.integer(df$n_risk[i]) + di <- as.integer(df$deaths[i]) + cn_raw <- ni - di + + if (cn_raw == ni) { + c_n <- 1L + c_d <- 1L + } else if (cn_raw == 0L) { + c_n <- 0L + c_d <- 1L + } else { + g <- gcd_fn(cn_raw, ni) + c_n <- cn_raw %/% g + c_d <- ni %/% g + } + + surv_n <- surv_n * c_n + surv_d <- surv_d * c_d + if (surv_n > 0L) { + g <- gcd_fn(surv_n, surv_d) + surv_n <- surv_n %/% g + surv_d <- surv_d %/% g + } + + frac_str <- if (c_n == 1L && c_d == 1L) { + "1" + } else if (c_n == 0L) { + "0" + } else { + sprintf("\\frac{%d}{%d}", c_n, c_d) + } + + contrib_cell <- if (di == 0L) { + sprintf( + "$\\left(1 - \\frac{%d}{%d}\\right) = 1$", + di, ni + ) + } else if (c_n == 0L) { + sprintf( + "$\\left(1 - \\frac{%d}{%d}\\right) = 0$", + di, ni + ) + } else { + sprintf( + "$\\left(1 - \\frac{%d}{%d}\\right) = \\frac{%d}{%d} = %s$", + di, ni, c_n, c_d, format(c_n / c_d) + ) + } + + surv_cell <- if (di == 0L) { + sprintf("$%s$", prev_surv_str) + } else if (surv_n == 0L) { + sprintf("$%s \\times %s = 0$", prev_surv_str, frac_str) + } else { + sprintf( + "$%s \\times %s = \\frac{%d}{%d} = %s$", + prev_surv_str, frac_str, surv_n, surv_d, format(surv_n / surv_d) + ) + } + + if (di > 0L) { + prev_surv_str <- if (surv_n == 0L) { + "0" + } else { + sprintf("\\frac{%d}{%d}", surv_n, surv_d) + } + } + + tbl$`Time-point`[i] <- df$time[i] + tbl$`Risk set`[i] <- ni + tbl$`Contribution`[i] <- contrib_cell + tbl$`Survival Estimate`[i] <- surv_cell + } + + knitr::kable(tbl, escape = FALSE) +} diff --git a/R/na_by_hand_table.R b/R/na_by_hand_table.R new file mode 100644 index 0000000000..5cc430548d --- /dev/null +++ b/R/na_by_hand_table.R @@ -0,0 +1,105 @@ +#' Build a Nelson-Aalen by-hand table +#' +#' Takes a data frame with `time` and `death` columns and returns a +#' `knitr_kable` table showing the time-point, risk set, hazard increment, +#' cumulative hazard, and Nelson-Aalen survival estimate at each time point. +#' +#' @param data A data frame with columns `time` (numeric) and `death` +#' (0/1 integer or logical). +#' +#' @returns A `knitr_kable` object (LaTeX/HTML math strings, `escape = FALSE`). +#' @export +na_by_hand_table <- function(data) { + stopifnot( + "data must have a 'time' column" = "time" %in% names(data), + "data must have a 'death' column" = "death" %in% names(data) + ) + + time <- data$time + status <- data$death + n <- length(time) + + gcd_fn <- function(a, b) { + while (b != 0) { + tmp <- b + b <- a %% b + a <- tmp + } + a + } + + df <- dplyr::tibble(time = time, status = status) |> + dplyr::arrange(time) |> + dplyr::group_by(time) |> + dplyr::summarise( + deaths = sum(status), + censored = sum(1L - as.integer(status)), + .groups = "drop" + ) + + # Number at risk at each time = sample size minus everyone who left + # (via death or censoring) at an *earlier* time point. + left_before <- cumsum(c(0L, utils::head(df$deaths + df$censored, -1L))) + df$n_risk <- n - left_before + + cum_haz_n <- 0L + cum_haz_d <- 1L + cum_haz_str <- "0" + + tbl <- dplyr::tibble( + `Time-point` = integer(nrow(df)), + `Risk set` = integer(nrow(df)), + `Hazard increment` = character(nrow(df)), + `Cumulative hazard` = character(nrow(df)), + `NA Survival` = character(nrow(df)) + ) + + for (i in seq_len(nrow(df))) { + ni <- as.integer(df$n_risk[i]) + di <- as.integer(df$deaths[i]) + + inc_str <- if (di == 0L) { + sprintf("$\\frac{0}{%d} = 0$", ni) + } else { + sprintf("$\\frac{%d}{%d}$", di, ni) + } + + prev_cum_haz_str <- cum_haz_str + + if (di > 0L) { + new_num <- cum_haz_n * ni + di * cum_haz_d + new_den <- cum_haz_d * ni + g <- gcd_fn(new_num, new_den) + cum_haz_n <- new_num %/% g + cum_haz_d <- new_den %/% g + + cum_haz_str <- if (cum_haz_d == 1L) { + as.character(cum_haz_n) + } else { + sprintf("\\frac{%d}{%d}", cum_haz_n, cum_haz_d) + } + + cum_haz_cell <- sprintf( + "$%s + \\frac{%d}{%d} = %s$", + prev_cum_haz_str, di, ni, cum_haz_str + ) + } else { + cum_haz_cell <- sprintf("$%s$", cum_haz_str) + } + + surv_val <- exp(-cum_haz_n / cum_haz_d) + surv_cell <- sprintf( + "$\\expf{-%s} \\approx %s$", + cum_haz_str, + format(surv_val) + ) + + tbl$`Time-point`[i] <- df$time[i] + tbl$`Risk set`[i] <- ni + tbl$`Hazard increment`[i] <- inc_str + tbl$`Cumulative hazard`[i] <- cum_haz_cell + tbl$`NA Survival`[i] <- surv_cell + } + + knitr::kable(tbl, escape = FALSE) +} diff --git a/man/km_by_hand_table.Rd b/man/km_by_hand_table.Rd new file mode 100644 index 0000000000..02da162a89 --- /dev/null +++ b/man/km_by_hand_table.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/km_by_hand_table.R +\name{km_by_hand_table} +\alias{km_by_hand_table} +\title{Build a Kaplan-Meier by-hand table} +\usage{ +km_by_hand_table(data) +} +\arguments{ +\item{data}{A data frame with columns \code{time} (numeric) and \code{death} +(0/1 integer or logical).} +} +\value{ +A \code{knitr_kable} object (LaTeX/HTML math strings, \code{escape = FALSE}). +} +\description{ +Takes a data frame with \code{time} and \code{death} columns and returns a +\code{knitr_kable} table showing the time-point, risk set, contribution, and +survival estimate at each event or censoring time. +} diff --git a/man/na_by_hand_table.Rd b/man/na_by_hand_table.Rd new file mode 100644 index 0000000000..c6f8d36c92 --- /dev/null +++ b/man/na_by_hand_table.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/na_by_hand_table.R +\name{na_by_hand_table} +\alias{na_by_hand_table} +\title{Build a Nelson-Aalen by-hand table} +\usage{ +na_by_hand_table(data) +} +\arguments{ +\item{data}{A data frame with columns \code{time} (numeric) and \code{death} +(0/1 integer or logical).} +} +\value{ +A \code{knitr_kable} object (LaTeX/HTML math strings, \code{escape = FALSE}). +} +\description{ +Takes a data frame with \code{time} and \code{death} columns and returns a +\code{knitr_kable} table showing the time-point, risk set, hazard increment, +cumulative hazard, and Nelson-Aalen survival estimate at each time point. +} From 1259e7a0f2c643de370c1188b8beea66e16800fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 07:52:51 +0000 Subject: [PATCH 2/3] Address review: fix truncation, sci-notation, overflow; validate death; add examples - Time-point columns initialised as numeric() not integer() (preserve non-integer times) - Replace format() with fixed-point formatC() in math strings (avoid LaTeX-breaking scientific notation near zero) - Track running fraction numerators/denominators as doubles (avoid 32-bit integer overflow) - Validate death is 0/1 or logical - Add runnable @examples to both functions - Document na_by_hand_table()'s \expf macro dependency in @details https://claude.ai/code/session_011MedxXwDfmV4GnQVDDgzGv --- R/km_by_hand_table.R | 45 ++++++++++++++++++++++++++--------------- R/na_by_hand_table.R | 33 ++++++++++++++++++++++++------ man/km_by_hand_table.Rd | 4 ++++ man/na_by_hand_table.Rd | 11 ++++++++++ 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/R/km_by_hand_table.R b/R/km_by_hand_table.R index 6d315556c0..9bf835dbe5 100644 --- a/R/km_by_hand_table.R +++ b/R/km_by_hand_table.R @@ -9,10 +9,14 @@ #' #' @returns A `knitr_kable` object (LaTeX/HTML math strings, `escape = FALSE`). #' @export +#' @examples +#' d <- data.frame(time = c(1, 2, 3, 3, 5), death = c(1, 0, 1, 1, 0)) +#' km_by_hand_table(d) km_by_hand_table <- function(data) { stopifnot( "data must have a 'time' column" = "time" %in% names(data), - "data must have a 'death' column" = "death" %in% names(data) + "data must have a 'death' column" = "death" %in% names(data), + "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)) ) time <- data$time @@ -28,6 +32,13 @@ km_by_hand_table <- function(data) { a } + # `format()` falls back to scientific notation for small values (e.g. + # `format(1e-6)` -> "1e-06"), which is invalid inside a `$...$` math + # environment; this always produces fixed-point output. + fmt_dec <- function(x) { + formatC(x, format = "f", digits = 4, drop0trailing = TRUE) + } + df <- dplyr::tibble(time = time, status = status) |> dplyr::arrange(time) |> dplyr::group_by(time) |> @@ -42,12 +53,14 @@ km_by_hand_table <- function(data) { left_before <- cumsum(c(0L, utils::head(df$deaths + df$censored, -1L))) df$n_risk <- n - left_before - surv_n <- 1L - surv_d <- 1L + # Running survival numerator/denominator are doubles so that whole-number + # products beyond .Machine$integer.max stay exact instead of overflowing. + surv_n <- 1 + surv_d <- 1 prev_surv_str <- "1" tbl <- dplyr::tibble( - `Time-point` = integer(nrow(df)), + `Time-point` = numeric(nrow(df)), `Risk set` = integer(nrow(df)), `Contribution` = character(nrow(df)), `Survival Estimate` = character(nrow(df)) @@ -59,11 +72,11 @@ km_by_hand_table <- function(data) { cn_raw <- ni - di if (cn_raw == ni) { - c_n <- 1L - c_d <- 1L + c_n <- 1 + c_d <- 1 } else if (cn_raw == 0L) { - c_n <- 0L - c_d <- 1L + c_n <- 0 + c_d <- 1 } else { g <- gcd_fn(cn_raw, ni) c_n <- cn_raw %/% g @@ -72,15 +85,15 @@ km_by_hand_table <- function(data) { surv_n <- surv_n * c_n surv_d <- surv_d * c_d - if (surv_n > 0L) { + if (surv_n > 0) { g <- gcd_fn(surv_n, surv_d) surv_n <- surv_n %/% g surv_d <- surv_d %/% g } - frac_str <- if (c_n == 1L && c_d == 1L) { + frac_str <- if (c_n == 1 && c_d == 1) { "1" - } else if (c_n == 0L) { + } else if (c_n == 0) { "0" } else { sprintf("\\frac{%d}{%d}", c_n, c_d) @@ -91,7 +104,7 @@ km_by_hand_table <- function(data) { "$\\left(1 - \\frac{%d}{%d}\\right) = 1$", di, ni ) - } else if (c_n == 0L) { + } else if (c_n == 0) { sprintf( "$\\left(1 - \\frac{%d}{%d}\\right) = 0$", di, ni @@ -99,23 +112,23 @@ km_by_hand_table <- function(data) { } else { sprintf( "$\\left(1 - \\frac{%d}{%d}\\right) = \\frac{%d}{%d} = %s$", - di, ni, c_n, c_d, format(c_n / c_d) + di, ni, c_n, c_d, fmt_dec(c_n / c_d) ) } surv_cell <- if (di == 0L) { sprintf("$%s$", prev_surv_str) - } else if (surv_n == 0L) { + } else if (surv_n == 0) { sprintf("$%s \\times %s = 0$", prev_surv_str, frac_str) } else { sprintf( "$%s \\times %s = \\frac{%d}{%d} = %s$", - prev_surv_str, frac_str, surv_n, surv_d, format(surv_n / surv_d) + prev_surv_str, frac_str, surv_n, surv_d, fmt_dec(surv_n / surv_d) ) } if (di > 0L) { - prev_surv_str <- if (surv_n == 0L) { + prev_surv_str <- if (surv_n == 0) { "0" } else { sprintf("\\frac{%d}{%d}", surv_n, surv_d) diff --git a/R/na_by_hand_table.R b/R/na_by_hand_table.R index 5cc430548d..fd999f0459 100644 --- a/R/na_by_hand_table.R +++ b/R/na_by_hand_table.R @@ -7,12 +7,23 @@ #' @param data A data frame with columns `time` (numeric) and `death` #' (0/1 integer or logical). #' +#' @details +#' The "NA Survival" column is rendered with the `\expf` macro from the +#' book's `latex-macros/macros.qmd`, so this function is intended for use +#' inside the `rme` book render pipeline. Outside that environment (a bare +#' `knitr` document or `pkgdown` page) define `\expf` or that column will +#' show an undefined control sequence. +#' #' @returns A `knitr_kable` object (LaTeX/HTML math strings, `escape = FALSE`). #' @export +#' @examples +#' d <- data.frame(time = c(1, 2, 3, 3, 5), death = c(1, 0, 1, 1, 0)) +#' na_by_hand_table(d) na_by_hand_table <- function(data) { stopifnot( "data must have a 'time' column" = "time" %in% names(data), - "data must have a 'death' column" = "death" %in% names(data) + "data must have a 'death' column" = "death" %in% names(data), + "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)) ) time <- data$time @@ -28,6 +39,13 @@ na_by_hand_table <- function(data) { a } + # `format()` falls back to scientific notation for small values (e.g. + # `format(1e-6)` -> "1e-06"), which is invalid inside a `$...$` math + # environment; this always produces fixed-point output. + fmt_dec <- function(x) { + formatC(x, format = "f", digits = 4, drop0trailing = TRUE) + } + df <- dplyr::tibble(time = time, status = status) |> dplyr::arrange(time) |> dplyr::group_by(time) |> @@ -42,12 +60,15 @@ na_by_hand_table <- function(data) { left_before <- cumsum(c(0L, utils::head(df$deaths + df$censored, -1L))) df$n_risk <- n - left_before - cum_haz_n <- 0L - cum_haz_d <- 1L + # Running cumulative-hazard numerator/denominator are doubles so that + # whole-number products beyond .Machine$integer.max stay exact instead + # of overflowing. + cum_haz_n <- 0 + cum_haz_d <- 1 cum_haz_str <- "0" tbl <- dplyr::tibble( - `Time-point` = integer(nrow(df)), + `Time-point` = numeric(nrow(df)), `Risk set` = integer(nrow(df)), `Hazard increment` = character(nrow(df)), `Cumulative hazard` = character(nrow(df)), @@ -73,7 +94,7 @@ na_by_hand_table <- function(data) { cum_haz_n <- new_num %/% g cum_haz_d <- new_den %/% g - cum_haz_str <- if (cum_haz_d == 1L) { + cum_haz_str <- if (cum_haz_d == 1) { as.character(cum_haz_n) } else { sprintf("\\frac{%d}{%d}", cum_haz_n, cum_haz_d) @@ -91,7 +112,7 @@ na_by_hand_table <- function(data) { surv_cell <- sprintf( "$\\expf{-%s} \\approx %s$", cum_haz_str, - format(surv_val) + fmt_dec(surv_val) ) tbl$`Time-point`[i] <- df$time[i] diff --git a/man/km_by_hand_table.Rd b/man/km_by_hand_table.Rd index 02da162a89..aea357ea62 100644 --- a/man/km_by_hand_table.Rd +++ b/man/km_by_hand_table.Rd @@ -18,3 +18,7 @@ Takes a data frame with \code{time} and \code{death} columns and returns a \code{knitr_kable} table showing the time-point, risk set, contribution, and survival estimate at each event or censoring time. } +\examples{ +d <- data.frame(time = c(1, 2, 3, 3, 5), death = c(1, 0, 1, 1, 0)) +km_by_hand_table(d) +} diff --git a/man/na_by_hand_table.Rd b/man/na_by_hand_table.Rd index c6f8d36c92..f8670766fc 100644 --- a/man/na_by_hand_table.Rd +++ b/man/na_by_hand_table.Rd @@ -18,3 +18,14 @@ Takes a data frame with \code{time} and \code{death} columns and returns a \code{knitr_kable} table showing the time-point, risk set, hazard increment, cumulative hazard, and Nelson-Aalen survival estimate at each time point. } +\details{ +The "NA Survival" column is rendered with the \verb{\\expf} macro from the +book's \code{latex-macros/macros.qmd}, so this function is intended for use +inside the \code{rme} book render pipeline. Outside that environment (a bare +\code{knitr} document or \code{pkgdown} page) define \verb{\\expf} or that column will +show an undefined control sequence. +} +\examples{ +d <- data.frame(time = c(1, 2, 3, 3, 5), death = c(1, 0, 1, 1, 0)) +na_by_hand_table(d) +} From 1468c24bc326dc472d60e49a0632a88d694d8670 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 23:39:35 +0000 Subject: [PATCH 3/3] Address review: guard NA times; use %.0f for double numerators/denominators - Add stopifnot guard rejecting NA in 'time' (NA rows sort last but still count toward n, inflating the risk set) - Format double-valued running numerators/denominators with %.0f instead of %d (avoids coercion warnings under options(warn=1) and float-rounding truncation); keep %d for the genuine-integer di/ni - Replace as.character(cum_haz_n) with sprintf("%.0f", ...) to avoid scientific notation for large integer hazard numerators https://claude.ai/code/session_011MedxXwDfmV4GnQVDDgzGv --- R/km_by_hand_table.R | 11 ++++++----- R/na_by_hand_table.R | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/R/km_by_hand_table.R b/R/km_by_hand_table.R index 9bf835dbe5..6280c7ab99 100644 --- a/R/km_by_hand_table.R +++ b/R/km_by_hand_table.R @@ -16,7 +16,8 @@ km_by_hand_table <- function(data) { stopifnot( "data must have a 'time' column" = "time" %in% names(data), "data must have a 'death' column" = "death" %in% names(data), - "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)) + "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)), + "'time' must not contain NA" = !anyNA(data$time) ) time <- data$time @@ -96,7 +97,7 @@ km_by_hand_table <- function(data) { } else if (c_n == 0) { "0" } else { - sprintf("\\frac{%d}{%d}", c_n, c_d) + sprintf("\\frac{%.0f}{%.0f}", c_n, c_d) } contrib_cell <- if (di == 0L) { @@ -111,7 +112,7 @@ km_by_hand_table <- function(data) { ) } else { sprintf( - "$\\left(1 - \\frac{%d}{%d}\\right) = \\frac{%d}{%d} = %s$", + "$\\left(1 - \\frac{%d}{%d}\\right) = \\frac{%.0f}{%.0f} = %s$", di, ni, c_n, c_d, fmt_dec(c_n / c_d) ) } @@ -122,7 +123,7 @@ km_by_hand_table <- function(data) { sprintf("$%s \\times %s = 0$", prev_surv_str, frac_str) } else { sprintf( - "$%s \\times %s = \\frac{%d}{%d} = %s$", + "$%s \\times %s = \\frac{%.0f}{%.0f} = %s$", prev_surv_str, frac_str, surv_n, surv_d, fmt_dec(surv_n / surv_d) ) } @@ -131,7 +132,7 @@ km_by_hand_table <- function(data) { prev_surv_str <- if (surv_n == 0) { "0" } else { - sprintf("\\frac{%d}{%d}", surv_n, surv_d) + sprintf("\\frac{%.0f}{%.0f}", surv_n, surv_d) } } diff --git a/R/na_by_hand_table.R b/R/na_by_hand_table.R index fd999f0459..a2a967ec48 100644 --- a/R/na_by_hand_table.R +++ b/R/na_by_hand_table.R @@ -23,7 +23,8 @@ na_by_hand_table <- function(data) { stopifnot( "data must have a 'time' column" = "time" %in% names(data), "data must have a 'death' column" = "death" %in% names(data), - "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)) + "'death' must be 0/1 or logical" = all(data$death %in% c(0, 1)), + "'time' must not contain NA" = !anyNA(data$time) ) time <- data$time @@ -95,9 +96,9 @@ na_by_hand_table <- function(data) { cum_haz_d <- new_den %/% g cum_haz_str <- if (cum_haz_d == 1) { - as.character(cum_haz_n) + sprintf("%.0f", cum_haz_n) } else { - sprintf("\\frac{%d}{%d}", cum_haz_n, cum_haz_d) + sprintf("\\frac{%.0f}{%.0f}", cum_haz_n, cum_haz_d) } cum_haz_cell <- sprintf(