Como usar corretamente listas em R?

320

Breve histórico: Muitas (mais?) Linguagens de programação contemporâneas de uso amplo têm pelo menos um punhado de ADTs [tipos de dados abstratos] em comum, em particular,

  • string (uma sequência composta de caracteres)

  • lista (uma coleção ordenada de valores) e

  • tipo baseado em mapa (uma matriz não ordenada que mapeia chaves para valores)

Na linguagem de programação R, os dois primeiros são implementados como charactere vector, respectivamente.

Quando comecei a aprender R, duas coisas eram óbvias quase desde o início: listé o tipo de dados mais importante em R (porque é a classe pai para o R data.frame) e, segundo, eu simplesmente não conseguia entender como eles funcionavam, pelo menos não é bom o suficiente para usá-los corretamente no meu código.

Por um lado, pareceu-me que o listtipo de dados de R era uma implementação direta do mapa ADT ( dictionaryem Python, NSMutableDictionaryem Objective C, hashem Perl e Ruby, object literalem Javascript e assim por diante).

Por exemplo, você os cria da mesma forma que faria com um dicionário Python, passando pares de valores-chave para um construtor (o que em Python dictnão é list):

x = list("ev1"=10, "ev2"=15, "rv"="Group 1")

E você acessa os itens de uma lista R exatamente como faria em um dicionário Python, por exemplo x['ev1'],. Da mesma forma, você pode recuperar apenas as 'chaves' ou apenas os 'valores' :

names(x)    # fetch just the 'keys' of an R list
# [1] "ev1" "ev2" "rv"

unlist(x)   # fetch just the 'values' of an R list
#   ev1       ev2        rv 
#  "10"      "15" "Group 1" 

x = list("a"=6, "b"=9, "c"=3)  

sum(unlist(x))
# [1] 18

mas Rs listtambém são diferentes de outros ADTs do tipo mapa (dentre os idiomas que aprendi de qualquer maneira). Meu palpite é que isso é uma consequência da especificação inicial para S, ou seja, uma intenção de projetar uma DSL de dados / estatísticas [idioma específico do domínio] a partir do zero.

três diferenças significativas entre Rs liste tipos de mapeamento em outros idiomas em uso generalizado (por exemplo, Python, Perl, JavaScript):

primeiro , lists em R são uma coleção ordenada , assim como vetores, mesmo que os valores sejam digitados (ou seja, as chaves podem ser qualquer valor que possa ser lavável e não apenas números inteiros seqüenciais). Quase sempre, o tipo de dados de mapeamento em outros idiomas não é ordenado .

segundo , lists podem ser retornados de funções, mesmo que você nunca tenha passado um listquando você chamou a função, e mesmo que a função que retornou listnão contenha um listconstrutor ( explícito) (é claro, você pode lidar com isso na prática, agrupando o resultado retornado em uma chamada para unlist):

x = strsplit(LETTERS[1:10], "")     # passing in an object of type 'character'

class(x)                            # returns 'list', not a vector of length 2
# [1] list

Uma terceira característica peculiar de R's list: não parece que eles possam ser membros de outro ADT e, se você tentar fazer isso, o contêiner primário será coagido a a list. Por exemplo,

x = c(0.5, 0.8, 0.23, list(0.5, 0.2, 0.9), recursive=TRUE)

class(x)
# [1] list

minha intenção aqui não é criticar a linguagem ou como ela é documentada; Da mesma forma, não estou sugerindo que haja algo errado com a listestrutura de dados ou como ela se comporta. Tudo o que eu preciso é corrigir o meu entendimento de como eles funcionam, para que eu possa usá-los corretamente no meu código.

Aqui estão os tipos de coisas que eu gostaria de entender melhor:

  • Quais são as regras que determinam quando uma chamada de função retornará uma list(por exemplo, strsplitexpressão recitada acima)?

  • Se eu não atribuir explicitamente nomes a list(por exemplo, list(10,20,30,40)) , os nomes padrão são apenas números inteiros sequenciais começando com 1? (Presumo, mas estou longe de ter certeza de que a resposta é sim, caso contrário não poderíamos coagir esse tipo de listvetor a uma chamada para unlist.)

  • Por que esses dois operadores diferentes,, []e [[]]retornam o mesmo resultado?

    x = list(1, 2, 3, 4)

    ambas as expressões retornam "1":

    x[1]

    x[[1]]

  • por que essas duas expressões não retornam o mesmo resultado?

    x = list(1, 2, 3, 4)

    x2 = list(1:4)

Por favor, não me aponte para a Documentação R ( ?list, R-intro) - Li-a com atenção e não me ajuda a responder ao tipo de perguntas que recitei logo acima.

(por último, eu aprendi recentemente e comecei a usar um pacote R (disponível no CRAN) chamado hashque implementa o comportamento do tipo de mapa convencional por meio de uma classe S4; certamente posso recomendar este pacote.)

doug
fonte
3
Com x = list(1, 2, 3, 4), ambos NÃO retornam o mesmo resultado:, x[1]e x[[1]]. O primeiro retorna uma lista e o segundo retorna um vetor numérico. Rolando abaixo, parece-me que Dirk foi o único entrevistado a responder corretamente a essa pergunta.
IRTFM
2
Eu não notei que alguém expandisse sua lista de maneiras que listem R não é como um hash. Tenho mais uma que acho digna de nota. listem R pode ter dois membros com o mesmo nome de referência. Considere que obj <- c(list(a=1),list(a=2))é válido e retorna uma lista com dois valores nomeados de 'a'. Nesse caso, uma chamada obj["a"]retornará apenas o primeiro elemento da lista correspondente. Você pode obter um comportamento semelhante (talvez idêntico) a um hash com apenas um item por nomes referenciados usando ambientes em R. por exemplox <- new.env(); x[["a"]] <- 1; x[["a"]] <- 2; x[["a"]]
russellpierce
1
Reli esta postagem com as respostas três vezes nos últimos 6 meses e encontrei mais iluminação a cada vez. Ótima pergunta e algumas ótimas respostas. Obrigado.
Rich Lysakowski PhD

Respostas:

150

Apenas para abordar a última parte da sua pergunta, já que isso realmente indica a diferença entre a liste vectorem R:

Por que essas duas expressões não retornam o mesmo resultado?

x = lista (1, 2, 3, 4); x2 = lista (1: 4)

Uma lista pode conter qualquer outra classe como cada elemento. Assim, você pode ter uma lista em que o primeiro elemento é um vetor de caractere, o segundo é um quadro de dados, etc. Nesse caso, você criou duas listas diferentes. xpossui quatro vetores, cada um com comprimento 1. x2possui 1 vetor com comprimento 4:

> length(x[[1]])
[1] 1
> length(x2[[1]])
[1] 4

Então, essas são listas completamente diferentes.

As listas R são muito parecidas com uma estrutura de dados de mapa de hash , em que cada valor de índice pode ser associado a qualquer objeto. Aqui está um exemplo simples de uma lista que contém 3 classes diferentes (incluindo uma função):

> complicated.list <- list("a"=1:4, "b"=1:3, "c"=matrix(1:4, nrow=2), "d"=search)
> lapply(complicated.list, class)
$a
[1] "integer"
$b
[1] "integer"
$c
[1] "matrix"
$d
[1] "function"

Dado que o último elemento é a função de pesquisa, posso chamar assim:

> complicated.list[["d"]]()
[1] ".GlobalEnv" ...

Como um comentário final sobre isso: deve-se notar que a data.frameé realmente uma lista (da data.framedocumentação):

Um quadro de dados é uma lista de variáveis ​​do mesmo número de linhas com nomes de linhas exclusivos, dada a classe '"data.frame"'

É por isso que as colunas em uma data.framepodem ter tipos de dados diferentes, enquanto as colunas em uma matriz não podem. Como exemplo, aqui eu tento criar uma matriz com números e caracteres:

> a <- 1:4
> class(a)
[1] "integer"
> b <- c("a","b","c","d")
> d <- cbind(a, b)
> d
 a   b  
[1,] "1" "a"
[2,] "2" "b"
[3,] "3" "c"
[4,] "4" "d"
> class(d[,1])
[1] "character"

Observe como não posso alterar o tipo de dados na primeira coluna para numérico porque a segunda coluna possui caracteres:

> d[,1] <- as.numeric(d[,1])
> class(d[,1])
[1] "character"
Shane
fonte
4
Isso ajuda, obrigado. (A propósito, seu exemplo de 'lista complicada', como você já deve saber, é a maneira padrão de replicar a instrução 'switch' em C ++, Java etc. em linguagens que não possuem uma; provavelmente é uma boa maneira para fazer isso no R quando preciso). +1
doug
8
Certo, embora exista uma switchfunção útil em R que possa ser usada para esse fim (ver help(switch)).
Shane
63

Em relação às suas perguntas, deixe-me resolvê-las em ordem e dar alguns exemplos:

1 ) Uma lista será retornada se e quando a instrução de retorno adicionar uma. Considerar

 R> retList <- function() return(list(1,2,3,4)); class(retList())
 [1] "list"
 R> notList <- function() return(c(1,2,3,4)); class(notList())
 [1] "numeric"
 R> 

2 ) Os nomes simplesmente não estão definidos:

R> retList <- function() return(list(1,2,3,4)); names(retList())
NULL
R> 

3 ) Eles não retornam a mesma coisa. Seu exemplo dá

R> x <- list(1,2,3,4)
R> x[1]
[[1]]
[1] 1
R> x[[1]]
[1] 1

onde x[1]retorna o primeiro elemento de x- que é o mesmo que x. Todo escalar é um vetor de comprimento um. Por outro lado, x[[1]]retorna o primeiro elemento da lista.

4 ) Por fim, os dois são diferentes entre eles, criando, respectivamente, uma lista contendo quatro escalares e uma lista com um único elemento (que por acaso é um vetor de quatro elementos).

Dirk Eddelbuettel
fonte
1
Muito útil, obrigado. (Reponha o item 1 na sua resposta - eu concordo, mas o que eu tinha em mente era embutido como 'strsplit', não funções criadas pelo usuário). De qualquer forma, +1 de mim.
doug
2
@doug Sobre o item 1 Acho que a única maneira é verificar a ajuda para uma função específica, seção Value. Como em ?strsplit: "Uma lista do mesmo tamanho que x". Mas você deve considerar que pode haver uma função retornar valores diferentes, dependendo dos argumentos (por exemplo, sapply pode retornar lista ou vetor).
Marek
34

Apenas para fazer um subconjunto de suas perguntas:

Este artigo sobre indexação aborda a questão da diferença entre []e [[]].

Em resumo [[]] seleciona um único item de uma lista e []retorna uma lista dos itens selecionados. No seu exemplo, o x = list(1, 2, 3, 4)'item 1 é um único inteiro, mas x[[1]]retorna um único 1 e x[1]retorna uma lista com apenas um valor.

> x = list(1, 2, 3, 4)
> x[1]
[[1]]
[1] 1

> x[[1]]
[1] 1
JD Long
fonte
A propósito, A = array( 11:16, c(2,3) ); A[5]é 15, na matriz plana ?!
Denis
13

Um dos motivos pelos quais as listas funcionam (ordenadas) é atender à necessidade de um contêiner ordenado que possa conter qualquer tipo em qualquer nó, que vetores não possuem. As listas são reutilizadas para diversos propósitos em R, incluindo a formação da base de a data.frame, que é uma lista de vetores do tipo arbitrário (mas com o mesmo comprimento).

Por que essas duas expressões não retornam o mesmo resultado?

x = list(1, 2, 3, 4); x2 = list(1:4)

Para adicionar à resposta de @ Shane, se você deseja obter o mesmo resultado, tente:

x3 = as.list(1:4)

O que força o vetor 1:4em uma lista.

Alex Brown
fonte
11

Apenas para adicionar mais um ponto a isso:

R tem uma estrutura de dados equivalente à Dict Python no hashpacote . Você pode ler sobre isso nesta postagem do blog do Open Data Group . Aqui está um exemplo simples:

> library(hash)
> h <- hash( keys=c('foo','bar','baz'), values=1:3 )
> h[c('foo','bar')]
<hash> containing 2 key-value pairs.
  bar : 2
  foo : 1

Em termos de usabilidade, a hashclasse é muito semelhante a uma lista. Mas o desempenho é melhor para grandes conjuntos de dados.

Shane
fonte
1
Estou ciente do pacote de hash - ele é mencionado na minha pergunta original como um proxy adequado para o tipo de hash tradicional.
doug
Observe também que o uso de hash :: hash é de utilidade questionável em relação aos ambientes de hash, rpubs.com/rpierce/hashBenchmarks .
russellpierce
9

Você diz:

Por outro lado, as listas podem ser retornadas de funções, mesmo que você nunca tenha passado em uma Lista quando você chamou a função e mesmo que a função não contenha um construtor de Lista, por exemplo,

x = strsplit(LETTERS[1:10], "") # passing in an object of type 'character'
class(x)
# => 'list'

E eu acho que você sugere que isso é um problema (?). Estou aqui para lhe dizer por que não é um problema :-). Seu exemplo é um pouco simples, pois quando você faz a divisão de cadeias, você tem uma lista com elementos com 1 elemento, então você sabe que x[[1]]é o mesmo que unlist(x)[1]. Mas e se o resultado dos strsplitresultados retornados de comprimento diferente em cada posição. Simplesmente retornar um vetor (versus uma lista) não funciona.

Por exemplo:

stuff <- c("You, me, and dupree",  "You me, and dupree",
           "He ran away, but not very far, and not very fast")
x <- strsplit(stuff, ",")
xx <- unlist(strsplit(stuff, ","))

No primeiro caso ( x: que retorna uma lista), você pode dizer que a "parte" 2nd da 3ª corda foi, por exemplo: x[[3]][2]. Como você pode fazer o mesmo usando xxagora que os resultados foram "desvendados" ( unlist-ed)?

Steve Lianoglou
fonte
5
x = list(1, 2, 3, 4)
x2 = list(1:4)
all.equal(x,x2)

não é o mesmo porque 1: 4 é o mesmo que c (1,2,3,4). Se você deseja que eles sejam iguais, então:

x = list(c(1,2,3,4))
x2 = list(1:4)
all.equal(x,x2)
JeremyS
fonte
4

Essa é uma pergunta muito antiga, mas acho que uma nova resposta pode agregar algum valor, pois, na minha opinião, ninguém abordou diretamente algumas das preocupações do OP.

Apesar do que as respostas aceitas sugerem, os listobjetos em R não são mapas de hash. Se você deseja fazer um paralelo com o python, listé mais parecido com o python lists (ou tuplena verdade).

É melhor descrever como a maioria dos objetos R é armazenada internamente (o tipo C de um objeto R é SEXP). Eles são feitos basicamente de três partes:

  • um cabeçalho, que declara o tipo R do objeto, o comprimento e alguns outros metadados;
  • a parte de dados, que é uma matriz alocada em heap C padrão (bloco contíguo de memória);
  • os atributos, que são uma lista vinculada de ponteiros nomeados para outros objetos R (ou NULLse o objeto não tiver atributos).

Do ponto de vista interno, há pouca diferença entre a liste um numericvetor, por exemplo. Os valores que eles armazenam são apenas diferentes. Vamos quebrar dois objetos no paradigma descrito anteriormente:

x <- runif(10)
y <- list(runif(10), runif(3))

Para x:

  • O cabeçalho dirá que o tipo é numeric( REALSXPno lado C), o comprimento é 10 e outras coisas.
  • A parte dos dados será uma matriz contendo 10 doublevalores.
  • Os atributos são NULL, pois o objeto não possui nenhum.

Para y:

  • O cabeçalho dirá que o tipo é list( VECSXPno lado C), o comprimento é 2 e outras coisas.
  • A parte dos dados será uma matriz contendo 2 ponteiros para dois tipos de SEXP, apontando para o valor obtido por runif(10)e runif(3)respectivamente.
  • Os atributos são NULL, como para x.

Portanto, a única diferença entre um numericvetor e a listé que a numericparte de dados é composta de doublevalores, enquanto que a listparte de dados é uma matriz de ponteiros para outros objetos R.

O que acontece com os nomes? Bem, nomes são apenas alguns dos atributos que você pode atribuir a um objeto. Vamos ver o objeto abaixo:

z <- list(a=1:3, b=LETTERS)
  • O cabeçalho dirá que o tipo é list( VECSXPno lado C), o comprimento é 2 e outras coisas.
  • A parte dos dados será uma matriz contendo 2 ponteiros para dois tipos de SEXP, apontando para o valor obtido por 1:3e LETTERSrespectivamente.
  • Os atributos estão agora presentes e são um namescomponente que é um characterobjeto R com valor c("a","b").

No nível R, você pode recuperar os atributos de um objeto com a attributesfunção

O valor-chave típico de um mapa de hash em R é apenas uma ilusão. Quando voce diz:

z[["a"]]

Isto é o que acontece:

  • a [[função de subconjunto é chamada;
  • o argumento da função ( "a") é do tipo character; portanto, o método é instruído a pesquisar esse valor no namesatributo (se presente) do objeto z;
  • se o namesatributo não estiver lá, NULLserá retornado;
  • se presente, o "a"valor é pesquisado nele. Se "a"não for um nome do objeto, NULLserá retornado;
  • se presente, a posição é determinada (1 no exemplo). Portanto, o primeiro elemento da lista é retornado, ou seja, o equivalente a z[[1]].

A pesquisa de valor-chave é bastante indireta e é sempre posicional. Além disso, é útil ter em mente:

  • nos mapas de hash, o único limite que uma chave deve ter é que deve ser lavável . namesem R devem ser cadeias ( charactervetores);
  • nos mapas de hash, você não pode ter duas chaves idênticas. Em R, você pode atribuir namesa um objeto com valores repetidos. Por exemplo:

    names(y) <- c("same", "same")

    é perfeitamente válido em R. Quando você tenta, y[["same"]]o primeiro valor é recuperado. Você deve saber o porquê neste momento.

Em conclusão, a capacidade de atribuir atributos arbitrários a um objeto fornece a aparência de algo diferente de um ponto de vista externo. Mas Rs listnão são mapas de hash de forma alguma.

nicola
fonte
2

Em relação aos vetores e ao conceito de hash / array de outros idiomas:

  1. Os vetores são os átomos de R. Por exemplo, rpois(1e4,5)(5 números aleatórios), numeric(55)(vetor zero com mais de 55 vezes o dobro) e character(12)(12 cadeias vazias), são todos "básicos".

  2. Listas ou vetores podem ter names.

    > n = numeric(10)
    > n
     [1] 0 0 0 0 0 0 0 0 0 0
    > names(n)
    NULL
    > names(n) = LETTERS[1:10]
    > n
    A B C D E F G H I J 
    0 0 0 0 0 0 0 0 0 0
  3. Os vetores exigem que tudo seja do mesmo tipo de dados. Vê isto:

    > i = integer(5)
    > v = c(n,i)
    > v
    A B C D E F G H I J           
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    > class(v)
    [1] "numeric"
    > i = complex(5)
    > v = c(n,i)
    > class(v)
    [1] "complex"
    > v
       A    B    C    D    E    F    G    H    I    J                          
    0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i 0+0i
  4. As listas podem conter tipos de dados variados, como visto em outras respostas e na própria pergunta do OP.

Vi linguagens (ruby, javascript) nas quais "matrizes" podem conter tipos de dados variáveis, mas, por exemplo, em C ++, "matrizes" devem ser todos do mesmo tipo de dados. Eu acredito que isso é uma questão de velocidade / eficiência: se você tem um, numeric(1e6)sabe o tamanho e a localização de cada elemento a priori ; se a coisa puder conter "Flying Purple People Eaters"alguma fatia desconhecida, você precisará analisar as coisas para conhecer fatos básicos sobre ela.

Certas operações R padrão também fazem mais sentido quando o tipo é garantido. Por exemplo, cumsum(1:9)faz sentido, ao passo cumsum(list(1,2,3,4,5,'a',6,7,8,9))que não, sem que o tipo seja garantido como duplo.


Quanto à sua segunda pergunta:

As listas podem ser retornadas de funções, mesmo que você nunca tenha passado em uma Lista quando chamou a função

As funções retornam tipos de dados diferentes dos que são inseridos o tempo todo. plotretorna uma plotagem, mesmo que ela não seja plotada como entrada. Argretorna a numericmesmo que tenha aceito a complex. Etc.

(E quanto a strsplit: o código fonte está aqui .)

isomorfismos
fonte
2

Embora essa seja uma pergunta bastante antiga, devo dizer que está tocando exatamente o conhecimento que estava perdendo durante meus primeiros passos no R - ou seja, como expressar dados na minha mão como objeto no R ou como selecionar objetos existentes. Não é fácil para um novato em R pensar "em uma caixa em R" desde o início.

Então eu mesmo comecei a usar muletas abaixo, o que me ajudou muito a descobrir qual objeto usar para quais dados e basicamente a imaginar o uso no mundo real.

Embora eu não esteja dando respostas exatas para a pergunta, o pequeno texto abaixo pode ajudar o leitor que acabou de começar com R e está fazendo perguntas semelhantes.

  • Vetor atômico ... Chamei essa "sequência" para mim, sem direção, apenas sequência dos mesmos tipos. [subconjuntos.
  • Vetor ... sequência com uma direção do 2D, [subconjuntos.
  • Matriz ... monte de vetores com o mesmo comprimento formando linhas ou colunas, [subconjuntos por linhas e colunas ou por sequência.
  • Matrizes ... matrizes em camadas formando 3D
  • Dataframe ... uma tabela 2D como no excel, onde posso classificar, adicionar ou remover linhas ou colunas ou fazer arit. operações com eles, somente depois de algum tempo eu realmente reconheci que o dataframe é uma implementação inteligente de listonde posso subconjunto usando [linhas e colunas, mas mesmo usando [[.
  • Lista ... para me ajudar, pensei na lista a partir de tree structureonde [i]seleciona e retorna ramificações inteiras e [[i]]retorna itens da ramificação. E, como é tree like structure, você pode até usar um index sequencepara endereçar cada folha de uma forma muito complexa listusando sua [[index_vector]]. As listas podem ser simples ou muito complexas e podem misturar vários tipos de objetos em um.

Assim, listsvocê pode descobrir mais maneiras de selecionar uma leafdependendo da situação, como no exemplo a seguir.

l <- list("aaa",5,list(1:3),LETTERS[1:4],matrix(1:9,3,3))
l[[c(5,4)]] # selects 4 from matrix using [[index_vector]] in list
l[[5]][4] # selects 4 from matrix using sequential index in matrix
l[[5]][1,2] # selects 4 from matrix using row and column in matrix

Essa maneira de pensar me ajudou muito.

Petr Matousu
fonte
1

Se ajudar, eu tendem a conceber "listas" em R como "registros" em outros idiomas pré-OO:

  • eles não fazem suposições sobre um tipo abrangente (ou melhor, o tipo de todos os registros possíveis de qualquer nome de campo e aridade está disponível).
  • seus campos podem ser anônimos (então você os acessa por ordem estrita de definição).

O nome "registro" entraria em conflito com o significado padrão de "registros" (também conhecido como linhas) na linguagem do banco de dados, e pode ser por isso que o nome deles se sugeriu: como listas (de campos).

Francisco J. Valverde Albacete
fonte
1

por que esses dois operadores diferentes [ ], e [[ ]]retornam o mesmo resultado?

x = list(1, 2, 3, 4)
  1. [ ]fornece operação de subconfiguração. Em geral, o subconjunto de qualquer objeto terá o mesmo tipo que o objeto original. Portanto, x[1] fornece uma lista. Da mesma forma, x[1:2]é um subconjunto da lista original, portanto, é uma lista. Ex.

    x[1:2]
    
    [[1]] [1] 1
    
    [[2]] [1] 2
  2. [[ ]]é para extrair um elemento da lista. x[[1]]é válido e extrai o primeiro elemento da lista. x[[1:2]]não é válido, pois [[ ]] não fornece subconjuntos como [ ].

     x[[2]] [1] 2 
    
    > x[[2:3]] Error in x[[2:3]] : subscript out of bounds
HariG
fonte