If value is present anywhere in row of other matrix with matching ID, populate cell with "1" -- R

Viewed 80

I'm trying to create a matrix to eventually run inter-rater reliability. I am trying to populate a matrix with TRUE and FALSE (or 1/0) based on whether a string is present in a row with a matching ID in a second or third matrix. I've included what that should look like at the bottom.

Below is my reproducible example, including the two existing matrices and what I've tried so far to produce the final matrix I want. I was able to get it to the point where I can confirm I'm selecting the correct columns that match the matrix I'm wanting to match (see output with "m1" in all columns that have "m1" in the column name). I haven't figured out how to get to the next stage of properly matching the id column between m1.mat and the final matrix, reliability.ex. In excel this would be something like a VLOOKUP, but when I search for VLOOKUP equivalents in R, I just get join/merge functions, which I don't think will work for what I need, but maybe I'm wrong. I tried doing all this in excel but ultimately got stuck and would rather have it in R if possible anyway.

require(stringr)

set.seed(327)
ids <- sample(1:1000, 5)

m.cols <- c("id", "IP1", "IP2", "IP3", "IP4", "IP5")
m1.mat <- matrix(data=NA, nrow=5, ncol=6)
colnames(m1.mat) <- m.cols
m1.mat[1,] <- c(ids[1], "abc", "ghi", NA, NA, NA)
m1.mat[2,] <- c(ids[2], "def", NA, NA, NA, NA)
m1.mat[3,] <- c(ids[3], "mno", "jkl", NA, NA, NA)
m1.mat[4,] <- c(ids[4], "ghi", "abc", NA, NA, NA)
m1.mat[5,] <- c(ids[5], "abc", "def", "ghi", "jkl", "mno")

m2.mat <- matrix(data=NA, nrow=5, ncol=6)
colnames(m2.mat) <- m.cols
m2.mat[1,] <- c(ids[1], "def", "ghi", NA, NA, NA)
m2.mat[2,] <- c(ids[2], "def", "mno", NA, NA, NA)
m2.mat[3,] <- c(ids[3], "mno", "jkl", "abc", NA, NA)
m2.mat[4,] <- c(ids[4], "ghi", "abc", NA, NA, NA)
m2.mat[5,] <- c(ids[5], "abc", "def", "ghi", "jkl", "mno")


reliability.ex <- matrix(data=NA, nrow=5, ncol=11)
ex.cols <- c("id", "abc_m1", "abc_m2", "def_m1", "def_m2", "ghi_m1", "ghi_m2", "jkl_m1", "jkl_m2", "mno_m1", "mno_m2")
colnames(reliability.ex) <- ex.cols
reliability.ex[,1] <- ids
ip.indx <- grepl('m1', colnames(reliability.ex))


for (i in 1:nrow(reliability.ex)) {
  for(j in 1:ncol(reliability.ex)) {
    if (grepl("m1", colnames(reliability.ex)[j])==TRUE) {
            reliability.ex[i,j] <- "m1"
    }
  }
}

Below are the matrices based on the above code:

> m1.mat
     id    IP1   IP2   IP3   IP4   IP5  
[1,] "345" "abc" "ghi" NA    NA    NA   
[2,] "615" "def" NA    NA    NA    NA   
[3,] "478" "mno" "jkl" NA    NA    NA   
[4,] "792" "ghi" "abc" NA    NA    NA   
[5,] "881" "abc" "def" "ghi" "jkl" "no"

> m2.mat
     id    IP1   IP2   IP3   IP4   IP5  
[1,] "345" "def" "ghi" NA    NA    NA   
[2,] "615" "def" "mno" NA    NA    NA   
[3,] "478" "mno" "jkl" "abc" NA    NA   
[4,] "792" "ghi" "abc" NA    NA    NA   
[5,] "881" "abc" "def" "ghi" "jkl" "mno" 

> reliability.ex
         id    abc_m1 abc_m2 def_m1 def_m2 ghi_m1 ghi_m2 jkl_m1 jkl_m2 mno_m1 mno_m2
    [1,] "345" "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA    
    [2,] "615" "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA    
    [3,] "478" "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA    
    [4,] "792" "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA    
    [5,] "881" "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA     "m1"   NA  

And this is what I want to be able to produce instead of what is currently named reliability.ex:

> reliability.desired
     id    abc_m1 abc_m2 def_m1 def_m2 ghi_m1 ghi_m2 jkl_m1 jkl_m2 mno_m1 mno_m2
[1,] "345" "1"    "0"    "0"    "1"    "1"    "1"    "0"    "0"    "0"    "0"   
[2,] "615" "0"    "0"    "1"    "1"    "0"    "0"    "0"    "0"    "0"    "1"   
[3,] "478" "0"    "1"    "0"    "0"    "0"    "0"    "1"    "1"    "1"    "1"   
[4,] "792" "1"    "1"    "0"    "0"    "1"    "1"    "0"    "0"    "0"    "0"   
[5,] "881" "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"   

Any help is appreciated! I'm still figuring out R.

3 Answers
library(dplyr)
library(tidyr)
make_rmat <- function(mat) {
    mat %>% 
        data.frame() %>% 
        pivot_longer(!id) %>% 
        pivot_wider(!name, names_from=value, values_fn = list) %>% 
        select(!`NA`) %>% 
        unnest(!id) %>% 
        mutate(across(!id, ~ifelse(is.na(.x), 0, 1)))
}
reliability.desired <- merge(make_rmat(m1.mat), make_rmat(m2.mat), by="id",suffixes=c("_m1","_m2"))

You could use an outer approach (outer calculates the outer product of a matrix and can be (ab)used to apply a function permutative onto the elements of two lists, in contrast to mapply() which is only able to do that consecutively).

First sort all the unique values into a vector u. Then define a Vectorized function FUN, that checks if any of the u is in an specific matrix cell. Put FUN in outer and lapply over with the two matrices. The aperm transposes the resulting arrays suitably to calculate rowSums, i.e. wheter the specific value of u is in the entire matrix. Finally just do some colnames stuff and cbind the thing.

u <- sort(unique(c(m1.mat[,-1], m2.mat[,-1])))
FUN <- Vectorize(\(m, u) any(u %in% m))
rr <- lapply(list(m1=m1.mat, m2=m2.mat), \(m) 
       `colnames<-`(rowSums(
         aperm(outer(m[,-1], u, FUN), c(1, 3, 2)), 
         dim=2), u))
rr <- do.call(cbind, 
              Map(\(x, y) 
                   `colnames<-`(x, paste0(colnames(x), '_', y)), rr, names(rr)))
rr <- cbind(id=m1.mat[,1], rr[, order(colnames(rr))])
rr
#   id    abc_m1 abc_m2 def_m1 def_m2 ghi_m1 ghi_m2 jkl_m1 jkl_m2 mno_m1 mno_m2
# [1,] "345" "1"    "0"    "0"    "1"    "1"    "1"    "0"    "0"    "0"    "0"   
# [2,] "615" "0"    "0"    "1"    "1"    "0"    "0"    "0"    "0"    "0"    "1"   
# [3,] "478" "0"    "1"    "0"    "0"    "0"    "0"    "1"    "1"    "1"    "1"   
# [4,] "792" "1"    "1"    "0"    "0"    "1"    "1"    "0"    "0"    "0"    "0"   
# [5,] "881" "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"    "1"  

Note: I used the new R4.1.* \(x) notation to define functions. If you don't have it, just use function(x) instead.

First off, it would be better to put the ids of each case in the rownames, so as not to confuse IDs with actual codings. Then, you can just loop over it without using stringr or dplyr:


set.seed(327)
ids <- sample(1:1000, 5)

m.cols <- c("IP1", "IP2", "IP3", "IP4", "IP5")
m1.mat <- matrix(data=NA, nrow=5, ncol=5)
colnames(m1.mat) <- m.cols
rownames(m1.mat) <- ids ## Store id's in the rownames
m1.mat[1,] <- c("abc", "ghi", NA, NA, NA)
m1.mat[2,] <- c("def", NA, NA, NA, NA)
m1.mat[3,] <- c("mno", "jkl", NA, NA, NA)
m1.mat[4,] <- c("ghi", "abc", NA, NA, NA)
m1.mat[5,] <- c("abc", "def", "ghi", "jkl", "mno")

m2.mat <- matrix(data=NA, nrow=5, ncol=5)
colnames(m2.mat) <- m.cols
rownames(m2.mat) <- ids ## Store id's in the rownames
m2.mat[1,] <- c("def", "ghi", NA, NA, NA)
m2.mat[2,] <- c("def", "mno", NA, NA, NA)
m2.mat[3,] <- c("mno", "jkl", "abc", NA, NA)
m2.mat[4,] <- c("ghi", "abc", NA, NA, NA)
m2.mat[5,] <- c("abc", "def", "ghi", "jkl", "mno")

possible = unique(c(m1.mat,m2.mat))   ## Get all possible values
possible = possible[!is.na(possible)] ## NA is not a valid field

reliability.ex <- matrix(data=0, nrow=length(ids), ncol=length(possible)*2)
rownames(reliability.ex) = ids ## Store id's in the rownames
colnames(reliability.ex) = 1:(length(possible)*2)

c0 = 1
for(c in possible){
  colnames(reliability.ex)[c0]=paste(c,"m1",sep="_")
  colnames(reliability.ex)[c0+1]=paste(c,"m2",sep="_")
  for(i in as.character(ids)){ ## Use the rownames to match codings
    if(c %in% m1.mat[i,]){reliability.ex[i,c0]=1}
    if(c %in% m2.mat[i,]){reliability.ex[i,c0+1]=1}
  }
  c0=c0+2
}

However, when computing interrater reliability, it might be good not to store the values for m1 and m2 independently but to already store the agreement. In the innermost part of the for-loop above, the code tests for the presence of an element in both matrices for this case. You could easily already compute the agreement on this code at that point (agreement = c %in% m1.mat[i,] == c %in% m2.mat[i,]). This might be worth doing (and storing) for the next steps. Otherwise, you will have to loop over that matrix again to compute agreements.

What I mean is:

agree <- matrix(data=FALSE, nrow=length(ids), ncol=length(possible))
rownames(agree) = ids
colnames(agree) = possible

for(c in possible){
  for(i in as.character(ids)){
    if(c %in% m1.mat[i,]==c %in% m2.mat[i,]){agree[i,c]=TRUE}
  }
}

Which would produce the output:

> agree  ## Cross table
      abc   def   mno  ghi  jkl
345 FALSE FALSE  TRUE TRUE TRUE
615  TRUE  TRUE FALSE TRUE TRUE
478 FALSE  TRUE  TRUE TRUE TRUE
792  TRUE  TRUE  TRUE TRUE TRUE
881  TRUE  TRUE  TRUE TRUE TRUE
> sum(agree)/length(agree)  ## Holsti Agreement
[1] 0.84
Related