How does this unique/filtering syntax work?

Viewed 56

I have created the following data.table in R:

> DT
    ID num code
 1:  b 1   C1
 2:  b 1   C1
 3:  b 1   C1
 4:  b 1   C1
 5:  b 1     
 6:  b 3     
 7:  b 3   C1
 8:  a 3   C2
 9:  a 3   C3
10:  a 3   C3
11:  a 3     
12:  c 4   C3

where empty values are empty strings "". I then run the instruction

res <- DT[, .(agg_code = unique(code)[!(code == "")]), .(ID, num)]

which results in

> res
   ID num agg_code
1:  b   1       C1
2:  b   1         
3:  b   1     <NA>
4:  b   1     <NA>
5:  b   3       C1
6:  a   3       C2
7:  a   3       C3
8:  a   3         
9:  c   4       C3

My question is: what is this command actually doing? I understand that it is grouping by ID and num, I don't understand how unique(code) and [!(code == "")] are working. They are deleting some rows, but they are also keep some "" and creating some <NA>. What is happening here?

For completeness, DT is created by

DT = data.table(
  ID = c("b","b","b", "b","b", "b","b","a","a","a","a", "c"),
  num = c(1,1,1,1,1,3,3,3,3,3,3,4),
  code = c("C1", "C1","C1","C1", "", "","C1", "C2", "C3", "C3","", "C3")
)
2 Answers

FWIW I would write the code differently:

res <- DT[nzchar(code), TRUE, by=.(ID, num, agg_code=code)]

Advantages:

  1. Only have to run the filter on non-empty code once up front, instead of running it within each group
  2. Let by= handle the unique(code) logic for you.

Caveats:

  1. The code in your example will still return ID/num pairs for which there are no non-empty codes; in my approach, by filtering first, these pairs are lost.
  2. nzchar(code) is different from !(code == "") for NA inputs -- note that nzchar(NA) is TRUE while !(NA == "") is NA. If this is a concern, you can change it to nzchar(code, keepNA = TRUE). Note that "" is different from NA (aka NA_character_).
  3. The TRUE in j is a throwaway. We could also write unique(DT[nzchar(code)], by=c('ID', 'num', 'code')) if that's clearer. We'll have to rename code->agg_code in the output.

These 3 commands should clarify:

DT[ ID == "b" & num == 1, ]
#    ID num code
# 1:  b   1   C1
# 2:  b   1   C1
# 3:  b   1   C1
# 4:  b   1   C1
# 5:  b   1     

DT[ ID == "b" & num == 1, .(x = unique(code))]
#     x
# 1: C1
# 2:   

DT[ ID == "b" & num == 1, .(x = code == "")]
#        x
# 1: FALSE
# 2: FALSE
# 3: FALSE
# 4: FALSE
# 5:  TRUE

In short, unique is getting 2 values, and code == "" gives us 5 values, so the output of 2nd command is getting recycled.

Related