One option is to convert the format from 'wide' to 'long' using melt . We are grouped by 'a', we paste elements of 'variable' that correspond to non-zero elements in 'value' (as a logical condition in 'i').
melt(df, id.var='a')[value!=0, .(z=paste(variable, collapse="_")), keyby =a] # az #1: 1 b_d #2: 2 c #3: 3 b_c #4: 4 b_d #5: 5 b_d
Or instead of melt ing, we can group by 'a', unlist subset of Data.table ( .SD ) and paste columns names columns that correspond to non-zero elements ('i1').
df[, {i1 <- !!unlist(.SD) paste(names(.SD)[i1], collapse="_")} , by= a]
Benchmarks
set.seed(24) df1 <- data.table(a=1:1e6, b = sample(0:5, 1e6, replace=TRUE), c = sample(0:4, 1e6, replace=TRUE), d = sample(0:3, 1e6, replace=TRUE)) akrun1 <- function() { melt(df1, id.var='a')[value!=0, .(z=paste(variable, collapse="_")), keyby =a] } akrun2 <- function() { df1[, {i1 <- !!unlist(.SD) paste(names(.SD)[i1], collapse="_")} , by= a] } ronak <- function() { data.table(z = lapply(apply(df1, 1, function(x) which(x[-1]!= 0)), function(x) paste0(names(x), collapse = "_"))) } eddi <- function(){ df1[, newcol := gsub("NA_|_NA|NA", "", do.call(function(...) paste(..., sep = "_"), Map(function(x, y) x[(y == 0) + 1], names(.SD), .SD))) , .SDcols = b:d] } alexis = function(x) { ans = character(nrow(x)) for(j in seq_along(x)) { i = x[[j]] > 0L ans[i] = paste(ans[i], names(x)[[j]], sep = "_") } return(gsub("^_", "", ans)) } system.time(akrun1())