Qual é o propósito de definir uma chave em data.table?


113

Estou usando data.table e há muitas funções que exigem que eu defina uma chave (por exemplo X[Y]). Como tal, desejo entender o que uma chave faz para definir as chaves corretamente em minhas tabelas de dados.


Uma fonte que li foi ?setkey.

setkey()classifica um data.tablee o marca como classificado. As colunas classificadas são a chave. A chave pode ser qualquer coluna em qualquer ordem. As colunas são classificadas sempre em ordem crescente. A tabela é alterada por referência. Nenhuma cópia é feita, exceto a memória de trabalho temporária com o tamanho de uma coluna.

Minha lição aqui é que uma chave "classificaria" a tabela de dados, resultando em um efeito muito semelhante a order(). No entanto, não explica o propósito de ter uma chave.


As FAQ 3.2 e 3.3 da data.table explicam:

3.2 Não tenho uma chave em uma mesa grande, mas o agrupamento ainda é muito rápido. Por que é que?

data.table usa classificação raiz. Isso é significativamente mais rápido do que outros algoritmos de classificação. Radix é especificamente para números inteiros, veja ?base::sort.list(x,method="radix"). Esse também é um dos motivos pelos quais setkey()é rápido. Quando nenhuma chave é definida ou agrupamos em uma ordem diferente daquela da chave, chamamos isso de ad hoc por.

3.3 Por que o agrupamento por colunas na chave é mais rápido do que um ad hoc por?

Porque cada grupo é contíguo na RAM, minimizando assim as buscas de páginas, e a memória pode ser copiada em massa ( memcpyem C) em vez de loop em C.

A partir daqui, acho que definir uma chave de alguma forma permite que R use "classificação raiz" em vez de outros algoritmos, e é por isso que é mais rápido.


O guia de início rápido de 10 minutos também contém um guia sobre as teclas.

  1. Chaves

Vamos começar considerando data.frame, especificamente nomes de linhas (ou em inglês, nomes de linhas). Ou seja, os vários nomes pertencentes a uma única linha. Os vários nomes pertencentes a uma única linha? Não é a isso que estamos acostumados em data.frame. Sabemos que cada linha tem no máximo um nome. Uma pessoa tem pelo menos dois nomes, um primeiro nome e um segundo nome. Isso é útil para organizar uma lista telefônica, por exemplo, que é classificada pelo sobrenome e depois pelo nome. No entanto, cada linha em um data.frame pode ter apenas um nome.

Uma chave consiste em uma ou mais colunas de nomes de domínio, que podem ser inteiros, fatores, caracteres ou alguma outra classe, não simplesmente caracteres. Além disso, as linhas são classificadas pela chave. Portanto, um data.table pode ter no máximo uma chave, pois não pode ser classificado de mais de uma maneira.

A exclusividade não é imposta, ou seja, valores de chave duplicados são permitidos. Uma vez que as linhas são classificadas pela chave, quaisquer duplicatas na chave irão aparecer consecutivamente

A lista telefônica foi útil para entender o que é uma chave, mas parece que uma chave não é diferente quando comparada a ter uma coluna de fator. Além disso, não explica por que uma chave é necessária (especialmente para usar certas funções) e como escolher a coluna a ser definida como chave. Além disso, parece que em uma data.table com o tempo como coluna, definir qualquer outra coluna como chave provavelmente bagunçaria a coluna de tempo também, o que torna ainda mais confuso, pois não sei se tenho permissão para definir qualquer outra coluna como chave. Alguém pode me iluminar, por favor?


"Eu acho que definir uma chave de alguma forma permite que R use" classificação raiz "em vez de outros algoritmos" - eu não entendi isso com a ajuda. Minha leitura é que definir uma chave é classificado por uma chave. Você pode fazer a classificação "ad hoc" por outras colunas além da chave, e é rápido, mas não tão rápido como se você já tivesse feito a classificação.
Ari B. Friedman

Acho que a busca binária é mais rápida do que a varredura vetorial ao selecionar linhas. Não sou um cientista da computação, então não sei o que isso realmente significa. Além do FAQ, veja a introdução .
Frank

Respostas:


125

Pequena atualização: consulte também as novas vinhetas HTML . Esta edição destaca as outras vinhetas que planejamos.


Atualizei esta resposta novamente (fevereiro de 2016) devido ao novo on=recurso que também permite junções ad-hoc . Veja o histórico de respostas anteriores (desatualizadas).

O que exatamente faz setkey(DT, a, b)?

Ele faz duas coisas:

  1. reordena as linhas da tabela de dados DT pela (s) coluna (s) fornecida (s) ( a , b ) por referência , sempre em ordem crescente .
  2. marcas essas colunas como chave colunas, definindo um atributo chamado sortedpara DT.

A reordenação é rápida (devido à classificação de raiz interna da data.table ) e eficiente em termos de memória (apenas uma coluna extra do tipo double é alocada).

Quando é setkey()necessário?

Para operações de agrupamento, setkey()nunca foi um requisito absoluto. Ou seja, podemos executar um cold-by ou adhoc-by .

## "cold" by
require(data.table)
DT <- data.table(x=rep(1:5, each=2), y=1:10)
DT[, mean(y), by=x] # no key is set, order of groups preserved in result

No entanto, antes de v1.9.6, as junções do formulário x[i]devem keyser ativadas x. Com o novo on=argumento da v1.9.6 + , isso não é mais verdade e, portanto, definir as chaves também não é um requisito absoluto aqui.

## joins using < v1.9.6 
setkey(X, a) # absolutely required
setkey(Y, a) # not absolutely required as long as 'a' is the first column
X[Y]

## joins using v1.9.6+
X[Y, on="a"]
# or if the column names are x_a and y_a respectively
X[Y, on=c("x_a" = "y_a")]

Observe que o on=argumento também pode ser especificado explicitamente até mesmo para keyedjunções.

A única operação que precisa keyser absolutamente definida é a função foverlaps () . Mas estamos trabalhando em mais alguns recursos que, quando concluídos, removeriam esse requisito.

  • Então, qual é a razão para implementar o on=argumento?

    Existem algumas razões.

    1. Ele permite distinguir claramente a operação como uma operação envolvendo dois data.tables . Apenas fazer X[Y]não distingue isso também, embora pudesse ficar claro nomeando as variáveis ​​de forma adequada.

    2. Também permite entender as colunas nas quais a junção / subconjunto está sendo executada imediatamente, olhando para aquela linha de código (e não tendo que retornar à setkey()linha correspondente ).

    3. Em operações onde as colunas são adicionadas ou atualizadas por referência , as on=operações têm muito mais desempenho, pois não precisa que toda a tabela de dados seja reordenada apenas para adicionar / atualizar coluna (s). Por exemplo,

      ## compare 
      setkey(X, a, b) # why physically reorder X to just add/update a column?
      X[Y, col := i.val]
      
      ## to
      X[Y, col := i.val, on=c("a", "b")]

      No segundo caso, não foi necessário reordenar. Não é computar o pedido que consome tempo, mas reordenar fisicamente a tabela de dados na RAM e, ao evitá-la, mantemos a ordem original e ela também apresenta desempenho.

    4. Mesmo caso contrário, a menos que você esteja realizando junções repetidamente, não deve haver nenhuma diferença de desempenho perceptível entre junções com chave e ad-hoc .

Isso leva à pergunta: qual a vantagem de digitar um data.table ainda?

  • Existe uma vantagem em digitar um data.table?

    A digitação de uma tabela data.table reordena-a fisicamente com base nessas colunas na RAM. Calcular o pedido geralmente não é a parte demorada, mas sim o próprio reordenamento . No entanto, uma vez que classificamos os dados na RAM, as linhas pertencentes ao mesmo grupo são todas contíguas na RAM e, portanto, são muito eficientes em cache. É a classificação que acelera as operações em data.tables digitados.

    Portanto, é essencial descobrir se o tempo gasto em reordenar todos os dados. Tabela vale o tempo para fazer uma junção / agregação eficiente em cache. Normalmente, a menos que haja operações repetitivas de agrupamento / junção sendo realizadas na mesma tabela de dados com chave, não deve haver uma diferença perceptível.

Na maioria dos casos, portanto, não deve haver mais necessidade de definir as chaves. Recomendamos usar on=sempre que possível, a menos que a chave de configuração tenha uma melhoria dramática no desempenho que você gostaria de explorar.

Pergunta: Qual você acha que seria o desempenho em comparação a uma junção com chave , se você usar setorder()para reordenar data.table e usar on=? Se você acompanhou até agora, deve ser capaz de descobrir :-).


3
Legal, obrigado! Até agora, eu não tinha pensado sobre o que "pesquisa binária" realmente significava, nem realmente entendi o motivo pelo qual ela foi usada em vez de um hash.
Frank

@Arun, é DT[J(1e4:1e5)]realmente equivalente a DF[DF$x > 1e4 & DF$x < 1e5, ]? Você poderia me apontar o que Jsignifica? Além disso, essa pesquisa não retornaria nenhuma linha, pois sample(1e4, 1e7, TRUE)não inclui números acima de 1e4.
fishtank

@fishtank, neste caso, deve ser >=e <=- corrigido. J(e .) são apelidos para list(ou seja, são equivalentes). Internamente, quando ié uma lista, é convertido em data.table seguindo a qual a pesquisa binária é usada para calcular índices de linha. Corrigido 1e4para 1e5evitar confusão. Obrigado por detectar. Observe que agora podemos usar o on=argumento diretamente para executar subconjuntos binários em vez de definir a chave. Leia mais nas novas vinhetas HTML . E fique de olho nessa página para vinhetas de junções.
Arun

talvez isso pudesse ser uma atualização mais completa? a seção "quando necessário" parece desatualizada, por exemplo
MichaelChirico

Que função indica a chave que está sendo usada?
skan de

20

Uma chave é basicamente um índice em um conjunto de dados, o que permite operações de classificação, filtro e junção muito rápidas e eficientes. Essas são provavelmente as melhores razões para usar tabelas de dados em vez de quadros de dados (a sintaxe para usar tabelas de dados também é muito mais amigável, mas isso não tem nada a ver com chaves).

Se você não entende os índices, considere o seguinte: uma lista telefônica é "indexada" por nome. Portanto, se eu quiser procurar o número de telefone de alguém, é muito simples. Mas suponha que eu queira pesquisar por número de telefone (por exemplo, procurar quem tem um número de telefone específico). A menos que eu possa "reindexar" a lista telefônica por número de telefone, levará muito tempo.

Considere o seguinte exemplo: suponha que eu tenha uma tabela, CEP, de todos os códigos postais dos EUA (> 33.000) junto com as informações associadas (cidade, estado, população, renda média etc.). Se eu quiser pesquisar as informações de um CEP específico, a pesquisa (filtro) é cerca de 1000 vezes mais rápida se eu setkey(ZIP,zipcode)primeiro.

Outro benefício tem a ver com associações. Suponha que eu tenha uma lista de pessoas e seus CEPs em uma tabela de dados (chame-a de "PPL") e eu queira acrescentar informações da tabela ZIP (por exemplo, cidade, estado e assim por diante). O código a seguir fará isso:

setkey(ZIP,zipcode)
setkey(PPL,zipcode)
full.info <- PPL[ZIP, nomatch=F]

Esta é uma "junção" no sentido de que estou juntando as informações de 2 tabelas baseadas em um campo comum (CEP). Junções como essa em tabelas muito grandes são extremamente lentas com quadros de dados e extremamente rápidas com tabelas de dados. Em um exemplo da vida real, tive que fazer mais de 20.000 junções como essa em uma tabela completa de CEPs. Com tabelas de dados, o script demorou cerca de 20 minutos. para correr. Eu nem tentei com frames de dados porque levaria mais de 2 semanas.

IMHO você não deve apenas ler, mas estudar o material de FAQ e Intro. É mais fácil de entender se você tiver um problema real ao qual aplicar isso.

[Resposta ao comentário de @Frank]

Re: classificação vs. indexação - Com base na resposta a esta pergunta , parece que de setkey(...)fato reorganiza as colunas na tabela (por exemplo, uma classificação física) e não cria um índice no sentido do banco de dados. Isso tem algumas implicações práticas: por um lado, se você definir a chave em uma tabela com setkey(...)e depois alterar qualquer um dos valores na coluna da chave, data.table simplesmente declara que a tabela não está mais classificada (desativando o sortedatributo); ele não é reindexado dinamicamente para manter a ordem de classificação apropriada (como aconteceria em um banco de dados). Além disso, "remover a chave" usando setky(DT,NULL)faz não restaurar a tabela para a sua ordem original, indiferenciados.

Re: filtro vs. junção - a diferença prática é que a filtragem extrai um subconjunto de um único conjunto de dados, enquanto a junção combina dados de dois conjuntos de dados com base em um campo comum. Existem muitos tipos diferentes de junção (interna, externa, esquerda). O exemplo acima é uma junção interna (apenas registros com chaves comuns a ambas as tabelas são retornados), e isso tem muitas semelhanças com a filtragem.


1
+1. Em relação à sua primeira frase ... já está resolvido né? E uma junção não é um caso especial de filtro (ou uma operação que leva a filtragem como sua primeira etapa)? Parece que a "melhor filtragem" resume todo o benefício.
Frank

1
Ou melhor digitalização, suponho.
Wet Feet

1
@jlhoward Obrigado. Minha crença anterior era que a classificação não estava entre os benefícios de definir a chave (já que se você deseja classificar, você deve apenas classificar) e também que setkeyrealmente reordena as linhas irreversivelmente. Se for apenas para fins de exibição, como faço para imprimir as primeiras dez linhas de acordo com a ordem "verdadeira" (que eu teria visto antes de setkey)? Tenho certeza setkey(DT,NULL)que não faz isso ... (cont.)
Frank

... (cont.) Além disso, não olhei para o código do pacote, mas para ingressar X[Y,...], você precisa "filtrar" as linhas de X usando a chave. Concedido, outras coisas acontecem depois disso (as colunas de Y são disponibilizadas, e há um por-sem-por implícito), mas ainda não vejo isso como um benefício conceitualmente distinto. Acho que sua resposta é colocada em termos de operações que você pode querer fazer, onde a distinção pode ser útil.
Frank

1
@Frank - setkey(DT,NULL)remove a chave, mas não afeta a ordem de classificação. Fiz uma pergunta sobre isso aqui . Vamos ver.
jlhoward de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.