compare multiple columns and create count of matches

Viewed 123

I have a dataset with ID numbers for respondents' friends and bullies.

I'd like to go through all the friendship nominations and all the bully nominations in each row and get a count of the number of people they nominate as both. Any help would be great!

HAVE DATA:

ID  friend_1  friend_2  friend_3  bully_1  bully_2
1          4        12         7       12       15
2          8         6         7       18       20
3          9        18         1        2        1
4         15         7         2        7       13 
5          1        17         9       17        1
6          9        19        20       14       12
7         19        12        20        9       12
8          7         1        16        2       15 
9          1        10        12        1        7
10         7        11         9       11        7

WANT DATA:

ID  friend_1  friend_2  friend_3  bully_1  bully_2  num_both
1          4        12         7       12       15         1
2          8         6         7       18       20         0
3          9        18         1        2        1         1
4         15         7         2        7       13         1
5          1        17         9       17        1         2
6          9        19        20       14       12         0
7         19        12        20        9       12         1
8          7         1        16        2       15         0
9          1        10        12        1        7         1
10         7        11         9       11        7         2
4 Answers

We can use apply row-wise and find out the the number of common friends which are present both in friend and bully columns

df$num_both <- apply(df, 1, function(x) 
      length(intersect(x[grep("friend", names(df))], x[grep("bully", names(df))])))


#   ID friend_1 friend_2 friend_3 bully_1 bully_2 num_both
#1   1        4       12        7      12      15        1
#2   2        8        6        7      18      20        0
#3   3        9       18        1       2       1        1
#4   4       15        7        2       7      13        1
#5   5        1       17        9      17       1        2
#6   6        9       19       20      14      12        0
#7   7       19       12       20       9      12        1
#8   8        7        1       16       2      15        0
#9   9        1       10       12       1       7        1
#10 10        7       11        9      11       7        2

Or if you are not a big fan of apply, you can use sapply with the same logic

friend_cols <- grep("friend", names(df))
bully_cols <- grep("bully", names(df))

sapply(seq_len(nrow(df)), function(i) 
 length(intersect(df[i, friend_cols, drop = TRUE], df[i, bully_cols, drop = TRUE])))

#[1] 1 0 1 1 2 0 1 0 1 2

EDIT

If there are some NA values and we want to exclude them we can use is.na and sum

apply(df, 1, function(x) sum(!is.na(intersect(x[friend_cols], x[bully_cols]))))

Assuming that values are unique within friend/bully groups, a simple approach would be:

apply(df[,-1], 1, function (x) sum(table(x) > 1)) 
[1] 1 0 1 1 2 0 1 0 1 2

You could try comparing each bully column with the friends columns and then taking the union to compute a matrix of matches. To get your num_both you simply rowSum this match matrix:

bully_cols <- grep("bully", names(df))
friend_cols <- grep("friend", names(df))
df$num_both <- rowSums(Reduce("|", lapply(df[,bully_cols], function(x, compare) compare == x, compare = df[,friend_cols])))

The lapply calculates the matches for each bully column and then the Reduce combines them into one matrix to be summed over the rows.

#   ID friend_1 friend_2 friend_3 bully_1 bully_2 num_both
#1   1        4       12        7      12      15        1
#2   2        8        6        7      18      20        0
#3   3        9       18        1       2       1        1
#4   4       15        7        2       7      13        1
#5   5        1       17        9      17       1        2
#6   6        9       19       20      14      12        0
#7   7       19       12       20       9      12        1
#8   8        7        1       16       2      15        0
#9   9        1       10       12       1       7        1
#10 10        7       11        9      11       7        2

Here is a melt based approach from data.table. We melt into 'long' format based on the patterns in the column names (start with friend, bully), grouped by 'ID', get the length of intersecting elements of long dataset columns 'value1', 'value2' and do a join on the 'ID'

library(data.table)
setDT(df1)[melt(df1, measure = patterns('^friend', '^bully'))[,
   .(num_both = length(intersect(value1, value2))), ID], on = .(ID)]
#    ID friend_1 friend_2 friend_3 bully_1 bully_2 num_both
# 1:  1        4       12        7      12      15        1
# 2:  2        8        6        7      18      20        0
# 3:  3        9       18        1       2       1        1
# 4:  4       15        7        2       7      13        1
# 5:  5        1       17        9      17       1        2
# 6:  6        9       19       20      14      12        0
# 7:  7       19       12       20       9      12        1
# 8:  8        7        1       16       2      15        0
# 9:  9        1       10       12       1       7        1
#10: 10        7       11        9      11       7        2

Or using tidyverse by gathering into 'long' format, grouped by 'ID', summarise with the length of intersecting elements of 'value' based on the occurrence of 'friend' or 'bully' in the 'key' column and right_join with the original dataset

library(tidyverse)
df1 %>% 
   gather(key, value, -ID) %>% 
   group_by(ID) %>% 
   summarise(num_both = length(intersect(value[str_detect(key, 'friend')], 
                         value[str_detect(key, 'bully')]))) %>% 
   right_join(df1)
# A tibble: 10 x 7
#      ID num_both friend_1 friend_2 friend_3 bully_1 bully_2
#   <int>    <int>    <int>    <int>    <int>   <int>   <int>
# 1     1        1        4       12        7      12      15
# 2     2        0        8        6        7      18      20
# 3     3        1        9       18        1       2       1
# 4     4        1       15        7        2       7      13
# 5     5        2        1       17        9      17       1
# 6     6        0        9       19       20      14      12
# 7     7        1       19       12       20       9      12
# 8     8        0        7        1       16       2      15
# 9     9        1        1       10       12       1       7
#10    10        2        7       11        9      11       7

or another approach by looping over rows with pmap

df1 %>% 
     mutate(num_both = pmap(.[-1], ~ c(...) %>%
                                 {length(intersect(.[1:3], .[4:5]))}))

data

df1 <- structure(list(ID = 1:10, friend_1 = c(4L, 8L, 9L, 15L, 1L, 9L, 
19L, 7L, 1L, 7L), friend_2 = c(12L, 6L, 18L, 7L, 17L, 19L, 12L, 
1L, 10L, 11L), friend_3 = c(7L, 7L, 1L, 2L, 9L, 20L, 20L, 16L, 
12L, 9L), bully_1 = c(12L, 18L, 2L, 7L, 17L, 14L, 9L, 2L, 1L, 
11L), bully_2 = c(15L, 20L, 1L, 13L, 1L, 12L, 12L, 15L, 7L, 7L
)), class = "data.frame", row.names = c(NA, -10L))
Related