3 Структуры данных
С тем, какие существуют данные, мы разобрались. Теперь надо понять, как мы их можем организовать.
3.1 Векторы
Простейший способ организации данных — это вектор. Казалось бы, мы знаем, что вектор — это направленный отрезок. Безусловно, это так — в рамках Евклидовой геометрии, которую мы в давнем прошлом учили. Однако это не единственный способ смотреть на вещи. С точки зрения структур данных, вектор — это одномерный массив, а если по-русски, то набор элементов одного типа (например, чисел).
Эти два представления, на самом деле, не противоречат друг другу. Геометрически, как мы сказали, вектор — это направленный отрезок. Он задаётся через координаты начала и конца. Если мы условимся всегда начинать вектор из начала координат — то есть будет считать равными все векторы, которые имеют одинаковую длину и одинаковое направление1 — то мы сможем задавать вектор только через координаты его конца. В случае двумерного пространства вектор будет однозначно задаваться парой чисел \((x, y)\), в случае трёхмерного — тройкой чисел \((x,y,z)\), а в случае \(n\)-мерного пространства — набором чисел \((x_1, x_2, x_3, \dots, x_n)\).
Чтобы создать вектор в R надо воспользоваться функцией c()
. Она принимает неограниченное количесво аргументов, которые объединяет в вектор. В вектор можно объединить элементы только одного типа.
Сохраним получившийся числовой вектор в переменную v
. Присваивание векторов ничем не отличается от присваивания чисел, во-первых, потому что в R нет скаляров, и все числа — это векторы типа numeric
длиной 1, а во-вторых, потому что и число, и вектор, и другие структуры данных (и даже функции!) — всё это объекты. А assignment — не что иное, как присваивание имени некоторому объекту, и нет разницы, что мы называем — число, матрицу, список, датафрейм или функцию.
3.1.1 Coercion [part two]
Взбунтуемся, и объеденим в один вектор разные типы данных:
## [1] 1 2 1 0
Бунт не удался — вектор всё равно был создан. Но что произошло?
![](img/mem-coercion.jpg)
С приведением типов мы уже сталкивались, когда пытались складывать логические константы. Аналогично R действовал и здесь:
- есть задача создать вектор
- но на выход функции поступили данные различных типов
- придётся сделать так, чтобы тип был всё-таки один
numeric
кlogical
однозначно привести сложно (что есть2
—TRUE
илиFALSE
?)logical
кnumeric
приводится очень хорошо и красиво (TRUE
—1
,FALSE
—0
)- после приведения типов можно выполнить команду создания вектора.
![](img/code.png)
А всё-таки: что есть 2
? TRUE
или FALSE
? Выясните, воспользовавшись функциями isTRUE()
и isFALSE()
.
![](img/code.png)
А будет ли работать (и как именно) ручное приведение numeric
к logical
? С помощью функции as.logical()
приведите числа 0
, 1
, 2
и -1
к логическому типу.
Сделаем вектор из полного салата — добавим сторовые значения:
## [1] "1" "2" "TRUE" "FALSE" "text" "string"
Наблюдаем, что все свелось к типу character
, что вполне ожидаемо, так как 2
в "2"
превращается однозначно, а вот в какое число (или логическую константу) превратить "string"
, не очень понятно.
![](img/code.png)
Как отработает следующая конструкция?
И почему именно так?
Собственно, можно вывести иерархию приведения типов:
3.1.2 Генерация числовых последовательностей
Создавать руками вектора — это, конечно, радостно и приятно, но не очень юзабельно. На практике часто возникает потребность сгенерировать определенную числовую последовательность. Например, у вас есть опросниковые данные, из которых необходимо удалить персональные данные, но при этом сохранить возможность соотнести персональные данные и результаты анализа по каждому респонденту — вам нужно сгенерировать переменную ID. Вам поможет оператор :
, который генерирует последовательность в заданных пределах с шагом 1:
## [1] 1 2 3 4 5 6 7 8 9 10
## [1] 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Если вам нужна последовательно с другим шагом, например, 0.5, то подойдет функция seq()
:
## [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
## [1] 0.0 -1.5 -3.0 -4.5 -6.0
## [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 человек), в которых каждые две строки относятся к одному респонденту, но к двум различным экспериментальным условиям (экспериментальному и контрольному). Тогда можно сделать такие переменные:
## [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
## [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"
![](img/code.png)
Сгенерируйте последовательность от 106 до 124 с шагом 4, в которой каждый элемент будет повторяться подряд три раза.
## [1] 106 106 106 110 110 110 114 114 114 118 118 118 122 122 122
Также можно сгенерировать случайную последовательность чисел (например, для того, чтобы использовать её при сабсете случайной подвыборки данных):
## [1] 11 30 22 5 17 20 1 14 15 26 24 29 23 9 13
![](img/question.png)
По умолчанию функция sample()
генерирует случайную последовательность с учетом того, что выпадение каждого числа равновероятно. Как изменить это условие?
![](img/code.png)
Сгенерируйте случайную последователность из 30 чисел от 1 до 10 при условии, что единица выпадает в два раза чаще, чем все остальные числа.
Чтобы результат получился такой же, как ниже, перед выполнением команды sample(...)
выполните команду:
## [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)
Над векторами можно выполнять арифметические операции:
## [1] 56 56 -2 66 110 200 132 9 88 147 78 -27 74 66 -4 110 115 70 -14
## [20] 154
## [1] 42 74 52 82 -74 0 -38 39 54 31 -4 67 -22 -60 86 -56 -43 -60 82
## [20] 20
## [1] 343 -585 -675 -592 1656 10000 3995 -360 1207 5162 1517 -940
## [13] 1248 189 -1845 2241 2844 325 -1632 5829
## [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
Они выполняются поэлементно, то есть соответсвующие элементы двух векторов складываются (вычитаются, умножаются, делятся), и в результате получается новый вектор.
Кроме того, векторы можно поэлементно сравнивать:
## [1] FALSE FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE TRUE FALSE
## [13] TRUE TRUE FALSE TRUE TRUE TRUE FALSE FALSE
## [1] FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE FALSE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [1] TRUE TRUE TRUE TRUE FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE
## [13] FALSE FALSE TRUE FALSE FALSE FALSE TRUE TRUE
Также к вектору можно применять и функции:
## [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
## [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
## [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
Можно применять несколько функций подряд:
## [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
Большинство арифметических функций выполняется поэлементно, однако существуют такие, которые поэлементно не могут быть выполнены, например сумма по вектору:
## [1] 878
Или функция, которая вычисляет длину вектора (в смысле количества элементов в нём):
## [1] 20
3.1.4 Recycling
Доныне мы складывали векторы одинаковой длины. С ними всё ясно — они складываются поэлементно. А что будет, если мы сложим векторы разной длины?
## [1] 1 1 1 1 1 1 1 1 1 1
## [1] 21 2
## [1] 58 10 40
## [1] 10
## [1] 2
## [1] 3
Итак, сумма:
## [1] 22 3 22 3 22 3 22 3 22 3
## 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) более короткого из двух, чтобы каждый элемент большего по длине вектора получил в соответствии элементн меньшего. Так как десять кратно двум, то по сути было выполнена следующая команда:
## [1] 22 3 22 3 22 3 22 3 22 3
Во втором случае длина меньшего вектора не кратна длине большего, поэтому recycling происходит до тех пор, пока не будут покрыты все элемент большего вектора. Вектор из трех элементов укладывается на вектор из десяти элементов три раза — поэтому мы видим в результате три раза последовательность 59, 11, 41
— и остается ещё один десятый элемент, который суммируется в первым элементом меньшего вектора — поэтому последний элемент в векторе результата 59
.
3.1.5 Индексация векторов
В практике мы постоянно сталкиваемся в необходимость анализировать не все данные в векторе, а их часть. Поэтому встаёт вопрос о том, как эту часть извлечь?
Извлечение части данных из вектора называется индексацией. Это делается так:
## [1] 49 65 25 74 18 100 47 24 71 89
## [1] 49 25 18 47
## [1] 18 49 36 47 74
Логика проста — чтобы взять часть вектора, нам нужен вектор индексов тех элементов, которые мы хотим вытащить. Его мы поместим в квадратные скобки — и будет нам счастье. Вектор индектов можно получить любыми способами:
- сгенерировать последовательноть (как в первом варианте),
- задать индексы вручную, не забыв при этом обернуть их в фнукцию
c()
, чтобы указать, что это вектор, (как во втором варианте), - воспользоваться функцией, которая возвращает вектор.
Полезно также является индексация через отрицательные индексы:
## [1] 7 -9 -27 -8 92 100 85 -15 17 58 41 -47 48 63 -45 83 79 65 -48
## [20] 67
## [1] -9 -27 -8 92 100 85 -15 17 58 41 -47 48 63 -45 83 79 65 -48 67
## [1] 100 85 -15 17 58 41 -47 48 63 -45 83 79 65 -48 67
![](img/code.png)
Запишите в вектор результат следующей команды:
read.csv('https://raw.githubusercontent.com/angelgardt/hseuxlab-wlm2021/master/book/wlm2021-book/data/indexing_vectors.csv')[['x']]
Задайте положение датчик случайный чисел:
Выберите из получившего вектора все значения, кроме тех, которые попадут в случайную подвыборку из 50 наблюдений. Вычислите сумму элементов получившегося вектора.
Подсказка. Функции sample()
необходимы границы интервала, из которого она генерирует случайную подвыборку. Одна из границ интервала — длина вектора, с которым вы работаете.
Особого внимания заслуживает индексация логическими векторами. Например, мы хотим отобрать все элементы вектора, которые больше некоторого числа. Как это сделать?
Нам нужен вектор, которым мы будем индексировать исходный вектор. Как его получить? Известно, что при сравнении векторов между собой получается логический вектор. Но ведь число — это тоже вектор, просто единичной длины? Значит, если мы будем сравнивать вектор с числом, произойдёт recycling, в результате которого каждый элемент вектора будет сравнен с этим числом. То есть:
## [1] TRUE TRUE FALSE TRUE FALSE TRUE TRUE FALSE TRUE TRUE FALSE FALSE
## [13] FALSE FALSE TRUE FALSE FALSE FALSE FALSE TRUE
Отлично! Вектор есть. Можно ли им проидексировать наш исходный вектор v1
? Можно! Аналогично тому, как мы это уже делали.
## [1] 49 65 74 100 47 71 89 41 87
Конструкция, возможно, выглядит немного странновато — но работает!
![](img/code.png)
Возмите вектор v1
, с которым мы только что работали и с опорой на имеющийся в предыдущем чанке код, вытащите из вектора элементы, который не больше среднего значения по вектору.
Мы ещё не изучали описательные статистики, но со средним арифметическим вроде все знакомы? Вычисляется оно с помощью функции mean()
.
## [1] 25 18 24 37 20 26 3 41 27 36 5 34
3.1.6 NA, NaN, NULL
Взбунтуемся ещё раз и посмотрим, что получится, если мы будем вытаскивать из вектора элементы, используя индексы, которые выходят за границы длины вектора. Например, у нас есть вектор v2
, длина которого
## [1] 20
Попробуем сделать так:
## [1] -45 83 79 65 -48 67 NA NA NA NA NA
Мы получили нечто, с чем ранее не сталкивались. NA
(от not available) обозначает значение, которое недоступно. Как правило, в реальных данных они появляются при каких-либо ошибках записи данных. Впрочем, не всегда. Можно придумать и такой дизайн исследования, когда пропуски также будут информативны и могут анализировться. В нашем случае мы обратились к элементам, которых нет в нашем векторе, поэтому R ничего более не смог сделать, как вернуть нам свидетельство того, что такие элементы он из вектора достать не смог.
![](img/question.png)
К какому типу даных относится константа NA
?
NA
ведёт себя весьма специфично. Например, если мы попробуем посчитать сумму по получившемуся вектору, то результат будет следующим:
## [1] NA
Аналогичная ситуация возникнет, если мы будем вычислять среднее:
## [1] NA
Такое поведение функций может поначалу напрягать, однако оказывается очень полезным при работе с реальными данными.
![](img/question.png)
И всё же функция sum()
не так проста, и умеет бороться с NA
. Как нужно изменить команду, чтобы сумма была посчитана?
Попробуем вычислить логарифм по вектору 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 его не может вычислить. Если мы сравним два вектора,
## [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
:
## Warning in log(v2): NaNs produced
## [1] NaN
![](img/question.png)
К какому типу даных относится константа NaN
?
Мы поговорили о двух важный константах используемых в R. Есть ещё одна, и имя её NULL
. Это имя обозначает «ничего», то есть, что объект пуст.
Например, возьмем вектор v
, который мы создавали в самом начале, и положим в него NULL
:
## NULL
Теперь в этом векторе ничего не лежит.
NULL
может использоваться при задании аргументов функций или как результат работы функций, если возвращается пустой объект.
3.2 Матрицы
Говоря о векторах, мы обозначили, что вектор — это одномерный массив. А раз есть одномерные массивы, значит бывают какие-то ещё? Да. Сгенерируем некоторый вектор:
## [1] 53 27 96 38 89 34 93 69 72 76 63 13
и попробуем его «сложить» в «таблицу» так, чтобы в каждой строке было по три числа:
## [,1] [,2] [,3]
## [1,] 53 89 72
## [2,] 27 34 76
## [3,] 96 93 63
## [4,] 38 69 13
Так как мы «складываем друг на друга» части одномерного массива, у нашего нового массива возникает новое измерение — если вектор был только одной строкой2, то теперь в нашем массиве есть и строки, и столбцы. Двумерный массив назвается матрицей.
## [1] "matrix" "array"
И поэтому для его создания мы использовали функцию matrix()
. В качестве основных аргументов она хочет видеть вектор, который мы будет «упаковывать» в матрицу, а также количество строк или столбцов новой матрицы.
3.2.1 Индексация матриц
От того, что мы свернули вектор в матрицу, он не перестал быть вектором. [Шок!] То есть матрица по сути всё ещё тот же самый вектор, поэтому индексировать её можно точно так же, как и вектор:
## [1] 53
## [1] 53
## [1] 38
## [1] 38
## [1] 63
## [1] 63
Однако поскольку матрица — это всё же матрица, она отличается от вектора тем, что у неё есть дополнительный атрибут dim
, который отображает её размерность:
## [1] 4 3
В данном случае наблюдаем, что размерность матрицы \(4 × 3\), и это справедливо, ведь именно такую матрицу мы и создавали. А раз у нас имеется указание на количество строк и столбцов в матрице, то мы можем вытащить элемент(ы) как раз по его позиции:
## [1] 89
Те же квадратные скобки, только указываем мы теперь две «координаты» — сначала строки, затем столбцы. Как не запутаться? Аналогия с координатами не случайна: строки — горизонтальны, первая координата на координата на координатной плоскости (\(x\)) тоже задаёт положение точки на горизонтальной оси; столбцы — вертикальны, вторая координата (\(y\)) задает положение точки на вертикальной оси.
![](img/question.png)
Какой результат вернет команда dim(v6)
? Почему именно такой?
Также мы можем вытащить не только отдельный элемент, но и какую-то часть матрицы. Всё работает аналогично векторам:
## [,1] [,2]
## [1,] 89 72
## [2,] 34 76
## [3,] 93 63
Если мы выползем за границы индексации, R начнёт ругаться:
## Error in m1[1:3, 2:4]: subscript out of bounds
![](img/question.png)
На практике нам часто надо вытащить отдельный столбец (или несколько столбцов), содержащих все строки. Как это можно сделать в R?
А если нам нужны несколько строк, содержащих все столбцы?
3.2.2 Операции в матрицами
Как вы знаете, с матрицами можно делать много всякого разного.
## [,1] [,2] [,3]
## [1,] 82 38 47
## [2,] 97 21 90
## [3,] 91 79 60
## [4,] 25 41 16
## [,1] [,2]
## [1,] 94 31
## [2,] 6 81
## [3,] 72 50
## [4,] 86 34
## [5,] 97 4
## [6,] 39 13
## [,1] [,2] [,3]
## [1,] 69 22 99
## [2,] 25 89 87
## [3,] 52 32 35
## [,1] [,2] [,3]
## [1,] 40 31 14
## [2,] 30 99 93
## [3,] 12 64 71
Матрицы можно складывать3 (поэлементно):
## [,1] [,2] [,3]
## [1,] 135 127 119
## [2,] 124 55 166
## [3,] 187 172 123
## [4,] 63 110 29
Но только те, которые имеют одинаковый размер:
## Error in m1 + m4: non-conformable arrays
Матрицы можно умножать4 (поэлементно):
## [,1] [,2] [,3]
## [1,] 4346 3382 3384
## [2,] 2619 714 6840
## [3,] 8736 7347 3780
## [4,] 950 2829 208
Но только те, которые имеют одинаковый размер:
## Error in m1 * m4: non-conformable arrays
![](img/question.png)
Что будет, если мы попробуем умножить матрицу на число?
Напоминание: матрица — это вектор.
![](img/question.png)
Что будет, если мы попробуем посчитать сумму по матрице?
Напоминание: матрица — это всё ещё вектор.
Но матрицы можно перемножать ещё и матрично. Тогда требуется соответствие вутренних размерностей матриц:
## [1] 4 3
## [1] 4 3
## Error in m1 %*% m2: non-conformable arguments
## [1] 4 3
## [1] 3 3
## [,1] [,2] [,3]
## [1,] 9626 11391 15510
## [2,] 6665 6052 8291
## [3,] 12225 12405 19800
## [4,] 5023 7393 10220
Помним, что матричное умножение некуммутативно:
## [,1] [,2] [,3]
## [1,] 4608 10653 10041
## [2,] 4714 15154 14804
## [3,] 3460 7020 6189
## [,1] [,2] [,3]
## [1,] 4263 4087 7147
## [2,] 9381 12447 14838
## [3,] 6120 8232 9241
Можно вычислить детерминант матрицы:
## [1] -275855
И найти обратную матрицу:
## [,1] [,2] [,3]
## [1,] -0.001199906 -0.008692973 0.02500227
## [2,] -0.013227964 0.009907379 0.01278933
## [3,] 0.013876856 0.003857099 -0.02026789
А таке вычислить след матрицы:
## [1] 193
3.3 Списки
До текущего момента мы говорили о структурах данных, которые требуют одинакового типа данных в себе. Давайте теперь вообразим вектор без ограничения на однотипность данных. Это будет список (list).
## [[1]]
## [1] 69
##
## [[2]]
## [1] "text"
##
## [[3]]
## [1] TRUE
Но список даёт нам ещё больше возможностей, потому что он может собирать в себя вообще любые объекты:
## [[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()
, которая отобразит структуру списка:
## 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
![](img/code.png)
В 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 появляются достаточно часто — и, главным образом, как результат работы функций. Собственными руками мы их создавать вряд ли когда либо будем, а вот вытаскивать из них инфу по частям нам научиться надо обязательно.
Поскольку список как и вектор состоит из отдельных элементов, которые в нём расположены в определённом порядке, то можно поступить со списком как с вектором:
## $description
## [1] "This" "list" "contains" "a" "matrix"
Мы помним, что с списке 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
Чтобы вытащить сам вектор, нам потребуются двойные квадратные скобки:
## [1] "This" "list" "contains" "a" "matrix"
Можно пойти далее и вытащить какой-то элемент из вектора прямо в этой же строке:
## [1] "This" "matrix"
Раз у нас именованный список, то можно вытащить элемент по имени (с векторами тоже работает):
## $matrix
## [,1] [,2] [,3]
## [1,] 53 89 72
## [2,] 27 34 76
## [3,] 96 93 63
## [4,] 38 69 13
## [,1] [,2] [,3]
## [1,] 53 89 72
## [2,] 27 34 76
## [3,] 96 93 63
## [4,] 38 69 13
![](img/code.png)
Из созданного в предыдущем задании вектора вытащите по имени элементы name
, surname
, birthdate
.
## name surname birthdate
## "Anton" "Angelgardt" "21.04.1997"
Но списки нам предоставляют ещё одну удобную и полезную фичу — индексацию по имени, но другим способом:
## [1] "This" "list" "contains" "a" "matrix"
## [,1] [,2] [,3]
## [1,] 53 89 72
## [2,] 27 34 76
## [3,] 96 93 63
## [4,] 38 69 13
Обратите внимание, что в таком случае сразу возвращается «голый» объект — вектор, матрица, etc. Запомните этот способ — так мы будем делать о-о-очень часто (примерно всегда).
![](img/code.png)
Скачайте по ссылке файл list.RData
. Поместите его в текущую рабочую директорию. Затем выполните команду ниже:
В окне Environment появиться объект типа «список», который содержит библиографическую информацию о нескольких книгах. Изучите этот объект и ответьте на вопросы ниже с применением навыков индексации списков.
- Сколько слов содержится в названии первой книги?
- Кто автор самой старой из представленных книг?
- У какой (каких) книг отсутствует 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!
Как устроена эта таблица? По сути датафрейм — это именованный список, каждый элемент которого — это вектор определённой длины (одинаковой для всех векторов, входящих в этот список). Так они одинаковой длины, то их можно «поставить рядом друг с другом» как колонки матрицы. Собственно, так и получается. Вот только в данному случае разные столбцы могут содержать разный тип данных.
И тем не менее, поскольку датафрейм наследует свойства обоих своих «родителей», обращаться с ним можно и как со списком, и как с матрицей:
## 'data.frame': 5 obs. of 4 variables:
## $ name: chr "Иван" "Алёна" "Сергей" "Елена" ...
## $ age : num 23 34 52 19 26
## $ sex : chr "муж" "жен" "муж" "жен" ...
## $ city: chr "Mосква" "Тверь" "Тверь" "Питер" ...
## [1] "Иван" "Алёна" "Сергей" "Елена" "Кристина"
## [1] "Иван" "Алёна" "Сергей"
## [1] "Иван" "Алёна" "Сергей" "Елена" "Кристина"
## name
## 1 Иван
## 2 Алёна
## 3 Сергей
## 4 Елена
## 5 Кристина
## sex city
## 1 муж Mосква
## 2 жен Тверь
Можно добавить новые переменные:
![](img/code.png)
Как вы поняли, возможностей индексации датафрейма — много. Но мы обозначили не все!
Вытащите из имеющегося датафрейма df
строки переменной city
со второй по пятую, не используя команды, обозначенные выше.
Подсказка: как можно ещё индексировать список?
## [1] "Тверь" "Тверь" "Питер" "Москва"
![](img/code.png)
Парой строк выше мы создавали новую переменную в датафрейме с помощью присваивания. Но давайте разнообразим наши данные. Используйте индексацию и оператор присваивания, чтобы привести датафрейм к следующему виду:
## name age sex city married
## 1 Иван 23 муж Mосква FALSE
## 2 Алёна 34 жен Тверь TRUE
## 3 Сергей 52 муж Тверь TRUE
## 4 Елена 19 жен Питер FALSE
## 5 Кристина 26 жен Москва TRUE
Когда мы говорили об индексации векторов, мы затрагивали тему индексации логическими векторами. Ничего не мешает нам воплотить эту идею и с датафреймами:
## [1] "жен" "муж"
Или вот так, например:
## [1] "жен" "муж"
![](img/code.png)
Мы вытащили по логическому условию значения определенной переменной датафрейма. А как вытащить часть датафрейма по некоторому условию? То есть необходимо оставить все колонки, но строки отобрать по определённому условию.
Создайте подвыборку наших респондентов, возраст которых меньше среднего по всей имеющейся выборке.
## name age sex city married
## 1 Иван 23 муж Mосква FALSE
## 4 Елена 19 жен Питер FALSE
## 5 Кристина 26 жен Москва TRUE
Ну, пожалуй, и хватит топтаться в основах — пора уже чем-то серьёзным заняться!