Все в наличии:
[1] 5
[1] 3
[1] 60
[1] 390625
[1] 0.5714286
[1] 1
[1] 2
Скобки работают:
Можно посчитать корень:
Или логарифм:
Джентельменский набор функций богат:
Операторы сравнения:
[1] TRUE
[1] FALSE
[1] TRUE
[1] FALSE
[1] FALSE
[1] TRUE
Логические операторы И (&
) и ИЛИ (|
):
Результаты вычислений и преобразований хотелось бы сохранять.
Над объектами тоже можно совершать разные операции:
Тип данных — это характеристика данных, которая определяет:
numeric
Это числа с десятичной частью.
integer
Это целые числа.
Хм…
complex
Комплексные числа тоже существуют, и мы с ними немного поработаем, чтобы перестать их бояться.
character
Текст тоже надо как-то хранить.
logical
Просто, ясно, лаконично — всего два возможных значения.
factor
Бывают такие переменные, которые группируют наши данные. Например,
Москва
, Санкт-Петербург
, Казань
, Екатеринбург
)бакалавриат
, специалитет
, магистратура
, аспирантура
)group1
, group2
, control
)Обычно они текстовые. Для них был придуман тип данных factor
.
Ordered factor
(упорядоченный фактор) — тип данных, который позволяет задать порядок групп. Например,
bachelor
< master
< phd
< postdoc
easy
< medium
< hard
NA
Пропущенное значение (Not Available). Обозначает отсутствие значения там, где оно вроде бы должно быть. Причины могут быть разные:
NA
, чтобы далее с ними работатьNaN
Это не число (Not a Number).
NULL
Это ничто. Пустота. Используется для задания аргументов функций.
Структура данных — это способ и форма объединения однотипных и/или логически связанных данных.
Воплощение привычной нам «таблицы» в R.
# A tibble: 6 × 10
carat cut color clarity depth table price x y z
<dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
4 0.29 Premium I VS2 62.4 58 334 4.2 4.23 2.63
5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
Это сложная структура данных. Чтобы понять всю её мощь, необходимо начать с более простых.
Вектор — это набор чисел.
\[ \pmatrix{1 & 4 & 36 & -8 & 90.1 & -14.5} \]
Возьмем направленный отрезок (вектор):
Договоримся, что все векторы у нас начинаются из точки \((0, 0)\):
Уберем вектор:
Получается, можно просто записать:
\[ \pmatrix{1 & 2} \]
\[ \pmatrix{1 & 0.5}, \quad \pmatrix{2 & 3}, \quad \pmatrix{4.2 & -3.5} \]
Вектор — это набор элементов одного типа.
Из вектора можно вытащить его элемент:
Для того, чтобы выполнить операцию на всем векторе поэлементно, не нужно перебирать его элементы.
Если мы будем, например, складывать два вектора разной длины, то более короткий зациклится.
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
[,1] [,2] [,3]
[1,] 1 5 9
[2,] 2 6 10
[3,] 3 7 11
[4,] 4 8 12
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 8
[3,] 9 10 11 12
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[3,] 7 8 9
[4,] 10 11 12
Из матрицы можно вытащить её элементы:
При объединении разных типов данных в одном массиве происходит приведение типов (coercion) по следующей иерархии:
logical
→ integer
→ numeric
→ complex
→ character
Списки позволяют объединять массивы различных типов данных.
$v1
[1] 1.0 6.0 -34.0 7.7
$v2
[1] "Москва" "Санкт-Петербург" "Нижний Новгород" "Пермь"
$m1
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
$ls
$ls$v
[1] 1 2 3 4 5 6 7 8 9 10 11 12
$ls$m
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 8
[3,] 9 10 11 12
$v1
[1] 1.0 6.0 -34.0 7.7
[1] 1.0 6.0 -34.0 7.7
$v
[1] 1 2 3 4 5 6 7 8 9 10 11 12
$m
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 8
[3,] 9 10 11 12
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 8
[3,] 9 10 11 12
# A tibble: 6 × 10
carat cut color clarity depth table price x y z
<dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
4 0.29 Premium I VS2 62.4 58 334 4.2 4.23 2.63
5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
[1] 0.23 0.21 0.23 0.29 0.31 0.24
# A tibble: 3 × 10
carat cut color clarity depth table price x y z
<dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
1 0.29 Premium I VS2 62.4 58 334 4.2 4.23 2.63
2 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
3 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
# A tibble: 6 × 3
cut color price
<ord> <ord> <int>
1 Ideal E 326
2 Premium E 326
3 Good E 327
4 Premium I 334
5 Good J 335
6 Very Good J 336
# A tibble: 4 × 2
carat price
<dbl> <int>
1 0.23 326
2 0.21 326
3 0.23 327
4 0.29 334
Если какой-либо кусок кода повторяется более трех раз, имеет смысл обернуть его в функцию.
Функция — это некий черный ящик, который
Элементы функции:
function_name
) — как мы к ней будем обращаться при вызовеarguments
) — какие значения и объекты она принимает на входbody
) — что она делает с входными объектамиreturn()
) — что функция вернет в качестве результата работыВызов функции:
Если функция простая, можно не создавать временные объекты:
Если функция короткая, можно даже не писать return()
:
mr_preproc <- function(d) {
require(tidyverse)
d |> select(
# select columns we need
correctAns,
resp_MR_easy.keys,
resp_MR_easy.corr,
resp_MR_easy.rt
) |>
drop_na() |> # remove technical NAs (recording artefacts, not missing data)
mutate(task = "MR",
# add task name (mental rotation)
level = "easy",
# add difficulty level
trial = 1:16) |> # number trials
rename(
"id" = "Индивидуальный_код",
# rename columns for handy usage
"key" = resp_MR_easy.keys,
"is_correct" = resp_MR_easy.corr,
"rt" = resp_MR_easy.rt
) -> MR
return(MR)
}
Иногда при написании функции может понадобиться обработать какие-то важные случаи.
Для этого подойдет условный оператор.
ms_preproc <- function(d) {
require(tidyverse)
# Since we our participants could fill the fields in any order,
# here is a function which allows us to count correct inputs
# our subjects made.
if ("mouse_MSe.time" %in% colnames(d)) {
d |> select(
"Индивидуальный_код",
matches("^noun"),
matches("resp\\d\\.text$"),
"mouse_MSe.time"
) |>
filter_at(vars(paste0("noun", 1:3)), all_vars(!is.na(.))) |>
filter_at(vars(paste0("noun", 4:7)), all_vars(is.na(.))) |>
mutate(task = "MS",
level = "easy") |>
rename(
"resp1" = resp1.text,
"resp2" = resp2.text,
"resp3" = resp3.text,
"id" = "Индивидуальный_код",
"rt" = "mouse_MSe.time"
) |>
select(-c(paste0("noun", 4:7))) -> MS
} else {
d |> select("Индивидуальный_код",
matches("^noun"),
matches("resp\\d\\.text$")) |>
filter_at(vars(paste0("noun", 1:3)), all_vars(!is.na(.))) |>
filter_at(vars(paste0("noun", 4:7)), all_vars(is.na(.))) |>
mutate(task = "MS",
level = "easy",
rt = NA) |>
rename(
"resp1" = resp1.text,
"resp2" = resp2.text,
"resp3" = resp3.text,
"id" = "Индивидуальный_код"
) |>
select(-c(paste0("noun", 4:7))) -> MS
}
return(MS)
}
Вне функций условный оператор практически не используется, потому что для предобработки данных есть удобная функция ifelse()
.
То, что мы написали функция, чтобы не дублировать код — это хорошо, однако эту функцию нам все равно придется запускать много раз, если нам надо этот кусок кода повторить.
Поэтому используем цикл:
for (i in 1:length(files)) {
print(files[i])
d <- read_csv(files[i], show_col_types = FALSE)
MR_data |> bind_rows(mr_preproc(d) |> mutate(file = files[i])) -> MR_data
ST_data |> bind_rows(st_preproc(d) |> mutate(file = files[i])) -> ST_data
MS_data |> bind_rows(ms_preproc(d) |> mutate(file = files[i])) -> MS_data
}
Да, циклы работает не быстро — это правда. Но, с другой стороны, мы и не терабайты данных анализируем.
Допустим, у нас 50 респондентов. Цикл, подобный тому, что на предыдущем слайде, отбработает секунд за 5. Даже чай не успеете заварить.
Конечно, если у вас огромные датасеты и вы работаете с Big Data, то прогон цикла может значительно затянуться — в этом случае разумно сразу использовать другие инструменты.
Антон Ангельгардт
WLM 2023