Randomly recode the first and second instance of a value in each row?

Viewed 106

I have a dataframe where there two instances of a value in each row (say the value is 34). I would like to replace one instance with 3 and the other with 4 (without replament, so if the first instance gets 4, the second instance gets 3. And visa versa.) And I would like to do the assignments randomly (so that some rows use 3 then 4, other rows 4 then 3.)

Here's my example:

# sample data
df1 <- data.frame(a= c(1, 2, NA, NA),b= c(2, NA, 1, NA),c= c(NA, NA,34, 2),
                 d= c(NA, 34, NA,1),e= c(34, 34,2,34),f= c(34, 1, NA,NA),
                 g= c(NA, NA,34, NA), h= c(NA,NA, NA, 34))

> df1
   a  b  c  d  e  f  g  h
1  1  2 NA NA 34 34 NA NA
2  2 NA NA 34 34  1 NA NA
3 NA  1 34 NA  2 NA 34 NA
4 NA NA  2  1 34 NA NA 34

And here is an output that fits with my goal:

   a  b  c  d e  f  g  h
1  1  2 NA NA 3  4 NA NA
2  2 NA NA  4 3  1 NA NA
3 NA  1  4 NA 2 NA  3 NA
4 NA NA  2  1 4 NA NA  3

In my attempt so far I've been able to identify the columns that hold 34 using which() with apply()

indexes_34 <- apply(df1, 1,  function(x) {which(x == 34)})

And I have randomly generated a list of vectors whose elements hold 3 and 4 or 4 and 3.

ord <- list()
for(i in 1:nrow(df1)){
  ord[[i]] <- sample(c(3,4), 2)
}

But I am having trouble with writing code that will assign the values in each 'ord' vector to each row of 'df1' at the correct indexes.

Is there a straightforward way to do this?

5 Answers

One dplyr and purrr option could be:

df1 %>%
    mutate(pmap_dfr(across(everything()), 
                    ~ `[<-`(c(...), which(c(...) == 34), sample(c(3, 4)))))

   a  b  c  d e  f  g  h
1  1  2 NA NA 4  3 NA NA
2  2 NA NA  4 3  1 NA NA
3 NA  1  4 NA 2 NA  3 NA
4 NA NA  2  1 4 NA NA  3

You can just loop over all columns and for each column create a boolean random vector that has about as many TRUE as FALSE values. Then, you just replace the 34 for TRUE values with 3 and the rest with 4.

for(i in 1:dim(df1)[2]){
  rb = runif(dim(df1)[1],0,1)>.5 ## Random vector
  df1[,i][df1[,i]==34 & rb] = 3
  df1[,i][df1[,i]==34] = 4
}

That's all. Now you have randomly replaced the 34 with 3 and 4

EDIT: If you don't want to do it randomly but force the restraint that 3 and 4 have to alternate on each row, you can do so by looping over the rows and replacing the 34 by c(3,4) or c(4,3), depending on chance. You'd have to have two 34 per row, however.

for(i in 1:dim(df1)[1]){
  if(runif(1,0,1)>0.5){replace=c(3,4)}else{replace=c(4,3)}
  df1[i,][df1[i,]==34] = replace
}

Here is a way to do it by using which(..., arr.ind = TRUE) to select 34s and replace them:

set.seed(123)
m <- as_tibble(which(df1 == 34, arr.ind = T))
m <- m %>%
    group_by(row) %>%
    mutate(col = sample(col), value = c(3, 4)) %>%
    ungroup()
df1[as.matrix(m[, 1:2])] <- m$value
#    a  b  c  d e  f  g  h
# 1  1  2 NA NA 3  4 NA NA
# 2  2 NA NA  3 4  1 NA NA
# 3 NA  1  3 NA 2 NA  4 NA
# 4 NA NA  2  1 4 NA NA  3

An option with purrr
using pmap_dfr() to do rowwise operations is usually very consistent. For every iteration, you can create a vector, then modify the selected elements in place with sample(c(3,4)).

df1%>%pmap_dfr(., ~{
        v1<-c(...)
        v1[v1 %in% 34]<-sample(c(3,4))
        v1})

# A tibble: 4 x 8
      a     b     c     d     e     f     g     h
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1     1     2    NA    NA     3     4    NA    NA
2     2    NA    NA     3     4     1    NA    NA
3    NA     1     3    NA     2    NA     4    NA
4    NA    NA     2     1     3    NA    NA     4

apply() could be useful here too
Since this dataframe could be converted to a matrix without any information loss:

data.frame(t(apply(df1, 1, function(x) replace(x, x %in% 34, sample(c(3,4))))))

   a  b  c  d e  f  g  h
1  1  2 NA NA 3  4 NA NA
2  2 NA NA  4 3  1 NA NA
3 NA  1  4 NA 2 NA  3 NA
4 NA NA  2  1 3 NA NA  4

Very loop in loop'ish but might be pretty straightforward to understand what's happening. I also tried to build on the OP's idea:

ord <- list()
for(i in 1:nrow(df1)){
  ord[[i]] <- sample(c(3,4), 2)
}

mat <- ifelse(df1 != 34 | is.na(df1), 0, 1 )

for (i in 1:nrow(df1)) {
  for (j in 1:ncol(df1)) {
    if (mat[i, j] == 1) {
      if (sum(mat[i,], na.rm = T) == 2) {
       df1[i, j] <- ord[[i]][1]
      } else if (sum(mat[i,], na.rm = T) == 1) {
        df1[i, j] <- ord[[i]][2]
      }
      mat[i, j] <- 0
    }
  }
}
Related