4 Сложные структуры данных в R
Если вдруг вас пугает это слово, то совершенно зря. Матрица (matrix) – это всего лишь “двумерный” вектор: вектор, у которого есть не только длина, но и ширина. Создать матрицу можно с помощью функции matrix() из вектора, указав при этом количество строк и столбцов.
A matrix(1:20, nrow = 5, ncol = 4) A
[,1] [,2] [,3] [,4] [1,] 1 6 11 16 [2,] 2 7 12 17 [3,] 3 8 13 18 [4,] 4 9 14 19 [5,] 5 10 15 20
Полезное: порядок заполнения матрицы
Заметьте, значения вектора заполняются следующим образом: сначала заполняется первый столбик сверху вниз, потом второй сверху вниз и так до конца, т.е. заполнение значений матрицы идет в первую очередь по вертикали. Это довольно стандартный способ создания матриц, характерный не только для R.
Если мы знаем сколько значений в матрице и сколько мы хотим строк, то количество столбцов указывать необязательно:
A matrix(1:20, nrow = 5) A
[,1] [,2] [,3] [,4] [1,] 1 6 11 16 [2,] 2 7 12 17 [3,] 3 8 13 18 [4,] 4 9 14 19 [5,] 5 10 15 20
Все остальное так же как и с векторами: внутри находится данные только одного типа. Поскольку матрица – это уже двумерный массив, то у него имеется два индекса. Эти два индекса разделяются запятыми.
A[2, 3]
[1] 12
Первый индекс – выбор строк, второй индекс – выбор колонок 1 . Результат – пересечение выбранных строк и столбцов.
Так же как и с векторами, матрицы можно индексировать числовыми векторами:
A[2:4, 1:3]
[,1] [,2] [,3] [1,] 2 7 12 [2,] 3 8 13 [3,] 4 9 14
И даже логическими матрицами (матрицы имеют такие же типы, как и вектора):
A[A > 10]
[1] 11 12 13 14 15 16 17 18 19 20
В этом случае матрица упростится до вектора.
Если же мы оставляем пустое поле вместо числа, то мы выбираем все строки/колонки в зависимости от того, оставили мы поле пустым до или после запятой:
A[, 1:3]
[,1] [,2] [,3] [1,] 1 6 11 [2,] 2 7 12 [3,] 3 8 13 [4,] 4 9 14 [5,] 5 10 15
[,1] [,2] [,3] [,4] [1,] 2 7 12 17 [2,] 3 8 13 18 [3,] 4 9 14 19
[,1] [,2] [,3] [,4] [1,] 1 6 11 16 [2,] 2 7 12 17 [3,] 3 8 13 18 [4,] 4 9 14 19 [5,] 5 10 15 20
Так же как и в случае с обычными векторами, часть матрицы можно переписать:
A[2:4, 2:4] 100 A
[,1] [,2] [,3] [,4] [1,] 1 6 11 16 [2,] 2 100 100 100 [3,] 3 100 100 100 [4,] 4 100 100 100 [5,] 5 10 15 20
В принципе, это все, что нам нужно знать о матрицах. Матрицы используются в R довольно редко, особенно по сравнению, например, с MATLAB. Но вот индексировать матрицы хорошо бы уметь: это понадобится в работе с датафреймами (см. Глава 4.4 ).
Для продвинутых: матрица как вектор
То, что матрица – это просто двумерный вектор, не является метафорой: в R матрица – это по сути своей вектор с дополнительными атрибутами dim и (опционально) dimnames . Атрибуты – это свойства объектов, своего рода “метаданные”. Для всех объектов есть обязательные атрибуты типа и длины и могут быть любые необязательные атрибуты. Можно задавать свои атрибуты или удалять уже присвоенные: удаление атрибута dim у матрицы превратит ее в обычный вектор. Про атрибуты подробнее можно почитать здесь или на стр. 99-101 книги “R in a Nutshell” (Adler 2010) .
4.2 Массив
Два измерения – это не предел! Структура с одним типом данных внутри, но с тремя измерениями или больше, называется массивом (array). Создание массива очень похоже на создание матрицы: задаем вектор, из которого будет собран массив, и размерность массива.
array_3d array(1:12, c(3, 2, 2)) array_3d
, , 1 [,1] [,2] [1,] 1 4 [2,] 2 5 [3,] 3 6 , , 2 [,1] [,2] [1,] 7 10 [2,] 8 11 [3,] 9 12
4.3 Список
Теперь представим себе вектор без ограничения на одинаковые данные внутри. И получим список (list)!
simple_list list(42, "Пам пам", TRUE) simple_list
[[1]] [1] 42 [[2]] [1] "Пам пам" [[3]] [1] TRUE
А это значит, что там могут содержаться самые разные данные, в том числе и другие списки, векторы и матрицы (и другие объекты, которые нам еще не знакомы)!
complex_list list(c("Wow", "this", "list", "is", "so", "big"), "16", simple_list, A) complex_list
[[1]] [1] "Wow" "this" "list" "is" "so" "big" [[2]] [1] "16" [[3]] [[3]][[1]] [1] 42 [[3]][[2]] [1] "Пам пам" [[3]][[3]] [1] TRUE [[4]] [,1] [,2] [,3] [,4] [1,] 1 6 11 16 [2,] 2 100 100 100 [3,] 3 100 100 100 [4,] 4 100 100 100 [5,] 5 10 15 20
Если у нас сложный список, то есть очень классная функция str() , чтобы посмотреть, как он устроен:
str(complex_list)
List of 4 $ : chr [1:6] "Wow" "this" "list" "is" . $ : chr "16" $ :List of 3 ..$ : num 42 ..$ : chr "Пам пам" ..$ : logi TRUE $ : num [1:5, 1:4] 1 2 3 4 5 6 100 100 100 10 .
Представьте, что список — это такое дерево с ветвистой структурой. А на конце этих ветвей — листья-векторы.
Как и в случае с векторами мы можем давать имена элементам списка:
named_list list(name = "Veronika", age = 26, student = FALSE) named_list
$name [1] "Veronika" $age [1] 26 $student [1] FALSE
К списку можно обращаться как с помощью индексов, так и по именам. Начнем с последнего:
named_list$age
[1] 26
А вот с индексами сложнее, и в этом очень легко запутаться. Давайте попробуем сделать так, как мы делали это раньше:
named_list[1]
$name [1] "Veronika"
Мы, по сути, получили элемент списка – просто как часть списка, т.е. как список длиной один:
class(named_list)
[1] "list"
class(named_list[1])
[1] "list"
А вот чтобы добраться до самого элемента списка (и сделать с ним что-то хорошее), нам нужна не одна, а две квадратных скобочки:
named_list[[1]]
[1] "Veronika"
class(named_list[[1]])
[1] "character"
Как и в случае с вектором, к элементу списка можно обращаться по имени. Здесь тоже будет иметь значение, одинарные или двойные квадратные скобки вы используете:
named_list["age"]
$age [1] 26
named_list[["age"]]
[1] 26
Хотя последнее – практически то же самое, что и использование знака $ .
Полезное: зачем нужны списки
Списки довольно часто используются в R, но реже, чем в Python. Со многими объектами в R, такими как результаты статистических тестов, удобно работать именно как со списками – к ним все вышеописанное применимо. Кроме того, некоторые данные мы изначально получаем в виде древообразной структуры – хочешь не хочешь, а придется работать с этим как со списком. Но обычно после этого стоит как можно скорее превратить список в датафрейм.
4.4 Датафрейм
Итак, мы перешли к самому главному. Самому-самому. Датафреймы (dataframes). Более того, сейчас станет понятно, зачем нам нужно было разбираться со всеми предыдущими темами.
Без векторов мы не смогли бы разобраться с матрицами и списками. А без последних мы не сможем понять, что такое датафрейм.
Представьте себе, что мы хотим записать различную информацию о нескольких респондентах. Мы могли бы записать это в список из векторов.
list(name = c("Veronika", "Eugeny", "Lena", "Misha", "Sasha"), age = c(26, 34, 23, 27, 26), student = c(FALSE, FALSE, TRUE, TRUE, TRUE))
$name [1] "Veronika" "Eugeny" "Lena" "Misha" "Sasha" $age [1] 26 34 23 27 26 $student [1] FALSE FALSE TRUE TRUE TRUE
Датафрейм очень похож на список. Просто поменяем в команде выше list() на data.frame() и посмотрим, что изменится:
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)
'data.frame': 5 obs. of 3 variables: $ name : chr "Veronika" "Eugeny" "Lena" "Misha" . $ age : num 26 34 23 27 26 $ student: logi FALSE FALSE TRUE TRUE TRUE
name age student 1 Veronika 26 FALSE 2 Eugeny 34 FALSE 3 Lena 23 TRUE 4 Misha 27 TRUE 5 Sasha 26 TRUE
Вообще, очень похоже на список, не правда ли? Так и есть, датафрейм – это что-то вроде проименованного списка, каждый элемент которого является atomic вектором фиксированной длины. Скорее всего, вы представляли список “горизонтально”. Если это так, то теперь “переверните” список у себя в голове на 90 градусов. Так, чтобы названия векторов оказались сверху, а элементы списка стали столбцами.
Поскольку длина всех этих векторов одинаковая (обязательное условие!), то данные представляют собой табличку, похожую на матрицу. Но в отличие от матрицы, разные столбцы могут иметь разные типы данных. В нашем случае первая колонка – character , вторая колонка – numeric , третья колонка – logical . Тем не менее, обращаться с датафреймом можно и как с проименованным списком, и как с матрицей:
df$age
[1] 26 34 23 27 26
Здесь мы сначала извлекли колонку age с помощью оператора $ . Результатом этой операции является числовой вектор. Колонки датафрейма – это и есть векторы!
df$age[2:3]
[1] 34 23
Теперь с ним можно работать как с обычным вектором: мы вытащили кусок, выбрав индексы 2 и 3 .
Используя оператор $ и присваивание можно создавать новые колонки датафрейма:
df$lovesR TRUE #правило recycling - узнали? согласны? df
name age student lovesR 1 Veronika 26 FALSE TRUE 2 Eugeny 34 FALSE TRUE 3 Lena 23 TRUE TRUE 4 Misha 27 TRUE TRUE 5 Sasha 26 TRUE TRUE
Ну а можно просто обращаться с помощью двух индексов через запятую, как мы это делали с матрицей:
df[3:5, 2:3]
age student 3 23 TRUE 4 27 TRUE 5 26 TRUE
Как и с матрицами, первый индекс означает строчки, а второй – столбцы.
А еще можно использовать названия колонок внутри квадратных скобок:
df[1:2, "age"]
[1] 26 34
df[1:2, c("age", "name")]
age name 1 26 Veronika 2 34 Eugeny
И здесь перед нами открываются невообразимые возможности! Узнаем, любят ли R те, кто моложе среднего возраста в группе:
df[df$age mean(df$age), 4]
[1] TRUE TRUE TRUE TRUE
Обратите внимание, как удобно нам здесь пригодилось то, что мы научились делать с векторами ( Глава 3 ). Сначала мы посчитали среднее значение абсолютно так же, как мы делали это с векторами:
mean(df$age)
[1] 27.2
Полученное среднее поэлементно сравнили с каждым значением колонки (т.е. вектора) df$age :
df$age mean(df$age)
[1] TRUE FALSE TRUE TRUE TRUE
Мы получили логический вектор, длина которого совпадает с длиной датафрейма. При этом TRUE стоит на тех позициях, где в соответствующей строчке в датафрейме возраст респондента больше среднего, а FALSE – в остальных случаях. Теперь этот логический вектор мы используем для выбора строк в исходном датафрейме:
df[df$age mean(df$age), ]
name age student lovesR 1 Veronika 26 FALSE TRUE 3 Lena 23 TRUE TRUE 4 Misha 27 TRUE TRUE 5 Sasha 26 TRUE TRUE
Наконец, тут же мы можем вытащить нужные колонки, по номеру колонки или ее названию:
df[df$age mean(df$age), 4]
[1] TRUE TRUE TRUE TRUE
Эту же задачу можно выполнить другими способами:
df$lovesR[df$age mean(df$age)]
[1] TRUE TRUE TRUE TRUE
df[df$age mean(df$age), 'lovesR']
[1] TRUE TRUE TRUE TRUE
В большинстве случаев подходят сразу несколько способов – тем не менее, стоит овладеть ими всеми. Чем богаче ваш арсенал инструментов работы в R, тем легче вам обрабатывать свои данные: возможность сделать одно и то же действие добавляет вам гибкости, потому что разные способы будут более или менее подходящими в разных ситуациях.
Датафреймы удобно просматривать в RStudio. Для это нужно написать команду View(df) или же просто нажать на названии нужной переменной из списка вверху справа (там где Environment). Тогда увидите табличку, очень похожую на Excel и тому подобные программы для работы с таблицами. Там же есть и всякие возможности для фильтрации, сортировки и поиска 2 .

Но, конечно, интереснее все эти вещи делать руками, т.е. с помощью написания кода.
Датафреймы – это структура, которая будет встречаться вам чаще всего при работе с данными в R. С одной стороны, кажется, что она все равно довольно ограниченная: в каждой колонке должно быть одинаковое количество значений, внутри колонки только один тип данных. Но именно так обычно и представлены наши данные. Например, если вы загрузите результаты опроса Google Forms в виде таблицы, то каждая строчка будет респондентом, а каждая колонка – ответом на какой-то вопрос. Поэтому количество значений в каждой колонке будет одинаковым (хотя значения могут быть пропущенными), а каждая колонка – имеет свой тип. Например, год рождения – и это должна быть числовая колонка, с которой вы сможете делать все, что вы умеете делать с числовыми колонками. Например, посчитать возраст. Если в колонке с годом рождения оказалось что-то кроме чисел, то это повод для исследования данных.
4.5 Атрибуты и классы
4.6 Формулы
Формулы – это специальный класс в R, который используется в первую очередь для статистических моделей.
Выглядит формула следующим образом:
y ~ x
Вокруг data.table
Эта заметка будет интересна для тех, кто использует библиотеку обработки табличных данных для R — data.table, и, возможно, будет рад увидеть гибкость ее применения на различных примерах.
Вдохновившись хорошим примером коллеги, и надеясь, что вы уже почитали его статью, предлагаю глубже копнуть в сторону оптимизации кода и производительности на основе data.table.
Введение: откуда идет data.table?
Лучше всего начать знакомство с библиотекой немного издалека, а именно, со структур данных, из которых может быть получен объект data.table (далее, ДТ).
Массив
## arrays --------- arrmatr
Одна из таких структур — это массив (?base::array). Как и в других языках массивы здесь многомерны. Однако интересным является то, что, например, двумерный массив начинает наследовать свойства от класса матрицы (?base::matrix), а одномерный массив, что тоже важно, не наследует от вектора (?base::vector).
При этом надо понимать, что тип данных, содержащихся в каком-либо объекте следует проверять функцией base::typeof, которая возвращает внутреннее описание типа согласно R Internals — общим протоколом языка, связанным с первородным C.
Еще одна команда, для определения класса объекта, base::class, возвращает в случае векторов векторный тип (он отличается названием от внутреннего, но позволяет также понять тип данных).
Список
Из двумерного массива, он же матрица, можно перейти к списку (?base::list).
## lists ------------------ mylist
При этом происходят несколько вещей сразу:
- Схлопывается второе измерение матрицы, то есть, мы получаем одновременно и список и вектор.
- Список, таким образом, наследует от этих классов. Надо иметь в виду, что элементу списка будет соответствовать одно (скалярное) значение из ячейки матрицы-массива.
Датафрейм
От списка, матрицы или вектора можно перейти к датафрейму (?base::data.frame).
## data.frames ------------ df
Что в нем интересно: датафрейм наследует от списка! Колонки датафрейма являются ячейками списка. Это будет важно в дальнейшем, когда мы будем использовать функции, применяемые к спискам.
data.table
Получить ДТ (?data.table::data.table) можно из датафрейма, списка, вектора или матрицы. Например, вот так (in place).
## data.tables ----------------------- library(data.table) data.table::setDT(df) is.list(df) is.data.frame(df) is.data.table(df)
Полезно то, что, как и датафрейм, ДТ наследует свойства списка.
ДТ и память
В отличие от всех остальных объектов в R base, ДТ передаются по ссылке. Если нужно сделать копирование в новую область памяти, нужна функция data.table::copy либо нужно сделать выборку из старого объекта.
На этом введение подходит к концу. ДТ — это продолжение развития структур данных в R, которое преимущественно происходит за счет расширения и ускорения операций, производимых над объектами класса датафрейм. При этом сохраняется наследование от других примитивов.
Некоторые примеры использования свойств data.table
Как список.
Итерировать по строкам датафрейма или ДТ не лучшая идея, так как код цикла на языке R гораздно медленее C, а пройтись в цикле по столбцам, которых, обычно, гораздно меньше, вполне можно. Идя по столбцам, помним, что каждый столбец это элемент списка, содержащий, как правило, вектор. А операции над векторами хорошо векторизованы в базовых функциях языка. Также можно использовать операторы выборки, свойственные спискам и векторам: `[[`, `$`.
## operations on data.tables ------------ #using list properties df$'V1'[1] df[['V1']] df[[1]][1] sapply(df, class) sapply(df, function(x) sum(is.na(x)))
Векторизация
Если есть необходимость пройти по строкам большого ДТ, лучшим решением будет написание функции с векторизацией. Но если это не получается, то следует помнить, что цикл внутри ДТ все равно быстрее цикла в R, так как выполняется на C.
Попробуем на бОльшем примере со 100К строк. Будем вытаскивать первую букву из слов, входящих в вектор-колонку w.
library(magrittr) library(microbenchmark) ## Bigger example ---- rown % .[, d := 1 + b + c + rnorm(nrow(.))] # vectorization microbenchmark(< dt[ , first_l := unlist(strsplit(w, split = ' ', fixed = T))[1] , by = 1:nrow(dt) ] >) # second first_l_f % do.call(rbind, .) %>% `[`(,1) > dt[, first_l := NULL] microbenchmark(< dt[ , first_l := .(first_l_f(w)) ] >) # third first_l_f2 % unlist %>% matrix(nrow = 3) %>% `[`(1,) > dt[, first_l := NULL] microbenchmark(< dt[ , first_l := .(first_l_f2(w)) ] >)
Первый прогон с итерацией по строкам:
Второй прогон, где векторизация идет через обращение списка в матрицу и взятие элементов на срезе с индексом 1 (последнее и есть собственно векторизация). Поправлюсь: векторизация на уровне функции strsplit, которая умеет принимать вектор на вход. Оказывается, процедура превращения списка в матрицу намного тяжелее самой векторизации, но и в этом случае намного быстрее невекторизованного варианта.
Unit: milliseconds
expr min lq mean median uq max neval
< dt[, `:=`(first_l, .(first_l_f(w)))] >93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100
Ускорение по медиане в 3 раз.
Третий прогон, где изменена схема превращения в матрицу.
Unit: milliseconds
expr min lq mean median uq max neval
< dt[, `:=`(first_l, .(first_l_f2(w)))] >32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100
Ускорение по медиане в 13 раз.
С этим делом надо экспериментировать, чем больше — тем лучше будет.
Еще один пример с векторизацией, где также текст, но он приближен к реальным условиям: разная длина слов, разное количество слов. Требуется достать первые 3 слова. Вот так:

Здесь уже предыдущая функция не работает, так как векторы разной длины, а мы задавали размер матрицы. Переделаем это, покопавшись в интернетах.
# fourth rown ) , collapse = ' ' ) > ) dt % .[, d := 1 + b + c + rnorm(nrow(.))] first_l_f3 % `[`(n,) %>% as.character > microbenchmark(< dt[ , (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x)) ] >) dt[ , (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x)) ]
Unit: milliseconds
expr min lq mean median
< dt[, `:=`((paste0(«w_», 1:3)), strsplit(w, split = " ", fixed = T))] >851.7623 916.071 1054.5 1035.199
uq max neval
1178.738 1356.816 100
Скрипт отработал со средней скоростью 1 секунда. Неплохо.
Еще один, более экономичный способ, найденный kablag:
# fifth rown ) , collapse = ' ' ) > ) dt % .[, d := 1 + b + c + rnorm(nrow(.))] microbenchmark(< w_split )
Медиана 186, дешевле в 5 раз.
Связанные одной цепью.
С объектами ДТ можно работать, используя chaining. Выглядит это как прицепление синтаксиса скобок справа, по сути, сахарок.
# chaining res1
Течет по трубам.
Такие же операции можно сделать через piping, выглядит похоже, но функционально богаче, так как можно использовать любые методы, а не только ДТ. Выведем коээфициенты логистической регрессии для наших синтетических данных с рядом фильтров на ДТ.
# piping samplpe_b % .[a %in% head(letters)] %>% .[, < dt0 % quantile(seq(0.1, 1, 0.1), na.rm = T) .(q = quants) > , .(cond = b > samplpe_b) ] %>% glm( cond ~ q -1 , family = binomial(link = "logit") , data = . ) %>% summary %>% .[[12]]
Статистика, машинное обучение и прочее внутри ДТ
Можно использовать лямбда-функции, но иногда лучше создать их отдельно, прописать весь пайплайн анализа данных, и вперед — они работают внутри ДТ. Пример обогащен всеми вышеперечисленными фичами, плюс несколько полезных вещей из арсенала ДТ (таких как обращение к самому ДТ внутри ДТ по ссылке, вставленных иногда не последовательно, но чтобы было).
# function rm(lm_preds) lm_preds else < lmm res > res5 % .[e < 0] %>% .[.[, .I[b > 0]]] %>% .[, `:=` ( low = as.numeric(lm_preds(.SD, .BY, .N)[[1]]) , mean = as.numeric(lm_preds(.SD, .BY, .N)[[2]]) , high = as.numeric(lm_preds(.SD, .BY, .N)[[3]]) , coef_c = as.numeric(lm_preds(.SD, .BY, .N)[[4]][1]) , coef_b = as.numeric(lm_preds(.SD, .BY, .N)[[4]][2]) , coef_int = as.numeric(lm_preds(.SD, .BY, .N)[[4]][3]) ) , a ] %>% .[!is.na(mean), -'e', with = F] # plot plo % ggplot + facet_wrap(~ a) + geom_ribbon( aes( x = c * coef_c + b * coef_b + coef_int , ymin = low , ymax = high , fill = a ) , size = 0.1 , alpha = 0.1 ) + geom_point( aes( x = c * coef_c + b * coef_b + coef_int , y = mean , color = a ) , size = 1 ) + geom_point( aes( x = c * coef_c + b * coef_b + coef_int , y = d ) , size = 1 , color = 'black' ) + theme_minimal() print(plo)
Заключение
Я надеюсь, что смог создать цельную, но, конечно, не полную, картину такого объекта как data.table, начиная от его свойств связанных с наследованием от классов R и заканчивая его собственными фишками и окружением из элементов tidyverse. Надеюсь, это поможет вам лучше изучить и применять эту библиотеку для работы и развлечения.

Полный код
## load libs ---------------- library(data.table) library(ggplot2) library(magrittr) library(microbenchmark) ## arrays --------- arrmatr % .[, d := 1 + b + c + rnorm(nrow(.))] # vectorization # zero - for loop microbenchmark( < for(i in 1:nrow(dt)) < dt[ i , first_l := unlist(strsplit(w, split = ' ', fixed = T))[1] ] >>) # first microbenchmark(< dt[ , first_l := unlist(strsplit(w, split = ' ', fixed = T))[1] , by = 1:nrow(dt) ] >) # second first_l_f % do.call(rbind, .) %>% `[`(,1) > dt[, first_l := NULL] microbenchmark(< dt[ , first_l := .(first_l_f(w)) ] >) # third first_l_f2 % unlist %>% matrix(nrow = 3) %>% `[`(1,) > dt[, first_l := NULL] microbenchmark(< dt[ , first_l := .(first_l_f2(w)) ] >) # fourth rown ) , collapse = ' ' ) > ) dt % .[, d := 1 + b + c + rnorm(nrow(.))] first_l_f3 % `[`(n,) %>% as.character > microbenchmark(< dt[ , (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x)) ] >) dt[ , (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x)) ] # chaining res1 % .[a %in% head(letters)] %>% .[, < dt0 % quantile(seq(0.1, 1, 0.1), na.rm = T) .(q = quants) > , .(cond = b > samplpe_b) ] %>% glm( cond ~ q -1 , family = binomial(link = "logit") , data = . ) %>% summary %>% .[[12]] # function rm(lm_preds) lm_preds else < lmm res > res5 % .[e < 0] %>% .[.[, .I[b > 0]]] %>% .[, `:=` ( low = as.numeric(lm_preds(.SD, .BY, .N)[[1]]) , mean = as.numeric(lm_preds(.SD, .BY, .N)[[2]]) , high = as.numeric(lm_preds(.SD, .BY, .N)[[3]]) , coef_c = as.numeric(lm_preds(.SD, .BY, .N)[[4]][1]) , coef_b = as.numeric(lm_preds(.SD, .BY, .N)[[4]][2]) , coef_int = as.numeric(lm_preds(.SD, .BY, .N)[[4]][3]) ) , a ] %>% .[!is.na(mean), -'e', with = F] # plot plo % ggplot + facet_wrap(~ a) + geom_ribbon( aes( x = c * coef_c + b * coef_b + coef_int , ymin = low , ymax = high , fill = a ) , size = 0.1 , alpha = 0.1 ) + geom_point( aes( x = c * coef_c + b * coef_b + coef_int , y = mean , color = a ) , size = 1 ) + geom_point( aes( x = c * coef_c + b * coef_b + coef_int , y = d ) , size = 1 , color = 'black' ) + theme_minimal() print(plo)
Введение в R. Разбираемся в анализе данных с использованием статистического пакета
В прошлой статье из декабрьского номера я начал говорить об анализе данных и закончил на том, как быстро решить задачу линейной регрессии на R. Сегодня я более подробно расскажу об R как о языке программирования.
Когда‑то это был язык для работы со статистикой, но сейчас его вполне можно считать языком общего назначения (хотя основную свою направленность он сохранил). Кто не верит, может заглянуть на страничку проекта Shiny, с помощью которого любой может создавать полноценные веб‑приложения на R.
Ну да ладно, нас язык R интересует именно в той области, где он действительно хорош. В этой статье я расскажу о самых базовых объектах в языке и его особенностях. Кто‑то мудрый давно заметил, что самое хорошее в языке R — это то, что он был создан специалистами по статистике, а самое плохое — то, что он был создан специалистами по статистике :).
Опустив вопросы установки R, с которыми довольно легко справиться (тем более что про это я немного уже писал в прошлой статье), приступим к изучению языка.
Все, что вы хотели знать о функции, но боялись спросить
В языке R доступна очень большая инфраструктура пакетов и, как следствие, совершенно невероятное количество функций для повседневного использования. Что делать, если ты знаешь название, но не помнишь правильное употребление функции? Решением станет очень приличный help, правильным образом встроенный в систему. Для того чтобы получить справку об использовании той или иной функции, достаточно набрать ?имя_функции .
Векторы
Начнем с базовых объектов, из которых состоит язык. В большинстве языков программирования, с которыми тебе приходилось иметь дело, примитивными объектами являются числа, объекты булева типа и прочие действительно примитивные вещи.
В языке R полно сюрпризов, и первый из них заключается в том, что примитивным объектом в R является вектор, то есть совокупность значений одной природы. К примеру, вектор вещественных чисел. Хочется спросить, а как быть с обычными числами? Скажем, с числом 10. Ответ на этот вопрос довольно прост — это вектор из одного элемента.
Векторы бывают следующих типов:
- целые;
- числовые (вещественные);
- символьные;
- комплексные;
- логические.
По умолчанию числа в R — это вещественные числа, то есть числа с плавающей запятой. Для того чтобы явно указать R, что число, с которым ты собираешься иметь дела, целое, нужно добавить суффикс L. Например, 10L. Это легко иллюстрирует следующий код:
[ 1 ] "integer"
Здесь будет использоваться символ приглашения > , чтобы отличать код от ответа системы. В работе с числами существует специальный символ Inf для представления бесконечности.
Следует обратить внимание на оператор присваивания < - . Рассмотрим следующий фрагмент:
> x # печатаем x
> print ( x ) # еще раз печатаем
В этом, казалось бы, очевидном фрагменте кода есть два важных момента: печатать можно, просто указывая имя переменной в строке или используя функцию, предназначенную для печати. Это довольно характерно для всех языков, в которых есть интерактивный интерпретатор REPL (Read — Evaluate — Print Loop). Функция print скорее используется для печати внутри других функций для отладки или просто для вывода какой‑либо информации. Что более важно и менее очевидно, строка [ 1] 10 , выводимая в качестве результата в R, говорит, что это первый (и единственный) элемент вектора.
Число 1 в квадратных скобках выводится для удобства чтения. К примеру, если вектор не влезает в ширину экрана, то он разбивается на строки и числа перед каждой строкой — это индекс элемента вектора, с которого начинается данная строка.
Для создания обычного вектора используется функция c :
> x < - c ( 1 , 2 , 3 )
Так же как и во многих других языках, можно создавать векторы, указывая интервал значений:
> x < - c ( 1 : 3 )
Казалось бы, результат тот же, однако все не совсем так. Так как это целочисленный интервал, содержимое второго вектора — это целые числа, а первого — вещественные. Что легко проверить с помощью функции typeof . В последнем случае еще можно писать просто x < - 1: 3 . Значения булева типа в языке R выглядят как TRUE и FALSE или просто T и F . Для того чтобы создать пустой вектор нужного типа, необходимо использовать функцию vector .
> x < - vector ( "numeric" , length = 10 ) [ 1 ] 0 0 0 0 0 0 0 0 0 0
Неявное преобразование типов в R хорошо иллюстрируется следующим примером:
> x < - c ( "a" , TRUE , 1. 3 ) [ 1 ] "a" "TRUE" "1. 3" > y < - c ( 2 , TRUE , FALSE )
Часто бывает необходимо воспользоваться явным преобразованием типов. Рассмотрим пример:
> as ( TRUE , "character" )
> as. character ( TRUE )
Два строчки делают в точности то же самое, однако с точки зрения читаемости кода
второй подход выглядит более предпочтительным. Вообще, функции создания, проверки и преобразования типов легко запомнить следующим образом. Для создания (пустого вектора строк) используется character( length=5) , где length — количество элементов, is. character используется для сравнения, а as. character для преобразования. Когда преобразование невозможно, то его результатом будет специальное значение NA .
Элементы вектора могут быть заименованы, это можно сделать следующим образом:
> v < - c ( x = 1. 0 , y = 2. 5 , z = -0. 1 ) 1. 0 2. 5 -0. 1 > u < - c ( 1. 0 , -0. 5 , -0. 5 ) > names ( u ) < - c ( "x" , "y" , "z" )
Матрицы
С векторами все довольно просто, давай теперь попробуем разобраться с другой полезной структурой данных — матрицами. Для создания матрицы есть специальная функция matrix :
> m < - matrix ( nrow = 2 , ncol = 3 ) [ , 1 ] [ , 2 ] [ , 3 ] [ 1 , ] NA NA NA [ 2 , ] NA NA NA
Как видно, изначально создается пустая матрица. Для того чтобы получить размеры матрицы, существует специальный атрибут dim :
> attributes ( m )
Следует отметить, что в смысле хранения двумерных объектов (массивов, матриц) все языки делятся на две группы: те, что хранят матрицы по строкам, такие как C и Java, и те, что хранят по столбцам, — это, к примеру, FORTRAN и R. В том, что это именно так, легко убедиться следующим образом:
> m < - matrix ( 1 : 6 , nrow = 2 , ncol = 3 ) [ , 1 ] [ , 2 ] [ , 3 ]
Причем задавать двумерную структуру можно, просто добавляя атрибут dim к вектору:
> dim ( v ) < - c ( 2 , 3 ) [ , 1 ] [ , 2 ] [ , 3 ]
Сверх того, строкам и колонкам матрицы можно давать имена:
> m < - matrix ( 1 : 4 , nrow = 2 , ncol = 2 ) > dimnames ( m ) < - list ( c ( "a" , "b" ) , c ( "c" , "d" ) )
В языке R существует также механизм создания двумерных структур из одномерных с помощью операций присоединения строки или столбца:
> y < - 11 : 13
> cbind ( x , y )
> rbind ( x , y )
[ , 1 ] [ , 2 ] [ , 3 ]
y 11 12 13
О том, как работать с отдельными элементами структур данных, я напишу чуть позже, а пока продолжим разбираться с самими структурами.
Списки и факторы
В этом разделе рассмотрим еще две полезные структуры данных. Первая — это, конечно, список. Дело в том, что часто приходится хранить данные разного типа в одном месте.
Как мы знаем, вектор здесь не подходит, потому что его элементы должны быть одного типа, поэтому в R предусмотрены списки:
> lst < - list ( "hello" , 1. 5 , TRUE , 1+2i )
Как видно, в списке содержится четыре элемента, и все они разного типа: строка, вещественное число, булево значение и комплексное число. Элементы списка можно именовать, как и элементы вектора:
> l < - list ( a = "test" , b = 3. 14 )
Во многих языках программирования есть перечислимый тип данных. Он нужен для того, чтобы работать с данными, в качестве значения которых могут выступать элементы конечного множества. Когда такого типа в языке нет, этот вопрос решается с помощью констант для соответствующего набора значений. В языке R для обеспечения подобной функциональности есть специальный тип — фактор:
> x < - factor ( c ( "yes" , "no" , "yes" , "no" , "no" ) ) [ 1 ] yes no yes no no Levels : no yes
Здесь создается вектор факторов с двумя возможными значениями: yes и no. Можно, к примеру, подсчитать, сколько соответствующих значений есть в нашем векторе:
Фрейм данных (Data Frame)
Фрейм данных — один из самых полезных типов данных в R. Когда мы работаем с реальными табличными данными, именно этот тип представляет таблицы. В отличие от матриц, данный тип позволяет хранить различные типы данных в разных колонках. С точки зрения хранения этот тип данных можно представить как список специального вида, где элементами списка являются списки одинаковой длины (колонки). Для загрузки фрейма данных из CSV-файла существует функция read. csv , которая уже встречалась нам в предыдущей статье этой серии.
Можно создать фрейм данных вручную, например так:
> x < - data. frame ( a = c ( F , F , T , T ) , b = c ( F , T , F , T ) , or = c ( F , T , T , T ) ) 1 FALSE FALSE FALSE 2 FALSE TRUE TRUE 3 TRUE FALSE TRUE 4 TRUE TRUE TRUE
Помимо атрибута names , для фрейма данных также есть row. names :
> row. names ( x )
[ 1 ] "1" "2" "3" "4"
Доступ к элементам
Как ты мог заметить, до сих пор я рассказывал лишь о том, какие бывают структуры данных, но ничего не сказал про то, как получать доступ к отдельным элементам или подмножествам. Посмотрим, как это работает, на простом примере с вектором:
> x < - c ( 11 , 21 , 31 , 41 , 11 , 21 , 31 ) [ 1 ] 31 41 31 [ 1 ] FALSE FALSE TRUE TRUE FALSE FALSE TRUE [ 1 ] 31 41 31 [ 1 ] 11 21 31
Наверное, единственный комментарий, который требуется к данному примеру, — это то, что операция > работает как векторная операция и результатом ее выполнения будет вектор булевых значений, этот вектор может быть использован для выборки данных из вектора.
При работе с матрицами также не возникает никаких сложностей, нужные строки и столбцы мы можем получить легко и непринужденно. К примеру, запись x[ 2, 3] — это элемент во второй строке и в третьем столбце, а x[ 1, ] и z[, 2] — это первая строка и второй столбец соответственно. По умолчанию эти операции возвращают вектор, а не матрицу, в которой одна строка или столбец, и если мы хотим, чтобы результатом выполнения данной операции была все‑таки матрица, пусть и другого размера, то нужно использовать дополнительный параметр x[ 1, , drop=FALSE] .
С доступом к элементам списка все немного сложнее. Рассмотрим следующий пример:
> l < - list ( a = 0. 5 , b = 1 : 3 ) [ 1 ] "integer"
На этом примере достаточно хорошо видны особенности доступа к элементам в R.
Как можно заметить, использование [[] ] не гарантирует соответствие типа возвращаемого значения изначальному, а в случае одинарных скобок [ ] возвращаемое значение также является списком.
В этом смысле $ и [[] ] работают очень похоже. Хотя есть некоторая особенность — значение в двойных квадратных скобках может быть вычислено, а имя после знака $ — нет.
Списки бывают вложенными, и доступ к их элементам осуществляется с помощью вложенных скобок, как и полагается: x[[ 1]][[ 3] ] , однако можно сделать запись чуть более понятной, используя функцию c . К примеру, последнее выражение можно записать как x[[ c( 1, 3)] ] . Причем использовать селектор с одной скобкой не получится (подумай почему).
Управляющие структуры
Надо заметить, что в данной статье я рассматриваю R именно как язык программирования, но на самом деле можно было бы взглянуть на него как на систему для работы со статистикой с типичными функциями для решения повседневных задач. Однако я предпочту быть консервативным в изложении и, как полагается после описания базовых типов, перейду к разговору об условных блоках и циклах.
Начнем с условного оператора. Надо сказать, что здесь в R нет почти ничего особенного, но все‑таки:
if ( x > 0 ) < y < - x >else < y < - - x >
Ничего нового тут нет, и else может быть опущен. Хотя и тут не обошлось без несколько необычного поведения. В функциональных языках, таких как Haskell, конструкция if является выражением, а не оператором, то есть возвращает значение, а не изменяет состояние. В языке R эта идея также нашла себе место в следующей конструкции:
y < - if ( x >0 ) < x >else < - x ># фигурные скобки можно опустить
Последняя конструкция делает то же самое, что и предыдущая, только в функциональном стиле. Однако в функциональных языках выражение должно быть определено и поэтому наличие ветви else обязательно. Здесь же это не так, конструкция z < - if ( x < 0) -x вполне допустима, но значением этого выражения при x >0 будет специальное значение NULL . Для сравнения с этим значением можно использовать функцию is. null .
Теперь стоит сказать пару слов о циклах. Циклы в R работают медленно, но когда мы имеем сравнительно небольшой объем данных, то использование циклов может быть вполне допустимо и даже удобно. С этой точки зрения R также мало отличается от других языков программирования. Начнем с цикла for , который реализует известную парадигму for-in.
x < - c ( "a" , "b" , "c" , "d" , "e" ) for ( i in 1 : 5 ) < print ( x [ i ] ) for ( ch in x ) print ( ch )
Как и полагается, цикл for-in реализует процесс итерации по вектору или последовательности, последний вариант более лаконичен, однако если нам каким‑либо образом нужны индексы, то хорошо бы иметь возможность создавать последовательность, соответствующую заданному вектору.
Для этого в R предусмотрена специальная функция seq_along , принимающая в качестве аргумента вектор или список, для которых строится последовательность индексов. Таким образом, первый цикл можно было бы переписать в виде for( i in seq_along( x)) < . >. Для того чтобы сгенерировать последовательность заданной длины, можно воспользоваться функцией seq_len .
Кстати, все эти вопросы легко решить известными средствами, используя лишь функцию вычисления длины length . Цикл while имеет вполне классическую форму while ( cond) < . >.
Логические связки в R также выглядят стандартным образом: && , || и ! . В дополнение к банальному while( TRUE) в R присутствует небанальный repeat < . >, выход из которого обеспечивает, как обычно, комбинация if и break . Для перехода к следующей итерации предусмотрен оператор с несколько неожиданным названием next .
Как можно заметить, разделителей вроде точки с запятой между операторами в R нет.
Функции
Как уж без функций в приличном языке программирования? В самом общем виде определение функции выглядит следующим образом:
Как и в функциональных языках, функции в R являются объектами класса. Это означает, что их можно передать в качестве аргумента в другую функцию и вернуть в качестве значения. Анонимные (лямбда) функции также присутствуют:
f < - function ( g ) < function ( x ) g ( g ( x ) ) y < - f ( function ( x ) x * x ) ( 5 )
Здесь функция f принимает в качестве аргумента функцию g и возвращает функцию, которая имеет один формальный параметр x и дважды применяет к нему функцию g . Также в коде можно увидеть передачу анонимной функции в качестве аргумента, а полученный результат (композиция функций g и самой себя) применяется к числу 5. Таким образом, число 5 будет дважды возведено в квадрат.
Порядок вычисления аргументов в R является отложенным (lazy), то есть аргумент не вычисляется, если он не нужен:
> f < - function ( x , y ) x * x > f ( 3 , 5 / 0 )
Для того чтобы правильно работать с состоянием в случае замыканий, существует оператор
Как создать DataFrame в Pandas?


Хотите создать DataFrame, но не знаете как? Мы поделимся с Вами сразу несколькими ваиантами! Однако, не бывает практики без теории, поэтому сначала — небольшой ликбез о том, что такое DataFrame и какое данные он может содержать.
Краткий ликбез о DataFrame
Говоря языком простым, DataFrame — это объект, созданный по образу и подобию таблицы, либо двумерной матрицы. При этом столбцы и строки таблицы могут быть именованы либо проиндексированы, а хранящиеся в DataFrame данные могут быть разных типов. На изображении ниже приведен пример DataFrame с именованными строками и столбцами, содержащий отметки учеников по разным предметам:

Если Вы работаете с большими данными, то, вероятнее всего, Вам чаще всего приходится иметь дело с табличными данными, в которых строки пронумерованы. Давайте посмотрим, как будет выглядеть DataFrame с пронумерованными строками и именованными столбцами:

Как же создать DataFrame ?
Существует несколько вариантов создания DataFrame. Но какой бы вариант мы ни выбрали, в основе него будет лежать конструкция:
pandas.DataFrame(data, [columns, index, type, copy])
Параметры, которые мы будем использовать:
- data — (обязательный параметр) это данные, из которых планируется создать DataFrame
- columns — (необязательный параметр) это список с именами столбцов
- index — (необязательный параметр) это список с именами столбцов
Если параметры columns или index не определены, то столбцы и строки будут пронумерованы автоматически целыми числовыми значениями, начиная с нуля.
Узнать больше информации о параметрах pandas.DataFrame() можно из официальной документации по ссылке: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html
Рассмотрим несколько наиболее практичных, на мой взгляд, вариантов создания DataFrame:
- Создание DataFrame из списка списков
- Создаем DataFrame из списка словарей
- Создаем DataFrame из numpy-массива
- Создание DataFrame из pandas.Series
Создание DataFrame из списка списков
Сначала создадим список из списков, с учетом того, что каждый вложенный список — это будущий ряд DataFrame. После этого создадим DataFrame по имени df , воспользовавшись конструкцией pd.DataFrame().
Для того, чтобы вместо имен строк выводились их индексы, нужно просто исключить определение параметра index:
import pandas as pd # Создать список из списков list_of_lists = [ [3, 4, 'отл.'], [5, 4, 'хор.'], [4, 5, 'отл.'], ] # Создаем DataFrame # Удалите параметр index, если вместо имен строк нужно выводить индексы: df = pd.DataFrame(list_of_lists, columns = ['Матем.', 'Лит-ра', 'Физ-ра'], index = ['Ворчун', 'Тихоня', 'Добряк'])
В результате получим DataFrame «df»:

Создаем DataFrame из списка словарей
Сначала создадим список словарей с учетом того, что каждый словарь представляет собой будущую строку DataFrame. Ключами словарей являются имена столбцов DataFrame, поэтому параметр columns в pd.DataFrame() определять не нужно. Параметр index определяется, если нужно получить DataFrame с именованными строками:
import pandas as pd # Создаем список словарей list_of_dict = [ , , ] # Удалите параметр index, если вместо имен строк нужно выводить индексы: df = pd.DataFrame(list_of_dict, index = ['Ворчун', 'Тихоня', 'Добряк'])
Полученный DataFrame df:

Как создать DataFrame из numpy-массива?
Сначала создадим numpy-массив arr так, чтобы строки массива включали в себя данные будущих столбцов DataFrame. После этого сформируем DataFrame из транспонированного массива arr.T, при необходимости зададим имена строк и столбцов в параметрах columns и index:
import pandas as pd import numpy as np arr = np.array([ [3, 5, 4], [4, 4, 5], ['отл.', 'хор.', 'отл.'] ]) # Удалите параметр index, если не нужны имена строк: df = pd.DataFrame(arr.T, columns = ['Матем.', 'Лит-ра', 'Физ-ра'], index = ['Ворчун', 'Тихоня', 'Добряк'])
Полученный DataFrame df:

Создание DataFrame из pandas.Series
Сначала создадим одномерные структуры данных — pandas-серии, содержащие значения из будущих столбцов DataFrame. После этого из pandas-серии сформируем DataFrame df, передав при необходимости имена строк и столбцов в параметрах columns и index:
import pandas as pd math = pd.Series([3, 5, 4]) litra = pd.Series([4, 4, 5]) fizra = pd.Series(['отл', 'хор', 'отл']) df = pd.DataFrame((math, litra, fizra), columns = ['Матем.', 'Лит-ра', 'Физ-ра'], index = ['Ворчун', 'Тихоня', 'Добряк' ])
В результате выполнения кода получим DataFrame df:

Используете для работы Jupiter Notebook? Тогда Вам точно будет интресно, как сменить тему в этом редакторе: «Как сменить тему в Jupyter Notebook?».
Вебторт рекомендует!