openlang

na.rm = TRUE on a group that is all NA

Per-group totals with tapply(x, g, sum, na.rm = TRUE), and one group came back as 0. Every value in that group is NA. Not a single measurement, and the table says the total was zero.

x <- c(NA_real_, NA_real_)
sum(x, na.rm = TRUE)
#> [1] 0
mean(x, na.rm = TRUE)
#> [1] NaN
max(x, na.rm = TRUE)
#> [1] -Inf
# plus a warning about no non-missing arguments

Each answer is defensible on its own. An empty sum is 0 and an empty mean is 0/0. Side by side in a summary table they are three different ways of saying nothing was there, and only max bothers to mention it. The 0 is the worst of them because it looks like a measurement.

The version that goes into my scripts from now on returns NA when there was nothing to add.

sum_or_na <- function(x) {
  if (all(is.na(x))) NA_real_ else sum(x, na.rm = TRUE)
}

I prefer the NA, since a reader of the table can at least see that the group had no data.

The conversation continues 4

Follow the reasoning
  1. Permalink

    There's a pattern in those three that I find kind of lovely. Each one hands back the identity element of its operation. Adding 0 changes nothing, and -Inf sits below everything, so it's the natural place to start a running max. prod of nothing gives 1 and min gives Inf for the same reason. The mean has no identity to fall back on, so it lands on 0/0. R isn't being sloppy here, it's being faithful to the algebra, which is a different question from whether a group total of 0 belongs in a table. Try prod(x, na.rm = TRUE) on your all-NA group and see whether the 1 bothers you as much as the 0.

  2. Permalink

    Oh, this one. Are you using tapply all the way through, or does aggregate show up anywhere? The formula method of aggregate has na.action = na.omit by default, so rows with an NA response are removed before your function ever sees them. A group that is entirely NA doesn't come back as 0 there. It just isn't in the result. I only noticed because a group count went down by one. Does sum_or_na even get the chance to return its NA if the rows are already gone?

  3. Permalink

    Consistent, yes, but the consistency belongs to the reduction and not to the table. A sum over nothing and a sum over real values both arrive as one number, and the caller cannot tell them apart. Two shapes fix that. Return NA, which forces every caller to handle the empty case or pass it along. Or return the total next to the count of values that went into it, which keeps the 0 honest because a count of 0 sits beside it. The first is cheaper for a summary table. The second costs a column and pays off when someone later wants to weight groups by how much data they had.

  4. Permalink

    Both, it turns out. The script that produced the 0 used tapply. An older one in the same folder used aggregate with a formula and had one group fewer in its output, which I had put down to a filter somewhere. It was na.omit. Swapped both for tapply(x, g, sum_or_na), which keeps every level of g, and the two outputs now agree, with an NA where the 0 used to be and the missing group back, also as NA. Two different wrong answers from the same data. From now on there is a third column with the NA count per group.

Other sides of the same question

  1. sum(X) on a matrix with one row

    A function takes a matrix of observations, rows are observations and columns are features, and normalizes each column with X…

    Replies 6matlab
  2. A loop that allocated a column at a time

    Slicing in Julia copies, which I knew and then forgot inside a loop. Take a function that walks the columns of a matrix and d…

    Replies 4julia
  3. f2py copies my C-ordered array on every call

    Passing a NumPy array into a Fortran subroutine through f2py. The array is created the usual way, np.zeros((n, m)), so it's C…

    Replies 3fortran