Списки

Отличие вектора от списка?

  • Вектор содержит данные одного типа
  • Списки могут содежать данные различной длины и типов
    Например, используется логарифм, который возвращает матрицу и логарифм одновременно - в таком случае и используют список
  • Списки создает функция list()

Допустим, создаем список, содержащий вектор, строку “my_data”, матрицу

list(1:5, "my_data", matrix(0,2,2))
## [[1]]
## [1] 1 2 3 4 5
## 
## [[2]]
## [1] "my_data"
## 
## [[3]]
##      [,1] [,2]
## [1,]    0    0
## [2,]    0    0

Именование элементов списка

Необязательно давать имена всем элементам списка. Невалидные имена должны быть заключены в кавычки

list(a = 1, b = 1:3, "1to5" = 1:5, 42)
## $a
## [1] 1
## 
## $b
## [1] 1 2 3
## 
## $`1to5`
## [1] 1 2 3 4 5
## 
## [[4]]
## [1] 42

Список может быть рекурсивным

Т.е. список может содержать список, тот еще список и так далее рекурсивно.
Рекурсию можно использовать, чтобы создавать древовидные структуры. Строго говоря, рекурсивный список - тот, который содержит сам себя в качестве подсписка

list(a = list(1,2,3), b = list(list(4), 5, 6))
## $a
## $a[[1]]
## [1] 1
## 
## $a[[2]]
## [1] 2
## 
## $a[[3]]
## [1] 3
## 
## 
## $b
## $b[[1]]
## $b[[1]][[1]]
## [1] 4
## 
## 
## $b[[2]]
## [1] 5
## 
## $b[[3]]
## [1] 6

Объединение списков (конкатенация)

Допустим, есть 2 списка

l1 <- list(name = "john", salary = 1000)
l2 <- list(has_car = TRUE, car = "lamborghini")
l1
## $name
## [1] "john"
## 
## $salary
## [1] 1000
l2
## $has_car
## [1] TRUE
## 
## $car
## [1] "lamborghini"

Объединить списки можно с помощью той же функции combine, с помощью которой объединяли вектора

c(l1, l2)
## $name
## [1] "john"
## 
## $salary
## [1] 1000
## 
## $has_car
## [1] TRUE
## 
## $car
## [1] "lamborghini"

Конверсия между списком и вектором

Вектора и списки очень похожи друг на друга. Потому конверсия между ними заслуживает отдельного рассмотрения
* В какой-то степени вектор может рассматриваться, как подмножества всех возможных списков
Проверить это можно следующим образом. Заведем вектор от 1 до 7. С помощью функции list мы получим список, первым и последним элементом которого служит вектор

v <- 1:7
list(v)
## [[1]]
## [1] 1 2 3 4 5 6 7

Т.е. любой вектор легко свести к списку.

Но обратно конверсия не всегда работает. Но если мы уверены, что сведение имеет смысл, то есть функция unlist:

l <- list(1:3, 4:5, last = 6)
l
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] 4 5
## 
## $last
## [1] 6
unlist(l)
##                          last 
##    1    2    3    4    5    6

Из списка получили вектор, при том, что сохранилось именование последнего элемента вектора.

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

unlist(c(l, "spy"))
##                                last       
##   "1"   "2"   "3"   "4"   "5"   "6" "spy"

Доступ к эелементам списка

Три способа:
* как для векторов - через индексы, []
* доступ к конкретному элементу, [[]]
* доступ по имени с частичным дополнением, $

1й способ - через индексы

l
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] 4 5
## 
## $last
## [1] 6

Положительное индексирование

l[3:2]; 
## $last
## [1] 6
## 
## [[2]]
## [1] 4 5

Отрицательное индексирование

l[-(1:2)]
## $last
## [1] 6

Логическое индексирование

l[c(F,T,F)]
## [[1]]
## [1] 4 5

Именованное индексирование

Имя должно быть заключено в кавычки

l["last"]
## $last
## [1] 6

Важно! Если в результате обращения возрващается только 1 элемент - это все равно список

2й способ - Доступ к конкретному элементу, [[]]

Указываем единичный номер или полное имя

l[[1]]
## [1] 1 2 3
l[["last"]]
## [1] 6

При доступе по имени получаем не список длины = 1, а вектор из одного элемента

3й способ - Доступ по имени с частичным дополнением, $

Получаем доступ к элементу last (при этом кавычки не нужны). Получаем непосредственно сам элемент, а не подсписок

l$last
## [1] 6

Частичное дополнение - когда мы обращаемся по минимально уникальному идентификатору к элементу. В данном случае у нас имя одно, поэтому возможны варианты

l$l
## [1] 6
l$las
## [1] 6

Резюме - доступ к элементам списка

Замена и добавление элементов списка

Последний элемент опустили

l <- list(1:3, 4:5, last = 6)
l
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] 4 5
## 
## $last
## [1] 6
l[[3]] <- NULL; l
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] 4 5
l
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] 4 5

Важно (<-)! - список перезаписан с новым элементом.

Т.е. например, если нужно присвоить NULL элементу списка, то используется конструкция:

l[[1]] <- list(NULL)

Но при этом первый элемент станет списком из одного элемента. Поэтому можно так:

l[1] <- list(NULL)
l[1:3] <- list(NULL)
l$name <- list(NULL)
# Вместо NULL можно использовать NA

Необязательно создавать элементы подряд, правда, в этом случае пропущенные элементы будут создаваться автоматическими со значением NA

l[[4]] <- 99; l
## [[1]]
## NULL
## 
## [[2]]
## NULL
## 
## [[3]]
## NULL
## 
## $name
## [1] 99

Списки могут содержать функции

l <- list(vec = 1:7, fun = sqrt)
# Функция работает как обычно при обращении к ней
l$fun(4)
## [1] 2
names(l) # Смотрим имена списка
## [1] "vec" "fun"

Хотим создать новый элемент. Проверяем, существует ли он уже в списке

is.null(l$string)
## [1] TRUE

Можем создать элемент

l$string <- "Citius, altius, fortius"
l
## $vec
## [1] 1 2 3 4 5 6 7
## 
## $fun
## function (x)  .Primitive("sqrt")
## 
## $string
## [1] "Citius, altius, fortius"

Применение функций к списку - lapply

Чтобы применить функцию к строкам или столбцам матрицы используется функция - apply. На самом деле apply - целое семейство функций. Познакомимся еще с 2мя функциями из этого семейства. ## * lapply - применяет функция к каждому элементу списка

l <- list(a = c("12","34"), b = LETTERS[5:10], c = 1:5)
l
## $a
## [1] "12" "34"
## 
## $b
## [1] "E" "F" "G" "H" "I" "J"
## 
## $c
## [1] 1 2 3 4 5

Например, мы хотим узнать длину каждого элемента. Функции length недостаточно

length(l)
## [1] 3

Функция в таком виде дает количество элементов в списке

lapply(l, length)
## $a
## [1] 2
## 
## $b
## [1] 6
## 
## $c
## [1] 5

Теперь видим длину каждого подсписка #N.B! lapply(l, length) - устаревшая конструкция Для того, чтобы узнать длины элементов списка, используется функция lengths, которую добавили в R 3.2.0. Так что конструкция sapply(l, length) можно объявить устаревшей. lengths также на порядок более производительнее

Как передать функции конкретный элемент списка?

Например, хочу склеить все элементы в подсписках.

#Первый аргумент lapply - список
#Второй аргумент lapply - применяемая функция 
#Все последующие аргументы будут считываться, как аргументы функции
lapply(l, paste, collapse = "|")
## $a
## [1] "12|34"
## 
## $b
## [1] "E|F|G|H|I|J"
## 
## $c
## [1] "1|2|3|4|5"

Вариант анонимной функции

Не очень понятно, что это и зачем

lapply(l, function(s) paste(s, collapse = "|"))
## $a
## [1] "12|34"
## 
## $b
## [1] "E|F|G|H|I|J"
## 
## $c
## [1] "1|2|3|4|5"

Результат такой же - зачем, не понятно

sapply - результат функции lapply сводится к вектору (simplify)

sapply(l, paste, collapse = "|")
##             a             b             c 
##       "12|34" "E|F|G|H|I|J"   "1|2|3|4|5"

Т.е. если хотим дополнительно упростить результат функции lapply, то воспользуемся функцией sapply и получим вектор

Дополнение о частичном дополнении $ и аргументам функции

Уже встречались с частичным дополнением по знаку $ при обращении к элементам списка или подсписку по более короткому уникальному имени

l <- list(some_name = 1, incredibly_long_name = 2)
l$incr + 1
## [1] 3

Дополнение по аргументам функций работает таким же образом. Если есть функция с очень длинным аргументом, то необязательно прописывать название аргумента полностью - достаточно указать уникальный префикс

f <- function(x, ridiculously_long_argument) x + ridiculously_long_argument
f(3, ridic = 5)
## [1] 8

Глоссарий для дополнительного чтения

?matrix ?dim, ?rownames, ?colnames ?rbind, ?cbind, ?apply ?rowSums, ?rowMeans, ?colSums, ?colMeans ?list, ?unlist ?“[”, (?“[[”), ?“$” ?lapply, ?sapply Partial matching, ?“…” (ellipsis)

Небольшой комментарий: NA, NaN, NULL

Небольшая добавка: в R есть три похожих ключевых слова, NA, NaN и NULL. Они различаются по смысловой нагрузке.

Список как возвращаемое значение функции

Допустим, есть список, элементы которого вектора различной длины. Поэтому матрицу использовать не можем, приходится оперировать списком.

Нам нужно найти позицию элемента максимальной длины и вернуть сам элемент. Что для этоо нужно сделать?

get_longest <- function(l) {
  #Создаем вектор из длин
  len <- lengths(l)
  #Узнаем индекс вектора наибольшей длины
  ind <- which.max(len)
  #Результат функции и номер элемента, и сам элемент
  list(number = ind, element = l[ind])
}

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

Напишем функцию, которая возвращала бы список, у которого определенное количество элементов, но элементы могут быть разной длины и содержат случайные значения

#Создадим функцию с 3 аругументами:
# * количество элементов списка
# * максимальная длина
# * значение датчика генератора случайных чисел (для воспроизведения результатов лекции Степки)
# Длина каждого элемента определяется случайным образом 
gen_list <- function(n_elements, max_len, seed = 111) {
  # устанавливаем значение seed по умолчанию
  set.seed(seed)
  # Генерируем вектор функцией sample
  len <- sample(1:max_len, n_elements)
  # Составляем список с помощью lapply
  lapply(1:n_elements, function(i) rnorm(len[i]))
}

Кстати 3ю строку можно написать проще, например:
* lapply(len, function(i) rnorm(i))
или
* lapply(len, rnorm)

Вообще запись:
#### lapply(1:n_elements, function(i) rnorm(len[i])) Это стандартная запись анонимной функции. Синтаксис для определения функции такой (help(function)):
function( arglist ) expr
где expr - это одно выражение, если нужно объединить несколько выражений в одно, то это делается с помощью фигурных скобок: “{}”.

####N.B! Замечание к функции Если n_elements > max_len, то sample(1:max_len, n_elements) вернёт ошибку.
Чтобы этого не происходило, в функцию sample, третьим аргументом нужно вписать replace = T. Зачем это нужно? Функция sample генерирует случайные числа на заданном промежутке (1й аргумент) и в заданном количестве (2й аргумент). Ее работу можно себе представить как черный мешочек, из которого она каждый раз достает какое-то значение. Но! Однажды взяв конкретное значение, повторно она его уже не вытянет, потому что в черный мешочек обратно “не возвращает”. Чтобы sample мог не просто генерировать случайные числа, но и создавать одинаковые в ходе такого случайного набора и нужен ‘replace = T’.

Что же за set.seed, с которым уже не первый раз сталкиваемся?

Для воспроизводимости результатов. set.seed задает отправную точку (порождающий элемент) для генератора псевдослучайных чисел.
https://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_%D0%BF%D1%81%D0%B5%D0%B2%D0%B4%D0%BE%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D1%85_%D1%87%D0%B8%D1%81%D0%B5%D0%BB#%D0%93%D0%9F%D0%A1%D0%A7_%D1%81_%D0%B8%D1%81%D1%82%D0%BE%D1%87%D0%BD%D0%B8%D0%BA%D0%BE%D0%BC_%D1%8D%D0%BD%D1%82%D1%80%D0%BE%D0%BF%D0%B8%D0%B8_%D0%B8%D0%BB%D0%B8_%D0%93%D0%A1%D0%A7 Данная отправная точка используется внутри функции get_list сразу двумя функциями: sample() и rnorm(). Без аргумента seed результат этих функций (а соотвественно и результат функции get_list) будет неопределенным. Таким образом, Вы не сможете обеспечить один и тот же вход в функцию get_longest, полученный с помощью функции get_list, без предварительного сохранения результата

Сгенерируем 1й список (4 элемента, длина не больше 10)

l1 <- gen_list(4, 10)
l1
## [[1]]
## [1] -2.3023457 -0.1708760  0.1402782 -1.4974267
## 
## [[2]]
## [1] -1.0101884 -0.9484756 -0.4939622
## 
## [[3]]
## [1] -0.17367413 -0.40659878  1.84563626  0.39405411  0.79752850 -1.56666536
## [7] -0.08585101
## 
## [[4]]
## [1] -0.3591395

Теперь проверим, что функция get_longest работает корректно

get_longest(l1)
## $number
## [1] 3
## 
## $element
## $element[[1]]
## [1] -0.17367413 -0.40659878  1.84563626  0.39405411  0.79752850 -1.56666536
## [7] -0.08585101

Можно результат преобразовать в список, например

gl1 <- get_longest(l1)

Теперь можно обратиться к элементам списка по имени

gl1$number
## [1] 3

И так можно проверить функцию несколько раз с разными значениями заданных аргументов, в том числе разным значением счетчика генератора случайных числе. Например:

l2 <- gen_list(4, 10, 777); l2
## [[1]]
## [1]  0.5108363 -0.3988121  1.6386861  0.6212740  0.2027043  1.1089378 -0.2062248
## [8] -0.3789650
## 
## [[2]]
## [1] -0.3042617
## 
## [[3]]
## [1]  0.05416232 -1.88093062
## 
## [[4]]
## [1] -0.03375647  2.31149476  0.97234017  0.96460804

Проверяем функцию

get_longest(l2)
## $number
## [1] 1
## 
## $element
## $element[[1]]
## [1]  0.5108363 -0.3988121  1.6386861  0.6212740  0.2027043  1.1089378 -0.2062248
## [8] -0.3789650

Все работает корректно

Задача

Если вектор достаточно длинный, то визуально сложно оценить, какие в нём содержатся элементы и сколько раз они повторяются. В этом случае полезно будет посмотреть на таблицу частот элементов.

Пусть x – целочисленный вектор. Напишите функцию, которая вернёт матрицу из двух строк. В первой строке перечислите все различные элементы вектора, упорядоченные по возрастанию. Во второй строке укажите частоты (количество повторов) этих элементов.

x <- c(5, 2, 7, 7, 7, 2, 0, 0)
z <- unique(sort(x))
z
## [1] 0 2 5 7
y <- table(x)
y
## x
## 0 2 5 7 
## 2 2 1 3
as.vector(y)
## [1] 2 2 1 3
m <- matrix(rbind(z,y), nrow = 2)
m
##      [,1] [,2] [,3] [,4]
## [1,]    0    2    5    7
## [2,]    2    2    1    3

Теперь сделаю из этого функцию

count_elements <- function(x) {
z <- unique(sort(x))
y <- table(x)
as.vector(y)
m <- matrix(rbind(z,y), nrow = 2)
m
}

Проверю

x <- c(5, 2, 7, 7, 7, 2, 0, 0)
count_elements(x)
##      [,1] [,2] [,3] [,4]
## [1,]    0    2    5    7
## [2,]    2    2    1    3

Готово. Есть другие более изящные решения из форму Степик.

count_elements <- function(x) {
  t <- table(x)
  rbind(as.numeric(names(t)), t)
}

или

count_elements <- function(x) {
  l <- rle(sort(x))
  rbind(l$values,l$lengths)
}