Wybierz / przypisz do data.table, gdy nazwy zmiennych są przechowywane w wektorze znakowym

92

Jak odnosisz się do zmiennych w a, data.tablejeśli nazwy zmiennych są przechowywane w wektorze znakowym? Na przykład działa to w przypadku data.frame:

df <- data.frame(col1 = 1:3)
colname <- "col1"
df[colname] <- 4:6
df
#   col1
# 1    4
# 2    5
# 3    6

Jak mogę wykonać tę samą operację dla tabeli data.table, z :=notacją lub bez ? Oczywista rzecz dt[ , list(colname)]nie działa (i nie spodziewałem się tego).

frankc
źródło

Odpowiedzi:

133

Dwa sposoby programowego wybierania zmiennych:

  1. with = FALSE:

     DT = data.table(col1 = 1:3)
     colname = "col1"
     DT[, colname, with = FALSE] 
     #    col1
     # 1:    1
     # 2:    2
     # 3:    3
    
  2. ..Przedrostek „kropka kropka” ( ):

     DT[, ..colname]    
     #    col1
     # 1:    1
     # 2:    2
     # 3:    3
    

Więcej informacji na temat notacji „kropka kropka” ( ..) można znaleźć w sekcji Nowe funkcje w wersji 1.10.2 (obecnie nie jest ona opisana w tekście pomocy).

Aby przypisać do zmiennych, zawiń LHS lub :=w nawiasy:

DT[, (colname) := 4:6]    
#    col1
# 1:    4
# 2:    5
# 3:    6

Ten ostatni jest znany jako plonk kolumny , ponieważ zastępujesz cały wektor kolumny przez odniesienie. Jeśli podzbiór ibył obecny, będzie to przypisywanie podrzędne przez odniesienie. Parens (colname)to skrót wprowadzony w wersji v1.9.4 w CRAN w październiku 2014 r. Oto aktualności :

Używanie with = FALSEwith :=jest teraz przestarzałe we wszystkich przypadkach, biorąc pod uwagę, że zawijanie LHS :=z nawiasami jest preferowane od jakiegoś czasu.

colVar = "col1"
DT[, (colVar) := 1]                             # please change to this
DT[, c("col1", "col2") := 1]                    # no change
DT[, 2:4 := 1]                                  # no change
DT[, c("col1","col2") := list(sum(a), mean(b))]  # no change
DT[, `:=`(...), by = ...]                       # no change

Zobacz także sekcję Szczegóły w ?`:=`:

DT[i, (colnamevector) := value]
# [...] The parens are enough to stop the LHS being a symbol

Aby odpowiedzieć na dalsze pytanie w komentarzu, oto jeden sposób (jak zwykle jest wiele sposobów):

DT[, colname := cumsum(get(colname)), with = FALSE]
#    col1
# 1:    4
# 2:    9
# 3:   15 

lub, może być łatwiejsze czytanie, pisanie i debugowanie tylko do evala paste, podobnie do konstruowania dynamicznej instrukcji SQL do wysłania na serwer:

expr = paste0("DT[,",colname,":=cumsum(",colname,")]")
expr
# [1] "DT[,col1:=cumsum(col1)]"

eval(parse(text=expr))
#    col1
# 1:    4
# 2:   13
# 3:   28

Jeśli często to robisz, możesz zdefiniować funkcję pomocniczą EVAL:

EVAL = function(...)eval(parse(text=paste0(...)),envir=parent.frame(2))

EVAL("DT[,",colname,":=cumsum(",colname,")]")
#    col1
# 1:    4
# 2:   17
# 3:   45

Teraz, gdy data.table1.8.2 automatycznie optymalizuje się jpod kątem wydajności, preferowane może być użycie tej evalmetody. get()W jzabezpiecza pewne optymalizacje, na przykład.

Albo jest set(). Niska, funkcjonalna forma :=, która byłaby tutaj w porządku. Zobacz ?set.

set(DT, j = colname, value = cumsum(DT[[colname]]))
DT
#    col1
# 1:    4
# 2:   21
# 3:   66
Matt Dowle
źródło
1
Dzięki za odpowiedź Matthew. Opcja with = FALSE zdecydowanie rozwiązuje część mojego problemu. Jednak w rzeczywistości chcę zastąpić kolumnę sumą kolumny. Czy mogę w jakiś sposób odwołać się do nazwy kolumny za pomocą zmiennej po prawej stronie przypisania?
frankc
Właściwie po prostu zapisałem sumę na zewnątrz z inną nazwą, która nie istnieje w dt i działa dobrze.
frankc
1
Ale to byłaby cała dodatkowa linia! Niezbyt eleganckie :) Ale ok czasem się przydaje. W takich przypadkach najlepiej zaczynać nazwę zmiennej od .lub w ..celu uniknięcia potencjalnego maskowania, jeśli DTw przyszłości będzie zawierać ten symbol jako nazwę kolumny (i trzymaj się konwencji, od której nazwy kolumn nie zaczynają się .). Istnieją pewne żądania funkcji, aby uczynić go bardziej odpornym na problemy z zakresem, takie jak dodawanie .()i ..().
Matt Dowle
Odpowiedziałem, zanim zauważyłem, że zredagowałeś swoją odpowiedź. Moja pierwsza myśl była eval (parse ()), ale z jakiegoś powodu miałem problemy z uruchomieniem tego, kiedy dotarło do mnie, że muszę to zrobić na zewnątrz. To świetna odpowiedź z wieloma rzeczami, o których nie myślałem. Ogólnie dzięki za data.table, to świetny pakiet.
frankc
2
Należy pamiętać, że można użyć interpolacji typu string quasi-perl w fn$pakiecie gsubfn do poprawy czytelności rozwiązania EVAL: library(gsubfn); fn$EVAL( "DT[,$colname:=cumsum($colname)]" ).
G. Grothendieck,
8

* To nie jest prawdziwa odpowiedź, ale nie mam wystarczających danych na temat ulicy, aby dodawać komentarze: /

W każdym razie, dla każdego, kto chciałby faktycznie utworzyć nową kolumnę w tabeli danych z nazwą przechowywaną w zmiennej, mam do pracy następujące rzeczy. Nie mam pojęcia, jak to działa. Jakieś sugestie dotyczące ulepszeń? Czy można bezpiecznie założyć, że bezimienna nowa kolumna zawsze otrzyma nazwę V1?

colname <- as.name("users")
# Google Analytics query is run with chosen metric and resulting data is assigned to DT
DT2 <- DT[, sum(eval(colname, .SD)), by = country]
setnames(DT2, "V1", as.character(colname))

Zauważ, że mogę odwołać się do tego w sumie (), ale nie mogę go przypisać w tym samym kroku. BTW, powodem, dla którego muszę to zrobić, jest to, że nazwa kolumny będzie oparta na danych wprowadzonych przez użytkownika w aplikacji Shiny.

efh0888
źródło
+1 za samą pracę: zgadzam się, że to nie może być „droga”, aby to zrobić, ale po spędzeniu około 45 minut na rozlewaniu każdego posta SO na ten temat, jest to jedyne rozwiązanie, do którego faktycznie byłem w stanie praca - dzięki za poświęcenie czasu na zwrócenie uwagi!
neuropsych
Cieszę się, że mogłem pomóc! Niestety, nigdy nie znalazłem bardziej eleganckiego rozwiązania bezpośrednio przy użyciu danych.tables, chociaż ta 3 linijka nie jest straszna. W moim scenariuszu zdałem sobie sprawę, że prostszą alternatywą byłoby użycie tidyr, aby moje dane były „długie” zamiast „szerokie”, ponieważ na podstawie danych wejściowych użytkownika zawsze mógłbym filtrować według pojedynczej kolumny zamiast wybierać z zestawu kolumn.
efh0888
2
Nie można bezpiecznie zakładać, że V1jest to nowa nazwa. Na przykład, jeśli czytasz csv z freadi istnieje nienazwana kolumna, będzie miała V1nazwę (i read.csvda X). Więc możliwe, że Twój stół ma już plik V1. Może po prostu zdobądź nazwę przeznames(DT)[length(names(DT))]
dracodoc
2

Dla wielu kolumn i funkcji zastosowanej do wartości kolumn.

Podczas aktualizowania wartości z funkcji, RHS musi być obiektem listy, więc użycie pętli na .SDwith lapplyzałatwi sprawę.

Poniższy przykład konwertuje kolumny liczb całkowitych na kolumny liczbowe

a1 <- data.table(a=1:5, b=6:10, c1=letters[1:5])
sapply(a1, class)  # show classes of columns
#         a           b          c1 
# "integer"   "integer" "character" 

# column name character vector
nm <- c("a", "b")

# Convert columns a and b to numeric type
a1[, j = (nm) := lapply(.SD, as.numeric ), .SDcols = nm ]

sapply(a1, class)
#         a           b          c1 
# "numeric"   "numeric" "character" 
Sathish
źródło
2

Pobierz wiele kolumn z data.table za pomocą zmiennej lub funkcji:

library(data.table)

x <- data.table(this=1:2,that=1:2,whatever=1:2)

# === explicit call
x[, .(that, whatever)]
x[, c('that', 'whatever')]

# === indirect via  variable
# ... direct assignment
mycols <- c('that','whatever')
# ... same as result of a function call
mycols <- grep('a', colnames(x), value=TRUE)

x[, ..mycols]
x[, .SD, .SDcols=mycols]

# === direct 1-liner usage
x[, .SD, .SDcols=c('that','whatever')]
x[, .SD, .SDcols=grep('a', colnames(x), value=TRUE)]

które wszystkie dają

   that whatever
1:    1        1
2:    2        2

Uważam, że .SDcolsdroga jest najbardziej elegancka.

CK
źródło
1

Możesz tego spróbować

colname <- as.name („COL_NAME”)

DT2 <- DT [, list (COL_SUM = sum (eval (nazwa kolumny, .SD))), by = c (grupa)]

shrilata murthy
źródło
1
Zawsze zaleca się dodanie wyjaśnienia do kodu zamiast tylko wysyłania kodu.
MBorg