Comment supprimer une ligne par référence dans data.table?

150

Ma question porte sur l'attribution par référence par rapport à la copie data.table. Je veux savoir si on peut supprimer des lignes par référence, similaire à

DT[ , someCol := NULL]

Je veux savoir

DT[someRow := NULL, ]

Je suppose qu'il y a une bonne raison pour laquelle cette fonction n'existe pas, alors peut-être pourriez-vous simplement indiquer une bonne alternative à l'approche de copie habituelle, comme ci-dessous. En particulier, en allant avec mon exemple préféré (data.table),

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
#      x y v
# [1,] a 1 1
# [2,] a 3 2
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Dites que je veux supprimer la première ligne de cette table data.table. Je sais que je peux le faire:

DT <- DT[-1, ]

mais souvent, nous voulons éviter cela, car nous copions l'objet (et cela nécessite environ 3 * N de mémoire, si N object.size(DT), comme indiqué ici . Maintenant, j'ai trouvé set(DT, i, j, value). Je sais comment définir des valeurs spécifiques (comme ici: définir tout valeurs des lignes 1 et 2 et des colonnes 2 et 3 à zéro)

set(DT, 1:2, 2:3, 0) 
DT
#      x y v
# [1,] a 0 0
# [2,] a 0 0
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Mais comment puis-je effacer les deux premières lignes, par exemple? Faire

set(DT, 1:2, 1:3, NULL)

définit le DT entier sur NULL.

Mes connaissances SQL sont très limitées, alors vous me dites: donnée data.table utilise la technologie SQL, y a-t-il un équivalent à la commande SQL

DELETE FROM table_name
WHERE some_column=some_value

dans data.table?

Florian Oswald
la source
17
Je ne pense pas que cela data.table()utilise autant la technologie SQL que l'on puisse établir un parallèle entre les différentes opérations en SQL et les différents arguments d'un data.table. Pour moi, la référence à la «technologie» implique quelque part qu'elle se data.tabletrouve quelque part au-dessus d'une base de données SQL, ce qui n'est pas le cas d'AFAIK.
Chase
1
merci chasse. ouais, je suppose que l'analogie avec sql était une supposition sauvage.
Florian Oswald
1
Souvent, il devrait être suffisant de définir un indicateur pour conserver les lignes, comme DT[ , keep := .I > 1], puis un sous-ensemble pour des opérations ultérieures :, DT[(keep), ...]peut-être même setindex(DT, keep)la vitesse de ce sous-ensemble. Ce n'est pas une panacée, mais cela vaut la peine d'être considéré comme un choix de conception dans votre flux de travail - voulez-vous vraiment supprimer toutes ces lignes de la mémoire ou préférez-vous les exclure? La réponse diffère selon le cas d'utilisation.
MichaelChirico

Réponses:

125

Bonne question. data.tableimpossible de supprimer des lignes par référence pour le moment.

data.tablepeut ajouter et supprimer des colonnes par référence car il suralloue le vecteur des pointeurs de colonne, comme vous le savez. Le plan est de faire quelque chose de similaire pour les lignes et de permettre rapidement insertet delete. Une suppression de ligne utiliserait memmoveen C pour déplacer les éléments (dans chaque colonne) après les lignes supprimées. La suppression d'une ligne au milieu de la table serait encore assez inefficace par rapport à une base de données de stockage de lignes telle que SQL, qui est plus adaptée pour une insertion et une suppression rapides de lignes où que ces lignes se trouvent dans la table. Mais encore, ce serait beaucoup plus rapide que de copier un nouvel objet volumineux sans les lignes supprimées.

D'un autre côté, comme les vecteurs de colonne seraient suralloués, les lignes pourraient être insérées (et supprimées) à la fin , instantanément; par exemple, une série chronologique croissante.


C'est classé comme un problème: supprimez les lignes par référence .

Matt Dowle
la source
1
@Matthew Dowle Y a-t-il des nouvelles à ce sujet?
statquant
15
@statquant Je pense que je devrais corriger les 37 bugs et finir freadpremier. Après cela, c'est assez élevé.
Matt Dowle
15
@MatthewDowle bien sûr, merci encore pour tout ce que vous faites.
statquant
1
@rbatt Correct. DT[b<8 & a>3]renvoie une nouvelle table data.table. Nous aimerions ajouter delete(DT, b>=8 | a<=3)et DT[b>=8 | a<=8, .ROW:=NULL]. L'avantage de ce dernier serait de se combiner avec d'autres fonctionnalités []telles que les numéros de ligne i, la participation iet le rollbénéfice de l' [i,j,by]optimisation.
Matt Dowle
2
@charliealpha Aucune mise à jour. Les contributions sont les bienvenues. Je suis prêt à guider. Il a besoin de compétences C - encore une fois, je suis prêt à guider.
Matt Dowle
29

l'approche que j'ai adoptée pour rendre l'utilisation de la mémoire similaire à la suppression sur place consiste à sous-définir une colonne à la fois et à la supprimer. pas aussi rapide qu'une solution memmove C appropriée, mais l'utilisation de la mémoire est tout ce qui me préoccupe ici. quelque chose comme ça:

DT = data.table(col1 = 1:1e6)
cols = paste0('col', 2:100)
for (col in cols){ DT[, (col) := 1:1e6] }
keep.idxs = sample(1e6, 9e5, FALSE) # keep 90% of entries
DT.subset = data.table(col1 = DT[['col1']][keep.idxs]) # this is the subsetted table
for (col in cols){
  DT.subset[, (col) := DT[[col]][keep.idxs]]
  DT[, (col) := NULL] #delete
}
vc273
la source
5
+1 Belle approche efficace de la mémoire. Donc, idéalement, nous devons supprimer un ensemble de lignes par référence, n'est-ce pas, je n'avais pas pensé à cela. Il faudra une série de memmoves pour combler les lacunes, mais ce n'est pas grave.
Matt Dowle
Cela fonctionnerait-il comme une fonction, ou est-ce que l'utilisation dans une fonction et le retour l'obligent à faire des copies en mémoire?
russellpierce
1
cela fonctionnerait dans une fonction, puisque data.tables sont toujours des références.
vc273
1
merci, gentil. Pour accélérer un peu (surtout avec de nombreuses colonnes), vous changez DT[, col:= NULL, with = F]deset(DT, NULL, col, NULL)
Michele
2
Mise à jour à la lumière du changement d'idiome et d'avertissement "avec = FALSE avec: = était obsolète dans la v1.9.4 publiée en octobre 2014. Veuillez placer la LHS de: = avec des parenthèses; par exemple, DT [, (maVar): = sum (b) , by = a] pour attribuer aux noms de colonnes contenus dans la variable myVar. Voir? ': =' pour d'autres exemples. Comme averti en 2014, il s'agit désormais d'un avertissement. "
Frank
6

Voici une fonction de travail basée sur la réponse de @ vc273 et les commentaires de @ Frank.

delete <- function(DT, del.idxs) {           # pls note 'del.idxs' vs. 'keep.idxs'
  keep.idxs <- setdiff(DT[, .I], del.idxs);  # select row indexes to keep
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs]); # this is the subsetted table
  setnames(DT.subset, cols[1]);
  for (col in cols[2:length(cols)]) {
    DT.subset[, (col) := DT[[col]][keep.idxs]];
    DT[, (col) := NULL];  # delete
  }
   return(DT.subset);
}

Et exemple de son utilisation:

dat <- delete(dat,del.idxs)   ## Pls note 'del.idxs' instead of 'keep.idxs'

Où "dat" est une table de données. La suppression de 14000 lignes de 1,4 million de lignes prend 0,25 seconde sur mon ordinateur portable.

> dim(dat)
[1] 1419393      25
> system.time(dat <- delete(dat,del.idxs))
   user  system elapsed 
   0.23    0.02    0.25 
> dim(dat)
[1] 1404715      25
> 

PS. Puisque je suis nouveau dans SO, je ne pourrais pas ajouter de commentaire au fil de discussion de @ vc273 :-(

Jarno P.
la source
J'ai commenté sous la réponse de vc expliquant la syntaxe modifiée pour (col): =. Assez bizarre d'avoir une fonction nommée "supprimer" mais un argument lié à ce qu'il faut conserver. Btw, il est généralement préférable d'utiliser un exemple reproductible plutôt que d'afficher dim pour vos propres données. Vous pouvez réutiliser DT de la question, par exemple.
Frank
Je ne comprends pas pourquoi vous le faites par référence, mais utilisez plus tard une affectation dat <-
skan
1
@skan, Cette affectation assigne "dat" pour pointer vers la data.table modifiée qui a elle-même été créée en sous-ensemble la data.table d'origine. Le <- assingment ne fait pas de copie des données de retour, lui attribue simplement un nouveau nom. lien
Jarno P.
@Frank, j'ai mis à jour la fonction pour la bizarrerie que vous avez signalée.
Jarno P.
OK merci. Je laisse le commentaire car je pense toujours qu'il vaut la peine de noter que montrer la sortie de la console au lieu d'un exemple reproductible n'est pas encouragé ici. De plus, une seule référence n'est pas si informative. Si vous avez également mesuré le temps nécessaire pour le sous-ensemble, ce serait plus informatif (puisque la plupart d'entre nous ne savent pas intuitivement combien de temps cela prend, encore moins combien de temps cela prend sur votre composition). Quoi qu'il en soit, je ne veux pas suggérer que c'est une mauvaise réponse; Je suis l'un de ses votants positifs.
Frank
4

À la place ou en essayant de définir sur NULL, essayez de définir sur NA (correspondant au type NA pour la première colonne)

set(DT,1:2, 1:3 ,NA_character_)
IRTFM
la source
3
ouais, ça marche je suppose. Mon problème est que j'ai beaucoup de données et je veux me débarrasser exactement de ces lignes avec NA, peut-être sans avoir à copier DT pour se débarrasser de ces lignes. merci pour ton commentaire quand même!
Florian Oswald
4

Le sujet intéresse encore beaucoup de monde (moi compris).

Que dire de cela? J'avais l'habitude assignde remplacer le glovalenvet le code décrit précédemment. Il serait préférable de capturer l'environnement d'origine, mais au moins, globalenvil est efficace en mémoire et agit comme un changement par ref.

delete <- function(DT, del.idxs) 
{ 
  varname = deparse(substitute(DT))

  keep.idxs <- setdiff(DT[, .I], del.idxs)
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs])
  setnames(DT.subset, cols[1])

  for (col in cols[2:length(cols)]) 
  {
    DT.subset[, (col) := DT[[col]][keep.idxs]]
    DT[, (col) := NULL];  # delete
  }

  assign(varname, DT.subset, envir = globalenv())
  return(invisible())
}

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
delete(DT, 3)
JRR
la source
Pour être clair, cela ne supprime pas par référence (basé sur address(DT); delete(DT, 3); address(DT)), bien que cela puisse être efficace dans un certain sens.
Frank
1
Non. Il émule le comportement et est efficace en mémoire. C'est pourquoi j'ai dit: ça agit comme ça . Mais à proprement parler, vous avez raison, l'adresse a changé.
JRR
3

Voici quelques stratégies que j'ai utilisées. Je crois qu'une fonction .ROW pourrait arriver. Aucune de ces approches ci-dessous n'est rapide. Ce sont des stratégies un peu au-delà des sous-ensembles ou du filtrage. J'ai essayé de penser comme dba en essayant simplement de nettoyer les données. Comme indiqué ci-dessus, vous pouvez sélectionner ou supprimer des lignes dans data.table:

data(iris)
iris <- data.table(iris)

iris[3] # Select row three

iris[-3] # Remove row three

You can also use .SD to select or remove rows:

iris[,.SD[3]] # Select row three

iris[,.SD[3:6],by=,.(Species)] # Select row 3 - 6 for each Species

iris[,.SD[-3]] # Remove row three

iris[,.SD[-3:-6],by=,.(Species)] # Remove row 3 - 6 for each Species

Remarque: .SD crée un sous-ensemble des données d'origine et vous permet de faire un peu de travail dans j ou data.table ultérieur. Voir https://stackoverflow.com/a/47406952/305675 . Ici, j'ai commandé mes iris par Sepal Length, prenez une Sepal.Length spécifiée comme minimum, sélectionnez les trois premiers (par Sepal Length) de toutes les espèces et renvoyez toutes les données d'accompagnement:

iris[order(-Sepal.Length)][Sepal.Length > 3,.SD[1:3],by=,.(Species)]

Les approches avant tout réorganisent une data.table de manière séquentielle lors de la suppression de lignes. Vous pouvez transposer une data.table et supprimer ou remplacer les anciennes lignes qui sont maintenant des colonnes transposées. Lorsque vous utilisez ': = NULL' pour supprimer une ligne transposée, le nom de la colonne suivante est également supprimé:

m_iris <- data.table(t(iris))[,V3:=NULL] # V3 column removed

d_iris <- data.table(t(iris))[,V3:=V2] # V3 column replaced with V2

Lorsque vous transposez le data.frame à un data.table, vous souhaiterez peut-être renommer à partir du data.table d'origine et restaurer les attributs de classe en cas de suppression. L'application de ": = NULL" à une table de données maintenant transposée crée toutes les classes de caractères.

m_iris <- data.table(t(d_iris));
setnames(d_iris,names(iris))

d_iris <- data.table(t(m_iris));
setnames(m_iris,names(iris))

Vous pouvez simplement supprimer les lignes en double que vous pouvez faire avec ou sans clé:

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]     

d_iris[!duplicated(Key),]

d_iris[!duplicated(paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)),]  

Il est également possible d'ajouter un compteur incrémentiel avec '.I'. Vous pouvez ensuite rechercher des clés ou des champs dupliqués et les supprimer en supprimant l'enregistrement avec le compteur. Ceci est coûteux en calcul, mais présente certains avantages car vous pouvez imprimer les lignes à supprimer.

d_iris[,I:=.I,] # add a counter field

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]

for(i in d_iris[duplicated(Key),I]) {print(i)} # See lines with duplicated Key or Field

for(i in d_iris[duplicated(Key),I]) {d_iris <- d_iris[!I == i,]} # Remove lines with duplicated Key or any particular field.

Vous pouvez également simplement remplir une ligne avec des 0 ou des NA, puis utiliser une requête i pour les supprimer:

 X 
   x v foo
1: c 8   4
2: b 7   2

X[1] <- c(0)

X
   x v foo
1: 0 0   0
2: b 7   2

X[2] <- c(NA)
X
    x  v foo
1:  0  0   0
2: NA NA  NA

X <- X[x != 0,]
X <- X[!is.na(x),]
rferrisx
la source
Cela ne répond pas vraiment à la question (sur la suppression par référence) et l'utilisation tsur un data.frame n'est généralement pas une bonne idée; vérifiez str(m_iris)que toutes les données sont devenues chaîne / caractère. Btw, vous pouvez également obtenir des numéros de ligne en utilisant d_iris[duplicated(Key), which = TRUE]sans créer de colonne de compteur.
Frank
1
Oui, tu as raison. Je ne réponds pas spécifiquement à la question. Mais supprimer une ligne par référence n'a pas encore de fonctionnalité ou de documentation officielle et de nombreuses personnes vont venir à cet article à la recherche de fonctionnalités génériques pour faire exactement cela. Nous pourrions créer un article pour simplement répondre à la question sur la façon de supprimer une ligne. Le débordement de pile est très utile et je comprends vraiment la nécessité de garder des réponses exactes à la question. Parfois cependant, je pense que SO peut être juste un peu fasciste à cet égard ... mais il y a peut-être une bonne raison à cela.
rferrisx
Ok, merci pour l'explication. Je pense que pour le moment, notre discussion ici est un indicateur suffisant pour quiconque se confond dans ce cas.
Frank