R: Inconsistent class returned by `median()` when vector labelled with Hmisc

Viewed 477

I have a column that has been labelled with the Hmisc R package. The class of the column is c("labelled", "numeric"). If I calculate the median() of the entire column, the returned median remains c("labelled", "numeric").

BUT, if I calculated the median() in two subgroups, one median is returned with the same class, but the other is returned as class "numeric". The differing classes returned is causing errors in dplyr::summarize().

  1. Can anyone help me understand why the class changes?
  2. What can I do to resolve the issue? FYI, this code appears in the internals of a package, and I would like to avoid special coding the for variables that have been labelled with Hmisc.
library(magrittr)

data <-
  structure(
    list(
      cd4_count = c(
        30, 97, 210, NA, 358, 242, 126,
        792, 6, 145, 22, 150, 43, 23, 39, 953, 357, 427, 367, 239, 72,
        61, 61, 438, 392, 1092, 245, 326, 42, 135, 199, 158, 17, NA,
        287, 187, 252, 477, 157, NA, NA, 362, NA, 183, 885, 109, 321,
        286, 142, 797
      ),
      unsuccessful = c(
        0, 0, 0, 1, 0, 0, 1, 1, 0, 1,
        1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0
      )
    ),
    row.names = c(NA, 50L),
    class = "data.frame"
  )

# Add label to CD4 count, using Hmisc package
Hmisc::label(data$cd4_count) <- "CD4 count"

# the classes here are all the same
data$cd4_count %>% class()
#> [1] "labelled" "numeric"
data$cd4_count[data$unsuccessful == 0] %>% class()
#> [1] "labelled" "numeric"
data$cd4_count[data$unsuccessful == 1] %>% class()
#> [1] "labelled" "numeric"


# Why are the results not the same class?!?!
data$cd4_count[data$unsuccessful == 0] %>% median(na.rm = TRUE) %>% class()
#> [1] "labelled" "numeric"
data$cd4_count[data$unsuccessful == 1] %>% median(na.rm = TRUE) %>% class()
#> [1] "numeric"

# Because the classes are different, I cannot run this code
data %>%
  dplyr::group_by(unsuccessful) %>%
  dplyr::summarize_at(dplyr::vars(cd4_count), median, na.rm = TRUE)
#> Error: Problem with `summarise()` input `cd4_count`.
#> x Input `cd4_count` must return compatible vectors across groups
#> i Result type for group 1 (unsuccessful = 0): <labelled>.
#> i Result type for group 2 (unsuccessful = 1): <double>.
#> i Input `cd4_count` is `(function (x, na.rm = FALSE, ...) ...`.

Created on 2021-04-27 by the reprex package (v2.0.0)

1 Answers

user20650 points out in the comments that attributes are droped and kept depending on the vector length of x.

When we look at the code of the median.default method, we can see why. If length(x) is an even number, then mean is used (inside median), otherwise x is just sorted and subsetted which, unlike mean, doesn't remove the attributes.

# lets have a look at the median.default method
function (x, na.rm = FALSE, ...) 
{
  if (is.factor(x) || is.data.frame(x)) 
    stop("need numeric data")
  if (length(names(x))) 
    names(x) <- NULL
  if (na.rm) 
    x <- x[!is.na(x)]
  else if (any(is.na(x))) 
    return(x[FALSE][NA])
  n <- length(x)
  if (n == 0L) 
    return(x[FALSE][NA])
  half <- (n + 1L)%/%2L
  if (n%%2L == 1L) 
    # when length is odd: attribute is kept
    sort(x, partial = half)[half] 
  # when length is even: `mean` drops attribute
  else mean(sort(x, partial = half + 0L:1L)[half + 0L:1L]) 
}

Created on 2021-04-28 by the reprex package (v0.3.0)

Let's have another look at different vectors and how they behave. We can define a keep_attr function which will keep the attributes of the wrapped function and input.

x1 <- 1
Hmisc::label(x1) = "qw"
class(median(x1)) # keeps attribute
#> [1] "labelled" "numeric"
class(mean(x1))  # drops attribute
#> [1] "numeric"

x2 <- c(1, 2)
Hmisc::label(x2) = "qw"
class(median(x2)) # uses mean
#> [1] "numeric"
class(mean(x2))
#> [1] "numeric"

x3 <- c(1, 2, NA)
Hmisc::label(x3) = "qw"
class(median(x3)) # doesn't use mean
#> [1] "labelled" "numeric"
class(mean(x3))
#> [1] "numeric"

keep_attr <- function(.f, x, ...) {
  x_att <- attributes(x)
  res <- .f(x, ...)
  attributes(res) <- x_att
  res
}

class(keep_attr(median, x2))
#> [1] "labelled" "numeric"
class(keep_attr(mean, x2))
#> [1] "labelled" "numeric"
keep_attr(median, x3, na.rm = TRUE)
#> qw 
#> [1] 1.5

Created on 2021-04-28 by the reprex package (v0.3.0)

Update Regarding your dplyr problem I was now able to reproduce the problem (I first forgot to label the cd4_count column and thought it was a dplyr versioning issue). However, the workaround with the keep_attr seems to be working.

library(dplyr)

data <-
  structure(
    list(
      cd4_count = c(
        30, 97, 210, NA, 358, 242, 126,
        792, 6, 145, 22, 150, 43, 23, 39, 953, 357, 427, 367, 239, 72,
        61, 61, 438, 392, 1092, 245, 326, 42, 135, 199, 158, 17, NA,
        287, 187, 252, 477, 157, NA, NA, 362, NA, 183, 885, 109, 321,
        286, 142, 797
      ),
      unsuccessful = c(
        0, 0, 0, 1, 0, 0, 1, 1, 0, 1,
        1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0
      )
    ),
    row.names = c(NA, 50L),
    class = "data.frame"
  )

# Add label to CD4 count, using Hmisc package
Hmisc::label(data$cd4_count) <- "CD4 count"

data %>%
  dplyr::group_by(unsuccessful) %>%
  dplyr::summarize_at(dplyr::vars(cd4_count), median, na.rm = TRUE)
#> Error: Problem with `summarise()` input `cd4_count`.
#> x Input `cd4_count` must return compatible vectors across groups
#> i Input `cd4_count` is `(function (x, na.rm = FALSE, ...) ...`.
#> i Result type for group 1 (unsuccessful = 0): <labelled>.
#> i Result type for group 2 (unsuccessful = 1): <double>.

data %>%
  dplyr::group_by(unsuccessful) %>%
  dplyr::summarize_at(dplyr::vars(cd4_count), ~ keep_attr(median, .x, na.rm = TRUE))
#> # A tibble: 2 x 2
#>   unsuccessful cd4_count 
#>          <dbl> <labelled>
#> 1            0 210.0     
#> 2            1 135.5

Created on 2021-04-28 by the reprex package (v0.3.0)

Related