Как правильно использовать списки в R?

Краткая история. Многие (большинство?) современных языков программирования в широком использовании имеют, по крайней мере, несколько ADT [абстрактных типов данных], в частности,

  • строка (последовательность, состоящая из символов)

  • список (упорядоченный набор значений) и

  • тип на основе карты (неупорядоченный массив, который сопоставляет ключи значениям)

На языке программирования R первые два реализованы как character и vector соответственно.

Когда я начал изучать R, две вещи были очевидны почти с самого начала: list - самый важный тип данных в R (потому что это родительский класс для R data.frame), а во-вторых, я просто не мог Не понимаю, как они работают, по крайней мере, недостаточно хорошо, чтобы правильно использовать их в моем коде.

Во-первых, мне казалось, что тип данных R list представляет собой прямую реализацию карты ADT (dictionary в Python, NSMutableDictionary в Objective C, hash в Perl и Ruby, object literal в Javascript и т.д.).

Например, вы создаете их так же, как и словарь Python, передавая пары ключ-значение конструктору (который в Python dict not list):

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

И вы получаете доступ к элементам R-списка так же, как и словам Python, например x['ev1']. Аналогично, вы можете получить только "ключи" или "значения":

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

но R list также в отличие от других ADT-адресов типа карты (из всех языков, которые я узнал в любом случае). Я предполагаю, что это является следствием исходной спецификации для S, т.е. Намерения разработать DSL данных/статистики [язык, специфичный для домена] с нуля.

три существенных различия между R list и типами отображения на других языках в широком использовании (например, Python, Perl, JavaScript):

во-первых, list в R - упорядоченный набор, точно так же, как и векторы, даже если значения вводятся ключом (т.е. ключи могут быть любым хешируемым значением, а не только целыми целыми числами). Почти всегда тип данных сопоставления на других языках неупорядочен.

second, list может быть возвращен из функций, даже если вы никогда не передавали в list при вызове функции, и даже если функция, которая вернула list, не содержит (явного) list constructor (Конечно, вы можете справиться с этим на практике, обернув возвращаемый результат при вызове 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

Третья особенность R list s: кажется, что они не могут быть членами другого ADT, и если вы попытаетесь это сделать, то основной контейнер будет принудительно привязан к list. Например.

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

class(x)
# [1] list

мое намерение здесь не критиковать язык или то, как оно документировано; Аналогично, я не предполагаю, что что-то не так с структурой данных list или тем, как она ведет себя. Все, что мне нужно - это правильно понять, как они работают, поэтому я могу правильно использовать их в своем коде.

Вот что я хотел бы лучше понять:

  • Каковы правила, определяющие, когда вызов функции вернет выражение list (например, strsplit, указанное выше)?

  • Если я не могу явно назначать имена для list (например, list(10,20,30,40)), это имена по умолчанию только последовательные целые числа, начинающиеся с 1? (Я предполагаю, но я далек от уверенности в том, что ответ "да", иначе мы не смогли бы принудить этот тип list к вектору w/к вызову unlist.)

  • Почему эти два разных оператора, [] и [[]], возвращают тот же результат?

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

    оба выражения возвращают "1":

    x[1]

    x[[1]]

  • почему эти два выражения не возвращают тот же результат?

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

    x2 = list(1:4)

Пожалуйста, не указывайте мне на документацию R (?list, R-intro). Я внимательно прочитал его, и это не помогает мне ответить на вопрос, который я читал чуть выше.

(наконец, я недавно узнал и начал использовать R-пакет (доступный на CRAN) под названием hash, который реализует обычный тип карты поведение через класс S4, я могу, конечно, рекомендовать этот пакет.)

Ответ 1

Просто чтобы ответить на последнюю часть вашего вопроса, так как это действительно указывает на разницу между list и vector в R:

Почему эти два выражения не возвращают один и тот же результат?

x = список (1, 2, 3, 4); x2 = список (1: 4)

Список может содержать любой другой класс в качестве каждого элемента. Таким образом, у вас может быть список, в котором первый элемент является символьным вектором, второй - фреймом данных и т.д. В этом случае вы создали два разных списка. x имеет четыре вектора, каждая из которых имеет длину 1. x2 имеет 1 вектор длины 4:

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

Итак, это совершенно разные списки.

Списки

R очень похожи на структуру данных хеш-карту, в которой каждое значение индекса может быть связано с любым объектом. Вот простой пример списка, который содержит 3 разных класса (включая функцию):

> 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"

Учитывая, что последний элемент является функцией поиска, я могу вызвать его так:

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

В качестве заключительного комментария к этому: следует отметить, что a data.frame - это действительно список (из документации data.frame):

Кадр данных представляет собой список переменных из того же числа строк с уникальными именами строк, заданный классом "data.frame"

Вот почему столбцы в data.frame могут иметь разные типы данных, а столбцы в матрице не могут. В качестве примера здесь я пытаюсь создать матрицу с числами и символами:

> 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"

Обратите внимание, как я не могу изменить тип данных в первом столбце на числовой, потому что второй столбец имеет символы:

> d[,1] <- as.numeric(d[,1])
> class(d[,1])
[1] "character"

Ответ 2

Что касается ваших вопросов, позвольте мне привести их в порядок и привести несколько примеров:

1) Список возвращается, если и когда оператор return добавляет его. Рассмотрим

 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) Имена просто не заданы:

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

3) Они не возвращают то же самое. В вашем примере

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

где x[1] возвращает первый элемент x - который совпадает с x. Каждый скаляр является вектором длины один. С другой стороны x[[1]] возвращает первый элемент списка.

4). Наконец, они отличаются друг от друга тем, что они создают, соответственно, список, содержащий четыре скаляра, и список с одним элементом (который представляет собой вектор из четырех элементов).

Ответ 3

Просто возьмите подмножество своих вопросов:

В этой статье об индексации рассматривается вопрос о разнице между [] и [[]].

Короче [[]] выбирает один элемент из списка, а [] возвращает список выбранных элементов. В вашем примере x = list(1, 2, 3, 4)' элемент 1 представляет собой одно целое число, но x[[1]] возвращает один 1 и x[1] возвращает список только с одним значением.

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

> x[[1]]
[1] 1

Ответ 4

Одна из причин приводит к тому, что работа, которую они выполняют (заказываются), предназначена для удовлетворения потребности в упорядоченном контейнере, который может содержать любой тип в любом node, который не выполняется. Списки повторно используются для различных целей в R, включая формирование базы data.frame, которая представляет собой список векторов произвольного типа (но с той же длиной).

Почему эти два выражения не возвращают один и тот же результат?

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

Чтобы добавить к ответу @Shane, если вы хотите получить тот же результат, попробуйте:

x3 = as.list(1:4)

Который принуждает вектор 1:4 к списку.

Ответ 5

Просто добавьте еще один момент:

R имеет структуру данных, эквивалентную Python dict в пакете hash. Вы можете прочитать об этом в этом сообщении в блоге из группы открытых данных. Вот простой пример:

> 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

В терминах удобства использования класс hash очень похож на список. Но производительность лучше для больших наборов данных.

Ответ 6

Вы говорите:

С другой стороны, списки могут быть возвращены от функций, даже если вы никогда в списке, когда вы вызвали функции, и хотя функция не содержит конструктор List, например.

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

И я думаю, вы предполагаете, что это проблема (?). Я здесь, чтобы рассказать вам, почему это не проблема:-). Ваш пример немного прост, в том случае, когда вы выполняете разделение строк, у вас есть список с элементами длиной 1 элемент, поэтому вы знаете, что x[[1]] совпадает с unlist(x)[1]. Но что, если результат strsplit возвращал результаты разной длины в каждый бит. Простое возвращение вектора (по сравнению с списком) вообще не будет выполняться.

Например:

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, ","))

В первом случае (x: который возвращает список) вы можете указать, что было 2-й "частью" третьей строки, например: x[[3]][2]. Как вы могли бы сделать то же самое с помощью xx теперь, когда результаты были "распущены" (unlist -ed)?

Ответ 7

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

не то же самое, поскольку 1: 4 совпадает с c (1,2,3,4). Если вы хотите, чтобы они были такими же:

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

Ответ 8

Относительно векторов и концепции хэш/массив от других языков:

  • Векторы представляют собой атомы R. Например, rpois(1e4,5) (5 случайных чисел), numeric(55) (длина - 55 нулевой вектор над удвоениями), а character(12) (12 пустых строк) - все "основной".

  • Либо списки, либо векторы могут иметь 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
    
  • Векторы требуют, чтобы все было одним и тем же типом данных. Смотрите это:

    > 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
    
  • Списки могут содержать различные типы данных, как видно из других ответов и самого вопроса OP.

Я видел языки (ruby, javascript), в которых "массивы" могут содержать переменные типы данных, но, например, в С++ "массивы" должен быть одинаковый тип данных. Я считаю, что это скорость/эффективность: если у вас есть numeric(1e6), вы знаете его размер и расположение каждого элемента априори; если предмет может содержать "Flying Purple People Eaters" в некотором неизвестном фрагменте, тогда вы должны фактически разбирать материал, чтобы знать основные факты об этом.

Некоторые стандартные операции R также имеют больше смысла, когда тип гарантирован. Например, cumsum(1:9) имеет смысл, тогда как cumsum(list(1,2,3,4,5,'a',6,7,8,9)) нет, без гарантированного двойного типа.


Что касается вашего второго вопроса:

Списки могут быть возвращены из функций, даже если вы никогда не передавали в List, когда вы вызывали функцию

Функции возвращают разные типы данных, чем они все время вводятся. plot возвращает график, даже если он не принимает график в качестве входа. Arg возвращает a numeric, даже если он принял complex. Etc.

(И что касается strsplit: исходный код здесь.)

Ответ 9

Если это помогает, я склонен воспринимать "списки" в R как "записи" на других языках до OO:

  • они не делают никаких предположений о всеобъемлющем типе (или, скорее, о типе всех возможных записей любых имен arity и field).
  • их поля могут быть анонимными (тогда вы получаете доступ к ним по строгим определениям).

Название "запись" столкнулось бы со стандартным значением "записей" (aka rows) в языке базы данных, и, возможно, именно поэтому их имя предложило себя как списки (полей).

Ответ 10

Почему эти два разных оператора [ ] и [[ ]] возвращают тот же результат?

x = list(1, 2, 3, 4)
  • [ ] выполняет вспомогательную настройку. В общем подмножестве любого объекта будет иметь тот же тип, что и исходный объект. Поэтому x[1] предоставляет список. Аналогично x[1:2] является подмножеством исходного списка, поэтому это список. Пример.

    x[1:2]
    
    [[1]] [1] 1
    
    [[2]] [1] 2
    
  • [[ ]] предназначен для извлечения элемента из списка. x[[1]] и извлечь первый элемент из списка. x[[1:2]] недействителен как [[ ]] не предоставляет настройку sub, например [ ].

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

Ответ 11

Хотя это довольно старый вопрос, я должен сказать, что это касается именно тех знаний, которые я отсутствовал во время моих первых шагов в R - то есть, как выразить данные в моей руке как объект в R или как выбирать из существующих объектов. Нелегко для начинающего R начинать думать "в коробке R" с самого начала.

Итак, я сам начал использовать костыли, которые помогли мне много узнать, какой объект использовать для каких данных, и в основном представить себе реальное использование.

Хотя я не даю точных ответов на вопрос, короткий текст ниже может помочь читателю, который только что начал с R и задает близкие вопросы.

  • Атомный вектор... Я назвал эту "последовательность" для себя, без направления, просто последовательность одного и того же типа. [ подмножества.
  • Вектор... последовательность с одним направлением от 2D, [ подмножеств.
  • Матрица... связка векторов с одинаковой длиной, образующая строки или столбцы, подмножества [ по строкам и столбцам или по последовательности.
  • Массивы... слоистые матрицы, образующие 3D
  • Dataframe... 2D-таблица, как в excel, где я могу сортировать, добавлять или удалять строки или столбцы или создавать arit. операции с ними, только через некоторое время я действительно осознал, что dataframe - это умная реализация list, где я могу подмножество с помощью [ по строкам и столбцам, но даже используя [[.
  • Список... чтобы помочь себе, я думал о списке с tree structure, где [i] выбирает и возвращает целые ветки, а [[i]] возвращает элемент из ветки. И поскольку это tree like structure, вы можете даже использовать index sequence для адресации каждого отдельного листа на очень сложном list, используя его [[index_vector]]. Списки могут быть простыми или очень сложными и могут смешивать различные типы объектов в один.

Итак, для lists вы можете больше использовать способ выбора leaf в зависимости от ситуации, как в следующем примере.

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

Этот способ мышления очень помог мне.

Ответ 12

Это очень старый вопрос, но я думаю, что новый ответ может добавить некоторую ценность, так как, по моему мнению, никто не обратил внимание на некоторые проблемы в ОП.

Несмотря на то, что подтверждают принятые ответы, объекты list в R являются не хэш-картами. Если вы хотите провести параллель с python, list больше похож на, как вы думаете, python list (или tuple на самом деле).

Лучше описать, как большинство объектов R хранятся внутри (тип C объекта R - SEXP). Они сделаны в основном из трех частей:

  • заголовок, который объявляет тип R объекта, длину и некоторые другие метаданные;
  • часть данных, которая является стандартным массивом, выделенным из кучи C (непрерывный блок памяти);
  • атрибуты, которые являются именованным связанным списком указателей на другие объекты R (или NULL, если объект не имеет атрибутов).

С внутренней точки зрения, существует небольшая разница, например, между вектором list и numeric. Значения, которые они хранят, просто разные. Давайте разберем два объекта в описанную нами парадигму:

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

Для x:

  • В заголовке будет сказано, что типом является numeric (REALSXP на стороне C), длина равна 10 и другие вещи.
  • Часть данных будет массивом, содержащим 10 значений double.
  • Атрибутами являются NULL, поскольку у объекта их нет.

Для y:

  • Заголовок скажет, что типом является list (VECSXP на стороне C), длина равна 2 и другие вещи.
  • Часть данных будет массивом, содержащим 2 указателя на два типа SEXP, указывающие на значение, полученное runif(10) и runif(3) соответственно.
  • Атрибуты NULL, как и для x.

Таким образом, единственное различие между вектором numeric и list состоит в том, что часть данных numeric состоит из значений double, в то время как для части list это массив указателей на другие R-объекты.

Что происходит с именами? Ну, имена - это только некоторые из атрибутов, которые вы можете назначить объекту. Давайте посмотрим на объект ниже:

z <- list(a=1:3, b=LETTERS)
  • В заголовке будет сказано, что типом является list (VECSXP на стороне C), длина равна 2 и другие вещи.
  • Часть данных будет массивом, содержащим 2 указателя на два типа SEXP, указывающие на значение, полученное 1:3 и LETTERS соответственно.
  • Атрибуты теперь присутствуют и являются компонентом names, который является объектом character R со значением c("a","b").

На уровне R вы можете получить атрибуты объекта с помощью функции attributes.

Значение ключа, типичное для хэш-карты в R, является всего лишь иллюзией. Когда вы говорите:

z[["a"]]

вот что происходит:

  • вызывается функция подмножества [[;
  • аргумент функции ("a") имеет тип character, поэтому метод получает указание найти это значение из атрибута names (если имеется) объекта z;
  • если атрибут names отсутствует, возвращается NULL;
  • если присутствует, в нем ищется значение "a". Если "a" не является именем объекта, возвращается NULL;
  • если присутствует, позиция определяется (1 в примере). Таким образом, возвращается первый элемент списка, то есть эквивалент z[[1]].

Поиск по значению ключа довольно косвенный и всегда позиционный. Также полезно иметь в виду:

  • в хэш-картах единственное ограничение, которое должен иметь ключ - это то, что он должен быть хэшируемым. names в R должно быть строками (character векторов);
  • в хэш-картах вы не можете иметь два одинаковых ключа. В R вы можете назначить names объекту с повторяющимися значениями. Например:

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

    совершенно верно в R. При попытке y[["same"]] первое значение получается. Вы должны знать, почему на этом этапе.

В заключение, способность давать произвольные атрибуты объекту дает вам видимость чего-то отличного от внешней точки зрения. Но R list никак не являются хеш-картами.