3 Структуры данных

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

3.1 Векторы

Простейший способ организации данных — это вектор. Казалось бы, мы знаем, что вектор — это направленный отрезок. Безусловно, это так — в рамках Евклидовой геометрии, которую мы в давнем прошлом учили. Однако это не единственный способ смотреть на вещи. С точки зрения структур данных, вектор — это одномерный массив, а если по-русски, то набор элементов одного типа (например, чисел).

Эти два представления, на самом деле, не противоречат друг другу. Геометрически, как мы сказали, вектор — это направленный отрезок. Он задаётся через координаты начала и конца. Если мы условимся всегда начинать вектор из начала координат — то есть будет считать равными все векторы, которые имеют одинаковую длину и одинаковое направление1 — то мы сможем задавать вектор только через координаты его конца. В случае двумерного пространства вектор будет однозначно задаваться парой чисел \((x, y)\), в случае трёхмерного — тройкой чисел \((x,y,z)\), а в случае \(n\)-мерного пространства — набором чисел \((x_1, x_2, x_3, \dots, x_n)\).

Чтобы создать вектор в R надо воспользоваться функцией c(). Она принимает неограниченное количесво аргументов, которые объединяет в вектор. В вектор можно объединить элементы только одного типа.

v <- c(1,2,3,5,6,7)

Сохраним получившийся числовой вектор в переменную v. Присваивание векторов ничем не отличается от присваивания чисел, во-первых, потому что в R нет скаляров, и все числа — это векторы типа numeric длиной 1, а во-вторых, потому что и число, и вектор, и другие структуры данных (и даже функции!) — всё это объекты. А assignment — не что иное, как присваивание имени некоторому объекту, и нет разницы, что мы называем — число, матрицу, список, датафрейм или функцию.

3.1.1 Coercion [part two]

Взбунтуемся, и объеденим в один вектор разные типы данных:

v0 <- c(1, 2, TRUE, FALSE)
v0
## [1] 1 2 1 0

Бунт не удался — вектор всё равно был создан. Но что произошло?

С приведением типов мы уже сталкивались, когда пытались складывать логические константы. Аналогично R действовал и здесь:

  • есть задача создать вектор
  • но на выход функции поступили данные различных типов
  • придётся сделать так, чтобы тип был всё-таки один
  • numeric к logical однозначно привести сложно (что есть 2TRUE или FALSE?)
  • logical к numeric приводится очень хорошо и красиво (TRUE1, FALSE0)
  • после приведения типов можно выполнить команду создания вектора.

А всё-таки: что есть 2? TRUE или FALSE? Выясните, воспользовавшись функциями isTRUE() и isFALSE().

А будет ли работать (и как именно) ручное приведение numeric к logical? С помощью функции as.logical() приведите числа 0, 1, 2 и -1 к логическому типу.

Сделаем вектор из полного салата — добавим сторовые значения:

v0 <- c(1, 2, TRUE, FALSE, 'text', 'string')
v0
## [1] "1"      "2"      "TRUE"   "FALSE"  "text"   "string"

Наблюдаем, что все свелось к типу character, что вполне ожидаемо, так как 2 в "2" превращается однозначно, а вот в какое число (или логическую константу) превратить "string", не очень понятно.

Как отработает следующая конструкция?

v0 <- c(c(1, 2, TRUE), FALSE, 'text', 'string')

И почему именно так?

Собственно, можно вывести иерархию приведения типов:

logical < integer < numeric < complex < character

3.1.2 Генерация числовых последовательностей

Создавать руками вектора — это, конечно, радостно и приятно, но не очень юзабельно. На практике часто возникает потребность сгенерировать определенную числовую последовательность. Например, у вас есть опросниковые данные, из которых необходимо удалить персональные данные, но при этом сохранить возможность соотнести персональные данные и результаты анализа по каждому респонденту — вам нужно сгенерировать переменную ID. Вам поможет оператор :, который генерирует последовательность в заданных пределах с шагом 1:

1:10
##  [1]  1  2  3  4  5  6  7  8  9 10
15:0
##  [1] 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0

Если вам нужна последовательно с другим шагом, например, 0.5, то подойдет функция seq():

seq(from = 1, to = 10, by = 0.5) # задаём шаг последовательности
##  [1]  1.0  1.5  2.0  2.5  3.0  3.5  4.0  4.5  5.0  5.5  6.0  6.5  7.0  7.5  8.0
## [16]  8.5  9.0  9.5 10.0
seq(0, -6, -1.5) # или без указания названий аргументов
## [1]  0.0 -1.5 -3.0 -4.5 -6.0
seq(from = 5, to = 30, length.out = 20) # задаём длину последовательности
##  [1]  5.000000  6.315789  7.631579  8.947368 10.263158 11.578947 12.894737
##  [8] 14.210526 15.526316 16.842105 18.157895 19.473684 20.789474 22.105263
## [15] 23.421053 24.736842 26.052632 27.368421 28.684211 30.000000

Допустим, у вас есть данные (пусть выборка будет 15 человек), в которых каждые две строки относятся к одному респонденту, но к двум различным экспериментальным условиям (экспериментальному и контрольному). Тогда можно сделать такие переменные:

rep(1:15, each = 2) # для id
##  [1]  1  1  2  2  3  3  4  4  5  5  6  6  7  7  8  8  9  9 10 10 11 11 12 12 13
## [26] 13 14 14 15 15
rep(c('exp', 'control'), times = 15) # для обозначения условия
##  [1] "exp"     "control" "exp"     "control" "exp"     "control" "exp"    
##  [8] "control" "exp"     "control" "exp"     "control" "exp"     "control"
## [15] "exp"     "control" "exp"     "control" "exp"     "control" "exp"    
## [22] "control" "exp"     "control" "exp"     "control" "exp"     "control"
## [29] "exp"     "control"

Сгенерируйте последовательность от 106 до 124 с шагом 4, в которой каждый элемент будет повторяться подряд три раза.

##  [1] 106 106 106 110 110 110 114 114 114 118 118 118 122 122 122

Также можно сгенерировать случайную последовательность чисел (например, для того, чтобы использовать её при сабсете случайной подвыборки данных):

sample(x = 1:30, size = 15)
##  [1] 11 30 22  5 17 20  1 14 15 26 24 29 23  9 13

По умолчанию функция sample() генерирует случайную последовательность с учетом того, что выпадение каждого числа равновероятно. Как изменить это условие?

Сгенерируйте случайную последователность из 30 чисел от 1 до 10 при условии, что единица выпадает в два раза чаще, чем все остальные числа.

Чтобы результат получился такой же, как ниже, перед выполнением команды sample(...) выполните команду:

set.seed(69)
##  [1]  6  9  4 10  5 10  1  9  1  1 10  8  2  3  3  3  1  7  1  9  5  1  4  8  1
## [26]  3  4  7  1  8

3.1.3 Операции с векторами

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

Пусть у нас будет два вектора:

set.seed(42) # задаём положение для датчика случайных чисел
v1 <- sample(1:100, 20)
v2 <- sample(-50:100, 20)

Над векторами можно выполнять арифметические операции:

v1 + v2
##  [1]  56  56  -2  66 110 200 132   9  88 147  78 -27  74  66  -4 110 115  70 -14
## [20] 154
v1 - v2
##  [1]  42  74  52  82 -74   0 -38  39  54  31  -4  67 -22 -60  86 -56 -43 -60  82
## [20]  20
v1 * v2
##  [1]   343  -585  -675  -592  1656 10000  3995  -360  1207  5162  1517  -940
## [13]  1248   189 -1845  2241  2844   325 -1632  5829
v1 / v2
##  [1]  7.00000000 -7.22222222 -0.92592593 -9.25000000  0.19565217  1.00000000
##  [7]  0.55294118 -1.60000000  4.17647059  1.53448276  0.90243902 -0.42553191
## [13]  0.54166667  0.04761905 -0.91111111  0.32530120  0.45569620  0.07692308
## [19] -0.70833333  1.29850746

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

Кроме того, векторы можно поэлементно сравнивать:

v1 < v2
##  [1] FALSE FALSE FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE FALSE  TRUE FALSE
## [13]  TRUE  TRUE FALSE  TRUE  TRUE  TRUE FALSE FALSE
v1 == v2
##  [1] FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
v2 <= v1
##  [1]  TRUE  TRUE  TRUE  TRUE FALSE  TRUE FALSE  TRUE  TRUE  TRUE FALSE  TRUE
## [13] FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE  TRUE

Также к вектору можно применять и функции:

sin(v1)
##  [1] -0.9537527  0.8268287 -0.1323518 -0.9851463 -0.7509872 -0.5063656
##  [7]  0.1235731 -0.9055784  0.9510547  0.8600694 -0.6435381  0.9129453
## [13]  0.7625585  0.1411200 -0.1586227  0.9563759 -0.9917789 -0.9589243
## [19]  0.5290827 -0.8218178
log(v1)
##  [1] 3.891820 4.174387 3.218876 4.304065 2.890372 4.605170 3.850148 3.178054
##  [9] 4.262680 4.488636 3.610918 2.995732 3.258097 1.098612 3.713572 3.295837
## [17] 3.583519 1.609438 3.526361 4.465908
exp(v2)
##  [1] 1.096633e+03 1.234098e-04 1.879529e-12 3.354626e-04 9.017628e+39
##  [6] 2.688117e+43 8.223013e+36 3.059023e-07 2.415495e+07 1.545539e+25
## [11] 6.398435e+17 3.873998e-21 7.016736e+20 2.293783e+27 2.862519e-20
## [16] 1.112864e+36 2.038281e+34 1.694889e+28 1.425164e-21 1.252363e+29

Можно применять несколько функций подряд:

log(abs(v2))
##  [1] 1.945910 2.197225 3.295837 2.079442 4.521789 4.605170 4.442651 2.708050
##  [9] 2.833213 4.060443 3.713572 3.850148 3.871201 4.143135 3.806662 4.418841
## [17] 4.369448 4.174387 3.871201 4.204693

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

sum(v1)
## [1] 878

Или функция, которая вычисляет длину вектора (в смысле количества элементов в нём):

length(v2)
## [1] 20

3.1.4 Recycling

Доныне мы складывали векторы одинаковой длины. С ними всё ясно — они складываются поэлементно. А что будет, если мы сложим векторы разной длины?

v3 <- rep(1, times = 10); v3 # создаём векторы
##  [1] 1 1 1 1 1 1 1 1 1 1
v4 <- sample(1:100, 2); v4
## [1] 21  2
v5 <- sample(1:100, 3); v5
## [1] 58 10 40
length(v3) # проверяем длину
## [1] 10
length(v4)
## [1] 2
length(v5)
## [1] 3

Итак, сумма:

v3 + v4
##  [1] 22  3 22  3 22  3 22  3 22  3
v3 + v5
## Warning in v3 + v5: longer object length is not a multiple of shorter object
## length
##  [1] 59 11 41 59 11 41 59 11 41 59

Внимательно посмотрим на результат. В первом случае мы складывали вектор из десяти элементов и вектор из двух элементов. Чтобы выполнирь эту операцию R выполняет зацикливание (recycling) более короткого из двух, чтобы каждый элемент большего по длине вектора получил в соответствии элементн меньшего. Так как десять кратно двум, то по сути было выполнена следующая команда:

v3 + rep(v4, 
         times = length(v3) / length(v4))
##  [1] 22  3 22  3 22  3 22  3 22  3

Во втором случае длина меньшего вектора не кратна длине большего, поэтому recycling происходит до тех пор, пока не будут покрыты все элемент большего вектора. Вектор из трех элементов укладывается на вектор из десяти элементов три раза — поэтому мы видим в результате три раза последовательность 59, 11, 41 — и остается ещё один десятый элемент, который суммируется в первым элементом меньшего вектора — поэтому последний элемент в векторе результата 59.

3.1.5 Индексация векторов

В практике мы постоянно сталкиваемся в необходимость анализировать не все данные в векторе, а их часть. Поэтому встаёт вопрос о том, как эту часть извлечь?

Извлечение части данных из вектора называется индексацией. Это делается так:

v1[1:10] # первые десять элементов вектора
##  [1]  49  65  25  74  18 100  47  24  71  89
v1[c(1,3,5,7)] # 1-й, 3-й, 5-й и 7-й элементы вектора
## [1] 49 25 18 47
v1[sample(1:20, 5)] # случайная подвыборка пяти элементов
## [1] 18 49 36 47 74

Логика проста — чтобы взять часть вектора, нам нужен вектор индексов тех элементов, которые мы хотим вытащить. Его мы поместим в квадратные скобки — и будет нам счастье. Вектор индектов можно получить любыми способами:

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

Полезно также является индексация через отрицательные индексы:

v2
##  [1]   7  -9 -27  -8  92 100  85 -15  17  58  41 -47  48  63 -45  83  79  65 -48
## [20]  67
v2[-1] # все элементы, кроме первого
##  [1]  -9 -27  -8  92 100  85 -15  17  58  41 -47  48  63 -45  83  79  65 -48  67
v2[-(1:5)] # все элементы, кроме первых пяти
##  [1] 100  85 -15  17  58  41 -47  48  63 -45  83  79  65 -48  67

Запишите в вектор результат следующей команды:

read.csv('https://raw.githubusercontent.com/angelgardt/hseuxlab-wlm2021/master/book/wlm2021-book/data/indexing_vectors.csv')[['x']]

Задайте положение датчик случайный чисел:

set.seed(123)

Выберите из получившего вектора все значения, кроме тех, которые попадут в случайную подвыборку из 50 наблюдений. Вычислите сумму элементов получившегося вектора.

Подсказка. Функции sample() необходимы границы интервала, из которого она генерирует случайную подвыборку. Одна из границ интервала — длина вектора, с которым вы работаете.

Особого внимания заслуживает индексация логическими векторами. Например, мы хотим отобрать все элементы вектора, которые больше некоторого числа. Как это сделать?

Нам нужен вектор, которым мы будем индексировать исходный вектор. Как его получить? Известно, что при сравнении векторов между собой получается логический вектор. Но ведь число — это тоже вектор, просто единичной длины? Значит, если мы будем сравнивать вектор с числом, произойдёт recycling, в результате которого каждый элемент вектора будет сравнен с этим числом. То есть:

v1 > 40
##  [1]  TRUE  TRUE FALSE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE
## [13] FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE

Отлично! Вектор есть. Можно ли им проидексировать наш исходный вектор v1? Можно! Аналогично тому, как мы это уже делали.

v1[v1 > 40]
## [1]  49  65  74 100  47  71  89  41  87

Конструкция, возможно, выглядит немного странновато — но работает!

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

Мы ещё не изучали описательные статистики, но со средним арифметическим вроде все знакомы? Вычисляется оно с помощью функции mean().

##  [1] 25 18 24 37 20 26  3 41 27 36  5 34

3.1.6 NA, NaN, NULL

Взбунтуемся ещё раз и посмотрим, что получится, если мы будем вытаскивать из вектора элементы, используя индексы, которые выходят за границы длины вектора. Например, у нас есть вектор v2, длина которого

length(v2)
## [1] 20

Попробуем сделать так:

v2[15:25]
##  [1] -45  83  79  65 -48  67  NA  NA  NA  NA  NA

Мы получили нечто, с чем ранее не сталкивались. NA (от not available) обозначает значение, которое недоступно. Как правило, в реальных данных они появляются при каких-либо ошибках записи данных. Впрочем, не всегда. Можно придумать и такой дизайн исследования, когда пропуски также будут информативны и могут анализировться. В нашем случае мы обратились к элементам, которых нет в нашем векторе, поэтому R ничего более не смог сделать, как вернуть нам свидетельство того, что такие элементы он из вектора достать не смог.

К какому типу даных относится константа NA?

NA ведёт себя весьма специфично. Например, если мы попробуем посчитать сумму по получившемуся вектору, то результат будет следующим:

sum(v2[15:25])
## [1] NA

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

mean(v2[15:25])
## [1] NA

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

И всё же функция sum() не так проста, и умеет бороться с NA. Как нужно изменить команду, чтобы сумма была посчитана?

Попробуем вычислить логарифм по вектору v2:

log(v2)
## Warning in log(v2): NaNs produced
##  [1] 1.945910      NaN      NaN      NaN 4.521789 4.605170 4.442651      NaN
##  [9] 2.833213 4.060443 3.713572      NaN 3.871201 4.143135      NaN 4.418841
## [17] 4.369448 4.174387      NaN 4.204693

Опять всё не слава богу. Теперь у нас NaN. Это почти как NA, но не совсем. NaN обозначает не-число (not a number). То есть, это не пропущенное значение, оно существует, но R его не может вычислить. Если мы сравним два вектора,

v2; log(v2)
##  [1]   7  -9 -27  -8  92 100  85 -15  17  58  41 -47  48  63 -45  83  79  65 -48
## [20]  67
## Warning in log(v2): NaNs produced
##  [1] 1.945910      NaN      NaN      NaN 4.521789 4.605170 4.442651      NaN
##  [9] 2.833213 4.060443 3.713572      NaN 3.871201 4.143135      NaN 4.418841
## [17] 4.369448 4.174387      NaN 4.204693

то обнаружим, что NaN появляется там, где мы пытаемся вычислить логарим отрицательного числа. А, как мы помним, функция логарифма определена только на положительно полуоси \(x\). Вот и получается, что логарифм отрицательного аргумента — это какая-то неведомая сущность, то точно не-число.

В функциях NaN ведёт себя аналогично NA:

sum(log(v2))
## Warning in log(v2): NaNs produced
## [1] NaN

К какому типу даных относится константа NaN?

Мы поговорили о двух важный константах используемых в R. Есть ещё одна, и имя её NULL. Это имя обозначает «ничего», то есть, что объект пуст.

Например, возьмем вектор v, который мы создавали в самом начале, и положим в него NULL:

v <- NULL
v
## NULL

Теперь в этом векторе ничего не лежит.

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

3.2 Матрицы

Говоря о векторах, мы обозначили, что вектор — это одномерный массив. А раз есть одномерные массивы, значит бывают какие-то ещё? Да. Сгенерируем некоторый вектор:

v6 <- sample(1:100, 12); v6
##  [1] 53 27 96 38 89 34 93 69 72 76 63 13

и попробуем его «сложить» в «таблицу» так, чтобы в каждой строке было по три числа:

m1 <- matrix(v6, ncol = 3); m1
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13

Так как мы «складываем друг на друга» части одномерного массива, у нашего нового массива возникает новое измерение — если вектор был только одной строкой2, то теперь в нашем массиве есть и строки, и столбцы. Двумерный массив назвается матрицей.

class(m1)
## [1] "matrix" "array"

И поэтому для его создания мы использовали функцию matrix(). В качестве основных аргументов она хочет видеть вектор, который мы будет «упаковывать» в матрицу, а также количество строк или столбцов новой матрицы.

3.2.1 Индексация матриц

От того, что мы свернули вектор в матрицу, он не перестал быть вектором. [Шок!] То есть матрица по сути всё ещё тот же самый вектор, поэтому индексировать её можно точно так же, как и вектор:

v6[1]; m1[1]
## [1] 53
## [1] 53
v6[4]; m1[4]
## [1] 38
## [1] 38
v6[11]; m1[11]
## [1] 63
## [1] 63

Однако поскольку матрица — это всё же матрица, она отличается от вектора тем, что у неё есть дополнительный атрибут dim, который отображает её размерность:

dim(m1)
## [1] 4 3

В данном случае наблюдаем, что размерность матрицы \(4 × 3\), и это справедливо, ведь именно такую матрицу мы и создавали. А раз у нас имеется указание на количество строк и столбцов в матрице, то мы можем вытащить элемент(ы) как раз по его позиции:

m1[1, 2] # вытаскиваем элемент из первой строки и второго столбца
## [1] 89

Те же квадратные скобки, только указываем мы теперь две «координаты» — сначала строки, затем столбцы. Как не запутаться? Аналогия с координатами не случайна: строки — горизонтальны, первая координата на координата на координатной плоскости (\(x\)) тоже задаёт положение точки на горизонтальной оси; столбцы — вертикальны, вторая координата (\(y\)) задает положение точки на вертикальной оси.

Какой результат вернет команда dim(v6)? Почему именно такой?

Также мы можем вытащить не только отдельный элемент, но и какую-то часть матрицы. Всё работает аналогично векторам:

m1[1:3, 2:3]
##      [,1] [,2]
## [1,]   89   72
## [2,]   34   76
## [3,]   93   63

Если мы выползем за границы индексации, R начнёт ругаться:

m1[1:3, 2:4]
## Error in m1[1:3, 2:4]: subscript out of bounds

На практике нам часто надо вытащить отдельный столбец (или несколько столбцов), содержащих все строки. Как это можно сделать в R?

А если нам нужны несколько строк, содержащих все столбцы?

3.2.2 Операции в матрицами

Как вы знаете, с матрицами можно делать много всякого разного.

# наделаем матриц для развлечения
m2 <- matrix(sample(1:100, 12), nrow = 4); m2
##      [,1] [,2] [,3]
## [1,]   82   38   47
## [2,]   97   21   90
## [3,]   91   79   60
## [4,]   25   41   16
m3 <- matrix(sample(1:100, 12), ncol = 2); m3
##      [,1] [,2]
## [1,]   94   31
## [2,]    6   81
## [3,]   72   50
## [4,]   86   34
## [5,]   97    4
## [6,]   39   13
m4 <- matrix(sample(1:100, 9), nrow = 3); m4
##      [,1] [,2] [,3]
## [1,]   69   22   99
## [2,]   25   89   87
## [3,]   52   32   35
m5 <- matrix(sample(1:100, 9), nrow = 3); m5
##      [,1] [,2] [,3]
## [1,]   40   31   14
## [2,]   30   99   93
## [3,]   12   64   71

Матрицы можно складывать3 (поэлементно):

m1 + m2
##      [,1] [,2] [,3]
## [1,]  135  127  119
## [2,]  124   55  166
## [3,]  187  172  123
## [4,]   63  110   29

Но только те, которые имеют одинаковый размер:

m1 + m4
## Error in m1 + m4: non-conformable arrays

Матрицы можно умножать4 (поэлементно):

m1 * m2
##      [,1] [,2] [,3]
## [1,] 4346 3382 3384
## [2,] 2619  714 6840
## [3,] 8736 7347 3780
## [4,]  950 2829  208

Но только те, которые имеют одинаковый размер:

m1 * m4
## Error in m1 * m4: non-conformable arrays

Что будет, если мы попробуем умножить матрицу на число?

Напоминание: матрица — это вектор.

Что будет, если мы попробуем посчитать сумму по матрице?

Напоминание: матрица — это всё ещё вектор.

Но матрицы можно перемножать ещё и матрично. Тогда требуется соответствие вутренних размерностей матриц:

dim(m1); dim(m2)
## [1] 4 3
## [1] 4 3
m1 %*% m2
## Error in m1 %*% m2: non-conformable arguments
dim(m1); dim(m4)
## [1] 4 3
## [1] 3 3
m1 %*% m4
##       [,1]  [,2]  [,3]
## [1,]  9626 11391 15510
## [2,]  6665  6052  8291
## [3,] 12225 12405 19800
## [4,]  5023  7393 10220

Помним, что матричное умножение некуммутативно:

m4 %*% m5
##      [,1]  [,2]  [,3]
## [1,] 4608 10653 10041
## [2,] 4714 15154 14804
## [3,] 3460  7020  6189
m5 %*% m4
##      [,1]  [,2]  [,3]
## [1,] 4263  4087  7147
## [2,] 9381 12447 14838
## [3,] 6120  8232  9241

Можно вычислить детерминант матрицы:

det(m4)
## [1] -275855

И найти обратную матрицу:

solve(m4)
##              [,1]         [,2]        [,3]
## [1,] -0.001199906 -0.008692973  0.02500227
## [2,] -0.013227964  0.009907379  0.01278933
## [3,]  0.013876856  0.003857099 -0.02026789

А таке вычислить след матрицы:

sum(diag(m4)) # специальной функции не придумали
## [1] 193

3.3 Списки

До текущего момента мы говорили о структурах данных, которые требуют одинакового типа данных в себе. Давайте теперь вообразим вектор без ограничения на однотипность данных. Это будет список (list).

l <- list(69, "text", TRUE)
l
## [[1]]
## [1] 69
## 
## [[2]]
## [1] "text"
## 
## [[3]]
## [1] TRUE

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

l2 <- list(c("This", "list", "contains", "a", "matrix"), m1, l)
l2
## [[1]]
## [1] "This"     "list"     "contains" "a"        "matrix"  
## 
## [[2]]
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13
## 
## [[3]]
## [[3]][[1]]
## [1] 69
## 
## [[3]][[2]]
## [1] "text"
## 
## [[3]][[3]]
## [1] TRUE

Таким образом, список может являть собой крайне сложную структуру. Чтобы разобраться, как устроен конкретный список, можно воспользоваться функцией str(), которая отобразит структуру списка:

str(l2)
## List of 3
##  $ : chr [1:5] "This" "list" "contains" "a" ...
##  $ : int [1:4, 1:3] 53 27 96 38 89 34 93 69 72 76 ...
##  $ :List of 3
##   ..$ : num 69
##   ..$ : chr "text"
##   ..$ : logi TRUE

Как видно в аутпуте функции, список содержит три элемента: текстовый вектор длиной 5, массив целых чисел, размером 4×3, и список, который в свою очерель состоит также из трёх элементов — числа 69, строкового вектора, содержащего одно значение (“text”) и логического вектора длиной 1, который содержит значение «истина».

Можно назвать отдельные элементы списка собственными именами:

# создадим список такой же, как l2, только именованный
l3 <- list(description = c("This", "list", "contains", "a", "matrix"),
           matrix = m1,
           inner_list = l)
l3
## $description
## [1] "This"     "list"     "contains" "a"        "matrix"  
## 
## $matrix
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13
## 
## $inner_list
## $inner_list[[1]]
## [1] 69
## 
## $inner_list[[2]]
## [1] "text"
## 
## $inner_list[[3]]
## [1] TRUE

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

Создайте вектор, который будет содержать пять элементов: ваше имя (name), вашу фамилию (surname), вашу дату рождения (birthdate), название вашей любимой книги (book) и ваш любимый цвет (color).

##                   name                surname              birthdate 
##                "Anton"           "Angelgardt"           "21.04.1997" 
##                   book                  color 
## "A. Camus. L'étranger"                "black"

3.3.1 Индексация списков

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

Поскольку список как и вектор состоит из отдельных элементов, которые в нём расположены в определённом порядке, то можно поступить со списком как с вектором:

l3[1]
## $description
## [1] "This"     "list"     "contains" "a"        "matrix"

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

l3[1:2]
## $description
## [1] "This"     "list"     "contains" "a"        "matrix"  
## 
## $matrix
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13

Чтобы вытащить сам вектор, нам потребуются двойные квадратные скобки:

l3[[1]]
## [1] "This"     "list"     "contains" "a"        "matrix"

Можно пойти далее и вытащить какой-то элемент из вектора прямо в этой же строке:

l3[[1]][c(1, 5)] # вытащим сразу первый и пятый
## [1] "This"   "matrix"

Раз у нас именованный список, то можно вытащить элемент по имени (с векторами тоже работает):

l3['matrix'] # так вернётся список
## $matrix
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13
l3[['matrix']] # а так сама матрица
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13

Из созданного в предыдущем задании вектора вытащите по имени элементы name, surname, birthdate.

##         name      surname    birthdate 
##      "Anton" "Angelgardt" "21.04.1997"

Но списки нам предоставляют ещё одну удобную и полезную фичу — индексацию по имени, но другим способом:

l3$description
## [1] "This"     "list"     "contains" "a"        "matrix"
l3$matrix
##      [,1] [,2] [,3]
## [1,]   53   89   72
## [2,]   27   34   76
## [3,]   96   93   63
## [4,]   38   69   13

Обратите внимание, что в таком случае сразу возвращается «голый» объект — вектор, матрица, etc. Запомните этот способ — так мы будем делать о-о-очень часто (примерно всегда).

Скачайте по ссылке файл list.RData. Поместите его в текущую рабочую директорию. Затем выполните команду ниже:

load('list.RData')

В окне Environment появиться объект типа «список», который содержит библиографическую информацию о нескольких книгах. Изучите этот объект и ответьте на вопросы ниже с применением навыков индексации списков.

  1. Сколько слов содержится в названии первой книги?
  1. Кто автор самой старой из представленных книг?
  1. У какой (каких) книг отсутствует Softcover ISBN? В ответ введите одно число или несколько чисел через пробел.

3.4 Датафреймы

Ура! Мы добрались до самого интересноо в самого важного!

Кратко вспомним, что мы умеем к этому моменту:

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

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

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

# немного изменим набор переменных для демонстрации возможностей
df <- data.frame(name = c('Иван', 'Алёна', 'Сергей', 'Елена', 'Кристина'),
                 age = c(23, 34, 52, 19, 26),
                 sex = c('муж', 'жен', 'муж', 'жен', 'жен'),
                 city = c("Mосква", "Тверь", "Тверь", "Питер", "Москва"))
df
##       name age sex   city
## 1     Иван  23 муж Mосква
## 2    Алёна  34 жен  Тверь
## 3   Сергей  52 муж  Тверь
## 4    Елена  19 жен  Питер
## 5 Кристина  26 жен Москва

Обратите внимание, датафрейм создается практически так же, как и список, только функция другая. А в итоге получается привычная нам таблица! Magique!

Как устроена эта таблица? По сути датафрейм — это именованный список, каждый элемент которого — это вектор определённой длины (одинаковой для всех векторов, входящих в этот список). Так они одинаковой длины, то их можно «поставить рядом друг с другом» как колонки матрицы. Собственно, так и получается. Вот только в данному случае разные столбцы могут содержать разный тип данных.

И тем не менее, поскольку датафрейм наследует свойства обоих своих «родителей», обращаться с ним можно и как со списком, и как с матрицей:

str(df) # изучаем структуру
## 'data.frame':    5 obs. of  4 variables:
##  $ name: chr  "Иван" "Алёна" "Сергей" "Елена" ...
##  $ age : num  23 34 52 19 26
##  $ sex : chr  "муж" "жен" "муж" "жен" ...
##  $ city: chr  "Mосква" "Тверь" "Тверь" "Питер" ...
df$name # вытаскиваем элемент списка (вектор имён респондентов)
## [1] "Иван"     "Алёна"    "Сергей"   "Елена"    "Кристина"
df$name[1:3] # индексируем вектор имён респондентов
## [1] "Иван"   "Алёна"  "Сергей"
df[, 1] # так тоже срабоает
## [1] "Иван"     "Алёна"    "Сергей"   "Елена"    "Кристина"
df[1] # и даже так, но какая структура данных вернулась?
##       name
## 1     Иван
## 2    Алёна
## 3   Сергей
## 4    Елена
## 5 Кристина
df[1:2, 3:4] # ну это просто пушка
##   sex   city
## 1 муж Mосква
## 2 жен  Тверь

Можно добавить новые переменные:

df$married <- FALSE # recycling has happened

Как вы поняли, возможностей индексации датафрейма — много. Но мы обозначили не все!

Вытащите из имеющегося датафрейма df строки переменной city со второй по пятую, не используя команды, обозначенные выше.

Подсказка: как можно ещё индексировать список?

## [1] "Тверь"  "Тверь"  "Питер"  "Москва"

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

##       name age sex   city married
## 1     Иван  23 муж Mосква   FALSE
## 2    Алёна  34 жен  Тверь    TRUE
## 3   Сергей  52 муж  Тверь    TRUE
## 4    Елена  19 жен  Питер   FALSE
## 5 Кристина  26 жен Москва    TRUE

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

df$sex[df$age > mean(df$age)]
## [1] "жен" "муж"

Или вот так, например:

df[df$age > mean(df$age), 'sex']
## [1] "жен" "муж"

Мы вытащили по логическому условию значения определенной переменной датафрейма. А как вытащить часть датафрейма по некоторому условию? То есть необходимо оставить все колонки, но строки отобрать по определённому условию.

Создайте подвыборку наших респондентов, возраст которых меньше среднего по всей имеющейся выборке.

##       name age sex   city married
## 1     Иван  23 муж Mосква   FALSE
## 4    Елена  19 жен  Питер   FALSE
## 5 Кристина  26 жен Москва    TRUE

Ну, пожалуй, и хватит топтаться в основах — пора уже чем-то серьёзным заняться!


  1. Такие векторы называются свободными.↩︎

  2. Или столбцом, что в данном случае не важно.↩︎

  3. А если можно складывать, соответственно, можно и вычитать.↩︎

  4. А если можно умножать, соответственно, можно и делить.↩︎