R get column names for changed rows

I have two dataframes, old and new, in R. Is there a way to add a column (called changed) to the new dataframe that lists the column names (in this case, separated with a ";") where the values are different between the two dataframes? I am also trying to use this is a function where the column names that I am comparing are contained in other variables (x1, x2, x3). Ideally, I would only refer to x1, x2, x3 instead of the actual column names, but I can make due if this isn't possible. A tidy solution is preferable.

old <- data.frame(var1 = c(1, 2, 3, 5), var2 = c("A", "B", "C", "D"))
new <- data.frame(var1 = c(1, 4, 3, 6), var2 = c("A", "B", "D", "Z"))
x1 <- "var1"
x2 <- "var2"
x3 <- "changed"

#Output, adding a new column changed to new dataframe
    var1 var2 changed
1    1    A   NA
2    4    B   var1
3    3    D   var2
4    6    Z   var1; var2

Solution 1:

A tidyverse way -

library(dplyr)
library(tidyr)

cols <- names(new)

bind_cols(new, map2_df(old, new, `!=`) %>%
  rowwise() %>%
  transmute(changed = {
     x <- c_across()
    if(any(x)) paste0(cols[x], collapse = ';') else NA
    }))

#  var1 var2   changed
#1    1    A      <NA>
#2    4    B      var1
#3    3    D      var2
#4    6    Z var1;var2

The same logic can be implemented in base R as well -

new$changed <- apply(mapply(`!=`, old, new), 1, function(x) 
                    if(any(x)) paste0(cols[x], collapse = ';') else NA)

Solution 2:

Here is a base R approach.

new$changed <- apply(old != new, 1L, \(r, nms) toString(nms[which(r)]), colnames(old))

Output

  var1 var2    changed
1    1    A           
2    4    B       var1
3    3    D       var2
4    6    Z var1, var2