Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export(define_observation)
export(define_parameter)
export(define_plan)
export(define_population)
export(format_number)
export(get_label)
export(meta_adam)
export(meta_add_total)
Expand All @@ -44,6 +45,7 @@ export(meta_split)
export(n_subject)
export(outdata)
export(plan)
export(round_half_away_from_zero)
export(spec_analysis_population)
export(spec_call_program)
export(spec_filename)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# metalite 0.1.4

- Add SAS-compatible rounding and fixed-decimal formatting helpers, and use
them for `collect_n_subject()` display values.
- Fix bug of `n_subject()` for empty factor.
- Add default mapping for subject level analysis.
- Update GitHub Actions workflows.
Expand Down
15 changes: 12 additions & 3 deletions R/collect_n_subject.R
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ meta_remove_blank_group <- function(meta,
#' @param decimal_places_summary Number of decimal places to be displayed in statistical summary values (Mean, SD, Median, Min, Max, Q1 and Q3). Default is 1.
#' @param decimal_places_percent Number of decimal places to be displayed in percentage values. Default is 1.
#'
#' @details
#' Summary statistics and percentages are calculated without rounding, then
#' rounded once at the display boundary with [round_half_away_from_zero()].
#' Decimal ties are rounded away from zero and trailing zeros are retained.
#'
#' @return A list containing number of subjects and its subset condition.
#'
#' @export
Expand Down Expand Up @@ -245,7 +250,7 @@ collect_n_subject <- function(meta,
q1 = stats::quantile(x, probs = 0.25, na.rm = TRUE, type = quantile_method, names = FALSE),
q3 = stats::quantile(x, probs = 0.75, na.rm = TRUE, type = quantile_method, names = FALSE)
)
value <- formatC(value, format = "f", digits = decimal_places_summary)
value <- format_number(value, digits = decimal_places_summary)
c(gluestick("{value[['mean']]} ({value[['sd']]})"), gluestick("{value[['median']]} [{value[['min']]}, {value[['max']]}]"), gluestick("{value[['q1']]} to {value[['q3']]}"))
})
pop_num <- data.frame(
Expand All @@ -260,7 +265,9 @@ collect_n_subject <- function(meta,
pop_tmp <- pop_n
for (i in seq(names(pop_n))) {
if ("integer" %in% class(pop_n[[i]])) {
pct <- formatC(pop_n[[i]] / pop_all[[i]] * 100, format = "f", digits = decimal_places_percent, width = 5)
pct <- format_number(pop_n[[i]] / pop_all[[i]] * 100,
digits = decimal_places_percent, width = 5
)
pop_tmp[[i]] <- gluestick("{pop_n[[i]]} ({pct}%)")
}
}
Expand Down Expand Up @@ -301,7 +308,9 @@ collect_n_subject <- function(meta,

for (i in seq(names(pop_tmp))) {
if ("integer" %in% class(pop_tmp[[i]])) {
pct <- formatC(pop_tmp[[i]] / pop_all[[i]] * 100, format = "f", digits = decimal_places_percent, width = 5)
pct <- format_number(pop_tmp[[i]] / pop_all[[i]] * 100,
digits = decimal_places_percent, width = 5
)
pop_tmp[[i]] <- gluestick("{pop_tmp[[i]]} ({pct}%)")
}
}
Expand Down
9 changes: 5 additions & 4 deletions R/meta_inherit.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@
#'
#' @export
meta_inherit <- function(
meta,
inherit,
name,
overwrite = FALSE) {
meta,
inherit,
name,
overwrite = FALSE
) {
mapping <- list()
for (i in seq_along(name)) {
x <- collect_adam_mapping(inherit, name[i])
Expand Down
123 changes: 123 additions & 0 deletions R/rounding.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright (c) 2023 Merck & Co., Inc., Rahway, NJ, USA and its affiliates.
# All rights reserved.
#
# This file is part of the metalite program.
#
# metalite is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

#' Round half away from zero
#'
#' Round numeric values to a given number of decimal places, with decimal
#' ties (for example, 1.25 at `digits = 1`) rounded half away from zero.
#' This differs from [base::round()], which uses round-to-even for ties.
#' Values that round to zero, including small negative values, return positive
#' zero so formatted output does not display negative zero.
#'
#' To account for floating-point representation, values within
#' `sqrt(.Machine$double.eps)` below a tie at the requested precision are
#' treated as ties. The implementation is adapted from `roundSAS()` in
#' [pharmaverse/tidytlg](https://github.com/pharmaverse/tidytlg/blob/5f169c76428976f53d2af9e7fe52460348ef6cb7/R/roundSAS.R).
#'
#' @param x A numeric vector, matrix, array, or data frame with only numeric
#' columns.
#' @param digits A finite, integer-valued scalar giving the number of decimal
#' places. Negative values round to positions left of the decimal point.
#'
#' @return A numeric object with the same dimensions, dimension names, and
#' names as `x`. A data frame input returns a data frame.
#'
#' @export
#'
#' @examples
#' round_half_away_from_zero(c(1.25, -1.25), digits = 1)
#' round_half_away_from_zero(c(-0.04, NA), digits = 1)
round_half_away_from_zero <- function(x, digits = 0) {
if (!is.numeric(digits) || length(digits) != 1L || is.na(digits) ||
!is.finite(digits) || digits %% 1 != 0) {
stop("`digits` must be one finite integer.", call. = FALSE)
}

if (is.data.frame(x)) {
if (!all(vapply(x, is.numeric, logical(1)))) {
stop("All columns of `x` must be numeric.", call. = FALSE)
}

x[] <- lapply(x, function(col) round_half_away_from_zero(col, digits = digits))
return(x)
}

if (!is.numeric(x)) {
stop("`x` must be numeric.", call. = FALSE)
}

n <- names(x)
d <- dim(x)
dn <- dimnames(x)

posneg <- sign(x)
z <- abs(x) * 10^digits
Comment thread
elong0527 marked this conversation as resolved.
Outdated
z <- z + 0.5 + sqrt(.Machine$double.eps)
z <- trunc(z)
z <- z / 10^digits
z <- ifelse(!is.na(z) & z > 0, z * posneg, z)

dim(z) <- d
dimnames(z) <- dn
names(z) <- n

z
}

#' Format numbers with fixed decimal places
#'
#' Round with [round_half_away_from_zero()] and format with a fixed number of
#' decimal places. Decimal ties round half away from zero, and values that
#' round to zero display as positive zero (for example, `"0.0"` rather than
#' `"-0.0"`).
#'
#' @param x A numeric vector.
#' @param digits A non-negative, integer-valued scalar giving the number of
#' decimal places.
#' @param width `NULL`, or a non-negative, integer-valued scalar giving the
#' minimum field width passed to [base::formatC()]. The default, `NULL`, does
#' not set a minimum width.
#'
#' @return A character vector containing the formatted values.
#'
#' @export
#'
#' @examples
#' format_number(c(1.25, -1.25), digits = 1)
#' format_number(c(6.25, -0.04), digits = 1, width = 5)
format_number <- function(x, digits = 1, width = NULL) {
if (!is.numeric(digits) || length(digits) != 1L || is.na(digits) ||
!is.finite(digits) || digits %% 1 != 0 || digits < 0) {
stop("`digits` must be one non-negative integer.", call. = FALSE)
}
if (!is.null(width) &&
(!is.numeric(width) || length(width) != 1L || is.na(width) ||
!is.finite(width) || width %% 1 != 0 || width < 0)) {
stop("`width` must be NULL or one non-negative integer.", call. = FALSE)
}

x <- round_half_away_from_zero(x, digits = digits)

out <- if (is.null(width)) {
formatC(x, digits = digits, format = "f")
} else {
formatC(x, digits = digits, format = "f", width = width)
}

out
}
4 changes: 4 additions & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ reference:
- title: Outdata
contents:
- "outdata"
- title: Formatting
contents:
- "round_half_away_from_zero"
- "format_number"
- title: Utilities
contents:
- "default_apply"
Expand Down
5 changes: 5 additions & 0 deletions man/collect_n_subject.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions man/format_number.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions man/round_half_away_from_zero.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion tests/testthat/test-independent-testing-define.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ test_that("warning if one of name is not in the plan data frame of meta define_o
})



test_that("meta_adam class object with list population contains in the object at define_observation", {
expect_equal(names(z)[4], "observation")
})
Expand Down
2 changes: 0 additions & 2 deletions tests/testthat/test-independent-testing-meta_check.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ test_that("variable 'RACE' checking", {
})



test_that("variable 'AEDECOD' checking in population", {
expect_error(meta_check_var(meta_example(), var = "AEDECOD", type = c("population")))
})
Expand All @@ -15,7 +14,6 @@ test_that("variable 'AEDECOD' checking in observation", {
})



test_that("variable 'BMIBL' checking in population or observation", {
expect_error(meta_check_var(meta_example(), var = "BMIBL", type = c("population", "observation")))
})
Expand Down
Loading
Loading