7
votes

Comment référencer la ligne entière lors de la création d'une nouvelle colonne dans un data.table?

J'ai une data.table avec plus de 200 variables qui sont toutes binaires. Je veux y créer une nouvelle colonne qui compte la différence entre chaque ligne et un vecteur de référence:

dt[,"distance":= sum(reference != c(V1,V2,V3,V4,V5))]
dt[,"distance":= sum(reference != .SD)]

Je peux le faire avec une petite boucle for, comme

distance = NULL
for(i in 1:nrow(dt)){      
  distance[i] = sum(reference != dt[i,])  
}

Mais c'est plutôt lent et ce n'est sûrement pas la meilleure façon de le faire. J'ai essayé:

#Example
dt = data.table(
"V1" = c(1,1,0,1,0,0,0,1,0,1,0,1,1,0,1,0),
"V2" = c(0,1,0,1,0,1,0,0,0,0,1,1,0,0,1,0),
"V3" = c(0,0,0,1,1,1,1,0,1,0,1,0,1,0,1,0),
"V4" = c(1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0),
"V5" = c(1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0)  
)

reference = c(1,1,0,1,0)

Mais ni l'un ni l'autre ne fonctionne, car ils renvoient la même valeur pour toutes les lignes. De plus, une solution où je n'ai pas à taper tous les noms de variables serait bien meilleure, car la vraie table data.table a plus de 200 colonnes


0 commentaires

4 Réponses :


7
votes

Vous pouvez utiliser sweep () avec rowSums , c'est-à-dire

HUGH <- function(dt) {
    dt[, I := .I] 
    distance_by_I <- melt(dt, id.vars = "I")[, .(distance = sum(reference != value)), keyby = "I"]
    return(dt[distance_by_I, on = "I"])
}

Sotos <- function(dt) {
    return(rowSums(sweep(dt, 2, reference) != 0))
}

dt1 <- as.data.table(replicate(5, sample(c(0, 1), 100000, replace = TRUE)))
microbenchmark(HUGH(dt1), Sotos(dt1))

#Unit: milliseconds
#       expr       min        lq      mean   median        uq       max neval cld
#  HUGH(dt1) 112.71936 117.03380 124.05758 121.6537 128.09904 155.68470   100   b
# Sotos(dt1)  23.66799  31.11618  33.84753  32.8598  34.02818  68.75044   100  a 

BENCHMARK

rowSums(sweep(dt, 2, reference) != 0)
 #[1] 2 2 2 2 4 4 3 2 4 3 2 1 3 4 1 3


8 commentaires

Pour l'enregistrer en tant que nouvelle colonne: dt [ distance: = rowSums (sweep (.SD, 2, reference)! = 0)]


Cette solution est vraiment sympa et fonctionne parfaitement dans l'exemple du jouet. Mais pour une raison quelconque, j'obtiens une erreur lors de l'application aux données réelles: Erreur dans Ops.data.frame (x, aperm (array (STATS, dims [perm]), order (perm)),: list d'une longueur de 3860579 sans signification . Une idée pourquoi?


Aucune idée. Quelle est la structure de votre tableau de données? ( str (dt) )


rowSums (dt - as.data.table (as.list (reference)) [rep (1, nrow (dt))]! = 0) semble être un peu plus rapide, si la vitesse est un problème.


Tout numérique. J'ai également essayé de changer les deux en tout entier mais sans succès. Mais merci, c'était déjà utile, j'ai juste besoin d'élargir l'exemple pour correspondre à ce que j'ai.


@Henrik J'ai essayé de l'ajouter dans l'analyse comparative mais cela génère une erreur, Erreur dans Ops.data.frame (dt1, as.data.table (as.list (référence)) [rep (1,: '- 'défini uniquement pour les blocs de données de taille égale


@Sotos OK. Les parties data.frame de l'erreur semblent suspectes. Ça marche ici. f <- function (dt) rowSums (dt - as.data.table (as.list (reference)) [rep (1, nrow (dt))]! = 0) ; all.equal (Sotos (dt1), f (dt1)) , TRUE .


Fait intéressant, je n'ai pas eu une si grande différence dans mon benchmark entre vos solutions.



2
votes

Faites fondre le tableau puis comparez chaque groupe.

dt[, I := .I]  # Add a dummy id if one doesn't already exist
distance_by_I <- melt(dt, id.vars = "I")[, .(distance = sum(reference != value)), keyby = "I"]
dt[distance_by_I, on = "I"]


0 commentaires

3
votes

Voici une autre méthode:

library(data.table)
dt1 <- as.data.table(replicate(5, sample(c(0, 1), 100000, replace = TRUE)))

identical(Sotos(dt1), mm(dt1))
# [1] TRUE

microbenchmark::microbenchmark(HUGH(dt1), Sotos(dt1), mm(dt1))
# Unit: milliseconds
#       expr       min         lq      mean     median        uq      max neval cld
#  HUGH(dt1) 85.542550 101.339416 129.71317 106.634169 112.66004 473.9380   100   b
# Sotos(dt1) 35.699128  42.677696 125.95430 180.302919 189.34098 377.9523   100   b
#    mm(dt1)  4.604986   7.002416  17.57238   9.819895  12.27015 165.1440   100  a 

benchmark

mm <- function(dt){
  colSums(t(dt) != reference)
}

mm(dt)
# [1] 2 2 2 2 4 4 3 2 4 3 2 1 3 4 1 3


1 commentaires

C'est vraiment intéressant. Je n'ai aucune idée de la raison pour laquelle l'analyse comparative est différente.



7
votes

Another:

Unit: milliseconds
            expr      min        lq     mean   median         uq       max neval
  HUGH(dt1, ref) 365.0529 370.05233 378.8826 375.0517  385.79737  396.5430     3
 Sotos(dt1, ref) 871.5693 926.50462 961.5527 981.4400 1006.54437 1031.6488     3
    mm(dt1, ref) 104.5631 121.74086 131.7157 138.9186  145.29197  151.6653     3
   ff(dt1, lref)  87.0800  87.48975  93.1361  87.8995   96.16415  104.4288     3

Comment ça marche. Nous prenons donc chaque colonne vectorielle dans .SD et la comparons au valeur unique correspondante dans ref . La fonction ! = est vectorisée, donc chaque élément de ref est recyclé pour correspondre à la longueur de chaque vecteur.

Cette carte call renvoie une liste de vecteurs TRUE / FALSE, un pour chaque colonne. Lorsque nous additionnons les valeurs TRUE / FALSE, elles sont traitées comme 1/0, il nous suffit donc d'ajouter ces colonnes. Ceci peut être réalisé en passant l'opérateur par paires + entre la première colonne et la seconde; puis de nouveau entre le résultat de ce calcul et la troisième colonne; etc. C'est ainsi que fonctionne Réduire . Il pourrait être plus lisible comme

HUGH <- function(dt, r) {
  dt[, I := .I] 
  res <- melt(dt, id.vars = "I")[, .(distance = sum(r != value)), keyby = "I"]$distance
  dt[, I := NULL]
  res
}

Sotos <- function(dt, r) {
  return(rowSums(sweep(dt, 2, r) != 0))
}

mm <- function(dt, r){
  colSums(t(dt) != r)
}

ff <- function(DT, r){
  DT[, Reduce(`+`, Map(`!=`, .SD, r))]
}

nr = 20000
nc = 500
dt1 <- as.data.table(replicate(nc, sample(0:1, nr, replace = TRUE)))
ref <- rep(as.integer(reference), length.out=nc)
lref = as.list(ref)

identical(HUGH(dt1, ref), ff(dt1, lref)) # integer output
identical(mm(dt1, ref), Sotos(dt1, ref)) # numeric output
all.equal(HUGH(dt1, ref), mm(dt1, ref))  # but they match
# all TRUE

microbenchmark::microbenchmark(times = 3, 
 HUGH(dt1, ref), 
 Sotos(dt1, ref), 
 mm(dt1, ref), 
 ff(dt1, lref)
)

qui peut être lu comme

  • v = 0
  • pour chaque xi de x, mettre à jour v = v + xi

Voir aussi ? Map et ?Reduce.


Timings. I ' m modifier les données de référence, car l'utilisation d'entiers semble beaucoup plus saine si l'OP a vraiment des données 0-1. De plus, ajouter plus de colonnes puisque l'OP dit qu'ils en ont beaucoup. Enfin, modifier la réponse de Hugh pour qu'elle soit comparable aux autres:

x = dt[, Map(`!=`, .SD, ref)]
Reduce(`+`, x, init = 0L)

Résultat:

ref = as.list(reference)
dt[, Reduce(`+`, Map(`!=`, .SD, ref))]


2 commentaires

J'adore cette réponse mais je ne comprends pas grand-chose de ce qui se passe ... + `, Map (`! = la fonction est-elle passée à Réduire ? Pourriez-vous expliquer un peu comment cela fonctionne ou donner un lien que je peux lire?


@Fino :) Bien sûr, j'ai ajouté quelques explications sur Map et Reduce.