Repository navigation
Expand file tree
/
Copy path016-complex_structures.qmd
More file actions
303 lines (198 loc) · 22 KB
/
Copy path016-complex_structures.qmd
File metadata and controls
303 lines (198 loc) · 22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# Сложные структуры данных в R {#sec-complex_structures}
Давайте повторим то, что мы знаем про вектор в R:
- Вектор --- это последовательность из значений.
- Значения идут в определенном порядке, но этот порядок одномерный.
- Внутри вектора могут быть данные только одного типа.
Как вы уже поняли, вектор --- это одно из важнейших понятий в R, и он нам будет встречаться дальше постоянно. Обычно работа с данными --- это именно работа с векторами, различные операции на векторах.
Однако иногда в понятии вектора нам уже становится несколько тесно. Поэтому нам нужно выйти за рамки его ограничений. Во-первых, нужна структура с двумя и более измерениями --- для двух измерений есть **матрица** *(matrix)*, а для трёх и более --- **массив** *(array)*. Во-вторых, нам нужна структура, которая могла бы содержать данные разных типов --- это **список** *(list)*.
## Матрица {#sec-matrix}
Если вдруг вас пугает это слово, то совершенно зря. Матрица --- это всего лишь «двумерный» вектор: вектор, у которого есть не только длина, но и ширина. Создать матрицу можно с помощью функции `matrix()` из вектора, указав при этом количество строк и столбцов.
```{r}
A <- matrix(1:12, nrow = 4)
A
```
::: callout-tip
## *Полезное:* порядок заполнения матрицы
Заметьте, значения вектора заполняются следующим образом: сначала заполняется первый столбик сверху вниз, потом второй сверху вниз и так до конца, т. е. заполнение значений матрицы идет в первую очередь по вертикали. Это довольно стандартный способ создания матриц, характерный не только для R.
<!--# ###Drawing needed### -->
:::
Все остальное так же, как и с векторами: внутри находятся данные только одного типа. Поскольку матрица --- это уже двумерная структура, то у нее имеется два индекса. Эти два индекса разделяются запятыми.
```{r}
A[2, 3]
```
Первый индекс --- выбор строк, второй индекс --- выбор колонок[^016-complex_structures-1]. Результат --- пересечение выбранных строк и столбцов.
[^016-complex_structures-1]: Это универсальный порядок: что в других языках программирования, что в линейной алгебре первый индекс --- выбор строчек, второй индекс --- выбор столбцов.
Так же, как и с векторами, матрицы можно индексировать числовыми векторами:
```{r}
A[1:2, 2:3]
```
{fig-alt="Схема индексирования матрицы: матрица 4×3 со значениями от 1 до 12; при выборе A[1:2, 2:3] подсвечиваются первые две строки и второй–третий столбцы, результат — матрица 2×2 (5, 9 / 6, 10)"}
И даже логическими матрицами (матрицы имеют такие же типы, как и вектора):
```{r}
A[A > 10]
```
В этом случае матрица упростится до вектора.
Если же мы оставляем пустое поле вместо числа, то мы выбираем все строки/колонки в зависимости от того, оставили мы поле пустым до или после запятой:
```{r}
A[, 2:3]
A[1:2, ]
A[, ]
```
Так же, как и в случае с обычными векторами, часть матрицы можно переписать:
```{r}
A[1:2, 1:2] <- 100
A
```
В принципе, это все, что нам нужно знать о матрицах. Матрицы используются в R довольно редко, особенно по сравнению, например, с MATLAB. Но вот индексировать матрицы хорошо бы уметь: это понадобится в работе с датафреймами (см. @sec-df).
::: callout-warning
## *Для продвинутых:* матрица как вектор
То, что матрица --- это просто двумерный вектор, не является метафорой: в R матрица --- это по сути своей вектор с дополнительными *атрибутами* `dim` и (опционально) `dimnames`. Атрибуты --- это свойства объектов, своего рода «метаданные». Для всех объектов есть обязательные атрибуты типа и длины и могут быть любые необязательные атрибуты. Можно задавать свои атрибуты или удалять уже присвоенные: удаление атрибута `dim` у матрицы превратит ее в обычный вектор. Про атрибуты подробнее можно почитать [в этом материале про объекты в R](https://perso.esiee.fr/~courivad/R/06-objects.html) или на стр. 99--101 книги "R in a Nutshell" [@adler2010r].
:::
## Массив {#sec-arrays}
Два измерения --- это не предел! Структура с одним типом данных внутри, но с тремя измерениями или больше, называется массивом. Создание массива очень похоже на создание матрицы: задаем вектор, из которого будет собран массив, и размерность массива.
```{r}
array_3d <- array(1:12, c(3, 2, 2))
array_3d
```
## Список {#sec-list}
Теперь представим себе вектор без ограничения на одинаковые данные внутри. И получим список!
```{r}
simple_list <- list(42, "Пам пам", TRUE)
simple_list
```
А это значит, что там могут содержаться самые разные данные, в том числе и другие списки, векторы и матрицы (и другие объекты, которые нам еще не знакомы)!
```{r}
complex_list <- list(c("Wow", "this", "list", "is", "so", "big"), "16", simple_list, A)
complex_list
```
Если у нас сложный список, то есть очень классная функция `str()`, чтобы посмотреть, как он устроен:
```{r}
str(complex_list)
```
> Представьте, что список --- это такое дерево с ветвистой структурой. А на конце этих ветвей --- листья-векторы.
Как и в случае с векторами, мы можем давать имена элементам списка:
```{r}
named_list <- list(name = "Veronika", age = 26, student = FALSE)
named_list
```
К списку можно обращаться как с помощью индексов, так и по именам. Начнем с последнего:
```{r}
named_list$age
```
А вот с индексами сложнее, и в этом очень легко запутаться. Давайте попробуем сделать так, как мы делали это раньше:
```{r}
named_list[1]
```
Мы, по сути, получили элемент списка --- просто как часть списка, т. е. как список длиной один:
```{r}
class(named_list)
class(named_list[1])
```
А вот чтобы добраться до самого элемента списка (и сделать с ним что-то хорошее), нам нужна не одна, а две квадратных скобочки:
```{r}
named_list[[1]]
class(named_list[[1]])
```
Как и в случае с вектором, к элементу списка можно обращаться по имени. Здесь тоже будет иметь значение, одинарные или двойные квадратные скобки вы используете:
```{r}
named_list["age"]
named_list[["age"]]
```
Хотя последнее --- практически то же самое, что и использование оператора `$`.
{fig-alt='Схема обращения к элементам списка: одинарные скобки LIST[1] и LIST["AGE"] возвращают подсписок, а двойные скобки LIST[[1]], LIST[["AGE"]] и LIST$AGE — сам вектор значений'}
::: callout-tip
## *Полезное:* зачем нужны списки
Списки довольно часто используются в R, но реже, чем в Python. Со многими объектами в R, такими как результаты статистических тестов, удобно работать именно как со списками --- к ним все вышеописанное применимо. Кроме того, некоторые данные мы изначально получаем в виде древообразной структуры --- хочешь не хочешь, а придется работать с этим как со списком. Но обычно после этого стоит как можно скорее превратить список в датафрейм.
:::
## Датафрейм {#sec-df}
Итак, мы перешли к самому главному. Самому-самому. **Датафреймы** *(data frames)*. Более того, сейчас станет понятно, зачем нам нужно было разбираться со всеми предыдущими темами.
Без векторов мы не смогли бы разобраться с матрицами и списками. А без последних мы не сможем понять, что такое датафрейм.
Представьте себе, что мы хотим записать различную информацию о нескольких респондентах. Мы могли бы записать это в список из векторов.
```{r}
list(name = c("Veronika", "Eugeny", "Lena", "Misha", "Sasha"),
age = c(26, 34, 23, 27, 26),
student = c(FALSE, FALSE, TRUE, TRUE, TRUE))
```
Заметьте, у нас получился особенный список: каждый элемент списка --- вектор, причём внутри этих векторов на одних и тех же позициях располагается информация об одних и тех же людях: имя, возраст, является ли человек студентом. Поэтому длина всех векторов внутри этого списка одинаковая --- пять значений (для пяти человек). Датафрейм ухватывает данную структуру:
```{r}
df <- data.frame(name = c("Veronika", "Eugeny", "Lena", "Misha", "Sasha"),
age = c(26, 34, 23, 27, 26),
student = c(FALSE, FALSE, TRUE, TRUE, TRUE))
str(df)
df
```
Так и есть, датафрейм --- это что-то вроде именованного списка, каждый элемент которого является вектором фиксированной длины. Скорее всего, вы представляли список «горизонтально». Если это так, то теперь «переверните» список у себя в голове на 90 градусов. Так, чтобы названия векторов оказались сверху, а элементы списка стали столбцами.
<!--# ###Drawing needed### -->
Поскольку длина всех этих векторов одинаковая (обязательное условие!), то данные представляют собой табличку, похожую на матрицу. Но в отличие от матрицы, разные столбцы могут иметь разные типы данных. В нашем случае первая колонка --- `character`, вторая колонка --- `numeric`, третья колонка --- `logical`. Тем не менее, обращаться с датафреймом можно и как с именованным списком, и как с матрицей:
```{r}
df$age
```
Здесь мы сначала извлекли колонку `age` с помощью оператора `$`. Результатом этой операции является числовой вектор. Колонки датафрейма --- это и есть векторы!
```{r}
df$age[2:3]
```
Теперь с ним можно работать как с обычным вектором: мы вытащили кусок, выбрав индексы `2` и `3`.
Используя оператор `$` и присваивание можно создавать новые колонки датафрейма:
```{r}
df$lovesR <- TRUE # правило ресайклинга — узнали? согласны?
df
```
Ну а можно просто обращаться с помощью двух индексов через запятую, как мы это делали с матрицей:
```{r}
df[3:5, 2:3]
```
Как и с матрицами, первый индекс означает строчки, а второй --- столбцы.
А еще можно использовать названия колонок внутри квадратных скобок:
```{r}
df[1:2, "age"]
df[1:2, c("age", "name")]
```
И здесь перед нами открываются невообразимые возможности! Узнаем, любят ли R те, кто моложе среднего возраста в группе:
```{r}
df[df$age < mean(df$age), 4]
```
Обратите внимание, как удобно нам здесь пригодилось то, что мы научились делать с векторами (@sec-vector). Сначала мы посчитали среднее значение абсолютно так же, как мы делали это с векторами:
```{r}
mean(df$age)
```
Полученное среднее поэлементно сравнили с каждым значением колонки (т. е. вектора) `df$age`:
```{r}
df$age < mean(df$age)
```
Мы получили логический вектор, длина которого совпадает с количеством строк в датафрейме. При этом `TRUE` стоит на тех позициях, где в соответствующей строчке в датафрейме возраст респондента меньше среднего, а `FALSE` --- в остальных случаях. Теперь этот логический вектор мы используем для выбора строк в исходном датафрейме:
```{r}
df[df$age < mean(df$age), ]
```
Наконец, тут же мы можем вытащить нужные колонки, по номеру колонки или ее названию:
```{r}
df[df$age < mean(df$age), 4]
```
Эту же задачу можно выполнить другими способами:
```{r}
df$lovesR[df$age < mean(df$age)]
df[df$age < mean(df$age), "lovesR"]
```
В большинстве случаев подходят сразу несколько способов --- тем не менее, стоит овладеть ими всеми. Чем богаче ваш арсенал инструментов работы в R, тем легче вам обрабатывать свои данные: возможность сделать одно и то же действие разными способами добавляет вам гибкости, потому что разные способы будут более или менее подходящими в разных ситуациях.
Датафреймы удобно просматривать в *RStudio*. Для этого нужно написать команду `View(df)` или же просто нажать на название нужной переменной из списка вверху справа (там, где `Environment`). Тогда увидите табличку, очень похожую на *Excel* и тому подобные программы для работы с таблицами. Там же есть и всякие возможности для фильтрации, сортировки и поиска[^016-complex_structures-2].
[^016-complex_structures-2]: Все, что вы нажмете в этом окошке, никак не повлияет на исходную переменную. Так что можете смело использовать эти функции для исследования содержимого датафрейма.
{fig-alt="Окно `View(df)` в RStudio: таблица с колонками name, age, student, lovesR и панелью Filter для фильтрации и сортировки"}
Но, конечно, интереснее все эти вещи делать руками, т. е. с помощью написания кода.
Датафреймы --- это структура, которая будет встречаться вам чаще всего при работе с данными в R. С одной стороны, кажется, что она все равно довольно ограниченная: в каждой колонке должно быть одинаковое количество значений, внутри колонки только один тип данных. Но именно так обычно и представлены наши данные. Например, если вы загрузите результаты опроса Google Forms в виде таблицы, то каждая строчка будет респондентом, а каждая колонка --- ответом на какой-то вопрос. Поэтому количество значений в каждой колонке будет одинаковым (хотя значения могут быть пропущенными), а каждая колонка --- имеет свой тип. Например, год рождения --- это должна быть числовая колонка, с которой вы сможете делать все, что умеете делать с числовыми колонками: скажем, посчитать возраст. Если в колонке с годом рождения оказалось что-то кроме чисел, то это повод для исследования данных.
## Атрибуты и классы {#sec-attr_class}
::: callout-note
## В разработке
Этот раздел находится в разработке и будет добавлен в следующих версиях учебника.
:::
## Формулы {#sec-formula}
Формулы --- это специальный класс в R, который используется в первую очередь для статистических моделей.
Выглядит формула следующим образом:
```{r}
y ~ x
class(y ~ x)
```
Как видите, здесь нет никаких кавычек, т. е. это не строковое значение, а отдельный класс. В каждой формуле должна быть тильда (`~`), которая имеет смысл знака равно (=) в уравнениях (например, для уравнений линейной регрессии). Слева от `~` обычно находится зависимая переменная, а справа --- независимые (предикторы).
Кроме статистических моделей, формулы используются много где как служебная конструкция, чтобы задать соответствующие пары значений. Вот несколько примеров:
- `case_when()` для задания множественных условий (см. @sec-ifelse),
- визуализация ящиков с усами в базовом R (см. @sec-r_vis),
- статистические тесты для сравнения двух (см. @sec-ttest) и более (см. @sec-anova) групп,
- задание фасеток в `{ggplot2}` (см. @sec-gg_ggplot2).