Чем data frame отличается от двумерной матрицы
Перейти к содержимому

Чем data frame отличается от двумерной матрицы

  • автор:

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) 

2 Введение в R

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

  • биоинформатика
  • вычислительная криминалистика
  • цифровые гуманитарные исследования
  • датажурналистика

Все больше книг “Data Science for …”:

  • psychologists (Hansjörg 2019)
  • immunologists (Thomas and Pallett 2019)
  • business (Provost and Fawcett 2013)
  • public policy (Brooks and Cooper 2013)
  • fraud detection (Baesens, Van Vlasselaer, and Verbeke 2015)

Среди умений датасаентистов можно перечислить следующие:

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

Большинство этих тем в той или иной мере будет представлено в нашем курсе.

2.2 Установка R и RStudio

В данной книге используется исключительно R (R Core Team 2019) , так что для занятий понадобятся:

  • R
    • на Windows
    • на Mac
    • на Linux, также можно добавить зеркало и установить из командной строки:
    sudo apt-get install r-cran-base
    • RStudio — IDE для R (можно скачать здесь)
    • и некоторые пакеты на R

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

    ## [1] "R version 4.0.2 (2020-06-22)"

    Некоторые люди не любят устанавливать лишние программы себе на компьютер, несколько вариантов есть и для них:

    • RStudio cloud — полная функциональность RStudio, пока бесплатная, но скоро это исправят;
    • RStudio on rollApp — облачная среда, позволяющая разворачивать программы.

    Первый и вполне закономерный вопрос: зачем мы ставили R и отдельно еще какой-то RStudio? Если опустить незначительные детали, то R — это сам язык программирования, а RStudio — это среда (IDE), которая позволяет в этом языке очень удобно работать.

    2.3 Полезные ссылки

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

    • книга (Wickham and Grolemund 2016) является достаточно сильной альтернативой всему курсу
    • stackoverflow — сервис, где достаточно быстро отвечают на любые вопросы (не обязательно по R)
    • RStudio community — быстро отвечают на вопросы, связанные с R
    • русский stackoverflow
    • R-bloggers — сайт, где собираются новинки, связанные с R
    • чат, где можно спрашивать про R на русском (но почитайте правила чата, перед тем как спрашивать)
    • чат по визуализации данных, чат датажурналистов
    • канал про визуализацию, дата-блог “Новой газеты”, …

    2.4 Rstudio

    Когда вы откроете RStudio первый раз, вы увидите три панели: консоль, окружение и историю, а также панель для всего остального. Если ткнуть в консоли на значок уменьшения, то можно открыть дополнительную панель, где можно писать скрипт.

    Существуют разные типы пользователей: одни любят работать в консоли (на картинке это 2 — R Console), другие предпочитают скрипты (1 — Code Editor). Консоль позволяет использовать интерактивный режим команда-ответ, а скрипт является по сути текстовым документом, фрагменты которого можно для отладки запускать в консоли.

    3 — Workspace and History: Здесь можно увидеть переменные. Это поле будет автоматически обновляться по мере того, как Вы будете запускать строчки кода и создавать новые переменные. Еще там есть вкладка с историей последних команд, которые были запущены.

    4 — Plots and files: Здесь есть очень много всего. Во-первых, небольшой файловый менеджер, во-вторых, там будут появляться графики, когда вы будете их рисовать. Там же есть вкладка с вашими пакетами (Packages) и Help по функциям. Но об этом потом.

    2.5 Введение в R

    2.5.1 R как калькулятор

    Ой-ей, консоль, скрипт че-то все непонятно.
    Давайте начнем с самого простого и попробуем использовать R как простой калькулятор. + , - , * , / , ^ (степень), () и т.д.

    Просто запускайте в консоли пока не надоест:

    ## [1] 42
    ## [1] 30
    ## [1] 11

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

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

    2.5.2 Функции

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

     16^0.5

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

    Вот, например, функция для корня:

     sqrt(16)

    R — case-sensitive язык, т.е. регистр важен. SQRT(16) не будет работать.

    А вот так выглядит функция логарифма:

     log(8)
    ## [1] 2.079442

    Так, вроде бы все нормально, но… Если Вы еще что-то помните из школьной математики, то должны понимать, что что-то здесь не так.

    Здесь не хватает основания логарифма!

    Логарифм — показатель степени, в которую надо возвести число, называемое основанием, чтобы получить данное число.

    То есть у логарифма 8 по основанию 2 будет значение 3:

    То есть если возвести 2 в степень 3 у нас будет 8:

    Только наша функция считает все как-то не так.

    Чтобы понять, что происходит, нам нужно залезть в хэлп этой функции:

    Справа внизу в RStudio появится вот такое окно:

    Действительно, у этой функции есть еще аргумент base = . По дефолту он равен числу Эйлера (2.7182818…), т.е. функция считает натуральный логарифм. В большинстве функций R есть какой-то основной инпут — данные в том или ином формате, а есть и дополнительные параметры, которые можно прописывать вручную, если параметры по умолчанию нас не устраивают.

     log(x = 8, base = 2)

    …или просто (если Вы уверены в порядке аргументов):

     log(8,2)

    Более того, Вы можете использовать оутпут одних функций как инпут для других:

     log(8, sqrt(4))

    Если эксплицитно писать имена аргументов, то их порядок в функции не важен:

     log(base = 2, x = 8)

    А еще можно недописывать имена аргументов, если они не совпадают с другими:

     log(b = 2, x = 8)

    Мы еще много раз будем возвращаться к функциям. Вообще, функции — это одна из важнейших штук в R (примерно так же как и в Python). Мы будем создавать свои функции, использовать функции как инпут для функций и многое-многое другое. В R очень крутые возможности работы с функциями. Поэтому подружитесь с функциями, они клевые.

    Арифметические знаки, которые мы использовали: +,-,/,^ и т.д. называются операторами и на самом деле тоже являются функциями:

    2.5.3 Переменные

    Важная штука в программировании на практически любом языке — возможность сохранять значения в переменных. В R это обычно делается с помощью вот этих символов: (но можно использовать и обычное =, хотя это не очень принято). Для этого есть удобное сочетание клавиш: нажмите одновременно Alt - (или option - на Маке).

     a 2 a

    После присвоения переменная появляется во вкладке Environment в RStudio:

    Можно использовать переменные в функциях и просто вычислениях:

     b a^a+a*a b
     log(b,a)

    Вы можете сравнивать разные переменные:

     a == b
    ## [1] FALSE

    Заметьте, что сравнивая две переменные мы используем два знака равно ==, а не один =. Иначе это будет означать присвоение.

     a = b a

    Теперь Вы сможете понять комикс про восстание роботов на следующей странице (пусть он и совсем про другой язык программирования)

    Этот комикс объясняет, как важно не путать присваивание и сравнение (хотя я иногда путаю до сих пор =( ).

    Иногда нам нужно проверить на неравенство:

     a 2 b 3 a==b
    ## [1] FALSE
    ## [1] TRUE

    Восклицательный язык в программировании вообще и в R в частности стандартно означает отрицание.

    Еще мы можем сравнивать на больше/меньше:

    ## [1] FALSE
    ## [1] TRUE
    ## [1] FALSE
    ## [1] TRUE

    2.6 Типы данных

    До этого момента мы работали только с числами (numeric):

     class(a)
    ## [1] "numeric"

    Теперь же нам нужно ознакомиться с двумя другими важными типами данных в R:

    1. character: строки символов. Они должны выделяться кавычками. Можно использовать как " , так и ' (что удобно, когда строчка внутри уже содержит какие-то кавычки).
     s "Всем привет!" s
    ## [1] "Всем привет!"
     class(s)
    ## [1] "character"
    1. logical: просто TRUE или FALSE .
     t1 TRUE f1 FALSE t1
    ## [1] TRUE
    ## [1] FALSE

    Вообще, можно еще писать T и F (но не True и False !)

     t2 T f2 F

    Это дурная практика, так как R защищает от перезаписи переменные TRUE и FALSE , но не защищает от этого T и F

     TRUE FALSE
    ## Error in TRUE 
     TRUE
    ## [1] TRUE
     T FALSE T
    ## [1] FALSE

    Теперь вы можете догадаться, что результаты сравнения, например, числовых или строковых переменных вы можете сохранять в переменные тоже!

     comparison a == b comparison
    ## [1] FALSE

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

    Чтобы этим хорошо уметь пользоваться, нам нужно еще освоить как работать с логическими операторами. Про один мы немного уже говорили — это не ( ! ):

    ## [1] TRUE
    ## [1] FALSE
     !!t1 #Двойное отрицание!
    ## [1] TRUE

    Еще есть И (выдаст TRUE только в том случае если обе переменные TRUE ):

     t1&t2
    ## [1] TRUE
     t1&f1
    ## [1] FALSE

    А еще ИЛИ (выдаст TRUE в случае если хотя бы одна из переменных TRUE ):

     t1 | f1
    ## [1] TRUE
     f1 | f2
    ## [1] FALSE

    Если кому-то вдруг понадобиться другое ИЛИ — есть функция xor() , принимающий два аргумента.

    Поздравляю, мы только что разобрались с самой занудной частью. Пора переходить к важному и интересному. ВЕКТОРАМ!

    2.7 Вектор

    Если у вас не было линейной алгебры (или у вас с ней было все плохо), то просто запомните, что вектор (или atomic vector или atomic) — это набор (столбик) чисел в определенном порядке.

    P.S. Если вы привыкли из школьного курса физики считать вектора стрелочками, то не спешите возмущаться и паниковать. Представьте стрелочки как точки из нуля координат до какой-то точки на координатной плоскости, например, . Вот последние два числа и будем считать вектором. Поэтому постарайтесь на время выбросить стрелочки из головы.

    На самом деле, мы уже работали с векторами в R, но, возможно, Вы об этом даже не догадывались. Дело в том, что в R нет как таковых “значений”, есть вектора длиной 1. Такие дела!

    Чтобы создать вектор из нескольких значений, нужно воспользоваться функцией c() :

     c(4,8,15,16,23,42)
    ## [1] 4 8 15 16 23 42
     c("Хэй", "Хэй", "Ха")
    ## [1] "Хэй" "Хэй" "Ха"

    Одна из самых мерзких и раздражающих причин ошибок в коде — это использование с из кириллицы вместо c из латиницы. Видите разницу? И я не вижу. А R видит. И об этом сообщает:

     с(3, 4, 5)
    ## Error in с(3, 4, 5): could not find function "с"

    Для создания числовых векторов есть удобный оператор :

    ## [1] 1 2 3 4 5 6 7 8 9 10
    ## [1] 5 4 3 2 1 0 -1 -2 -3

    Этот оператор создает вектор от первого числа до второго с шагом 1. Вы не представляете, как часто эта штука нам пригодится… Если же нужно сделать вектор с другим шагом, то есть функция seq() :

     seq(10,100, by = 10)
    ## [1] 10 20 30 40 50 60 70 80 90 100

    Кроме того, можно задавать не шаг, а длину вектора. Тогда шаг функция seq() посчитает сама:

     seq(1,13, length.out = 4)
    ## [1] 1 5 9 13

    Другая функция — rep() — позволяет создавать вектора с повторяющимися значениями. Первый аргумент — значение, которое нужно повторять, а второй аргумент — сколько раз повторять.

     rep(1, 5)
    ## [1] 1 1 1 1 1

    И первый, и второй аргумент могут быть векторами!

     rep(1:3, 3)
    ## [1] 1 2 3 1 2 3 1 2 3
     rep(1:3, 1:3)
    ## [1] 1 2 2 3 3 3

    Еще можно объединять вектора (что мы, по сути, и делали, просто с векторами длиной 1):

     v1 c("Hey", "Ho") v2 c("Let's", "Go!") c(v1,v2)
    ## [1] "Hey" "Ho" "Let's" "Go!"

    2.7.1 Coercion

    Что будет, если вы объедините два вектора с значениями разных типов? Ошибка? Мы уже обсуждали, что в atomic может быть только один тип данных. В некоторых языках программирования при операции с данными разных типов мы бы получили ошибку. А вот в R при несовпадении типов пройзойдет попытка привести типы к “общему знаменателю”, то есть конвертировать данные в более “широкий” тип.

     c(FALSE, 2)
    ## [1] 0 2

    FALSE превратился в 0 (а TRUE превратился бы в 1 ), чтобы можно было оба значения объединить в вектор. То же самое произошло бы в случае операций с векторами:

     2 + TRUE

    Это называется coercion. Более сложный пример:

     c(TRUE, 3, "Привет")
    ## [1] "TRUE" "3" "Привет"

    Если Вы боитесь полагаться на coercion, то можете воспользоваться функциями as.нужныйтипданных :

     as.numeric(c(TRUE, FALSE, FALSE))
    ## [1] 1 0 0
     as.character(as.numeric(c(TRUE, FALSE, FALSE)))
    ## [1] "1" "0" "0"

    Можно превращать и обратно, например, строковые значения в числовые. Если среди числа встретится буква или другой неподходящий знак, то мы получим предупреждение NA — пропущенное значение (мы очень скоро научимся с ними работать).

     as.numeric(c("1", "2", "три"))
    ## Warning: NAs introduced by coercion
    ## [1] 1 2 NA

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

    Все те арифметические операции, что мы использовали ранее, можно использовать с векторами одинаковой длины:

     n 1:4 m 4:1 n + m
    ## [1] 5 5 5 5
     n - m
    ## [1] -3 -1 1 3
     n * m
    ## [1] 4 6 6 4
     n / m
    ## [1] 0.2500000 0.6666667 1.5000000 4.0000000
     n ^ m + m * (n - m)
    ## [1] -11 5 11 7

    Если после какого-нибудь MATLAB Вы привыкли, что по умолчанию операторы работают по правилам линейной алгебры и m*n будет давать скалярное произведение (dot product), то снова нет. Для скалярного произведения нужно использовать операторы с % по краям:

     n %*% m
    ## [,1] ## [1,] 20

    Абсолютно так же и с операциями с матрицами в R, хотя про матрицы будет немного позже.

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

     sqrt(1:10)
    ## [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427 ## [9] 3.000000 3.162278

    2.7.3 Recycling

    Допустим мы хотим совершить какую-нибудь операцию с двумя векторами. Как мы убедились, с этим обычно нет никаких проблем, если они совпадают по длине. А что если вектора не совпадают по длине? Ничего страшного! Здесь будет работать правило ресайклинга (recycling = правило переписывания). Это означает, что если короткий вектор кратен по длине длинному, то он будет повторять короткий необходимое количество раз:

     n 1:4 m 1:2 n * m
    ## [1] 1 4 3 8

    А что будет, если совершать операции с вектором и отдельным значением? Можно считать это частным случаем ресайклинга: короткий вектор длиной 1 будет повторятся столько раз, сколько нужно, чтобы он совпадал по длине с длинным:

     n * 2
    ## [1] 2 4 6 8

    Если же меньший вектор не кратен большему (например, один из них длиной 3, а другой длиной 4), то R посчитает результат, но выдаст предупреждение.

     n + c(3,4,5)
    ## Warning in n + c(3, 4, 5): longer object length is not a multiple of shorter ## object length
    ## [1] 4 6 8 7

    Проблема в том, что эти предупреждения могут в неожиданный момент стать причиной ошибок. Поэтому не стоит полагаться на ресайклинг некратных по длине векторов. См. здесь. А вот ресайклинг кратных по длине векторов — это очень удобная штука, которая используется очень часто.

    2.7.4 Индексирование векторов

    Итак, мы подошли к одному из самых сложных моментов. И одному из основных. От того, как хорошо вы научись с этим работать, зависит весь Ваш дальнейший успех на R-поприще!

    Речь пойдет об индексировании векторов. Задача, которую Вам придется решать каждые пять минут работы в R - как выбрать из вектора (или же списка, матрицы и датафрейма) какую-то его часть. Для этого используются квадратные скобочки [] (не круглые - они для функций!).

    Самое простое - индексировать по номеру индекса, т.е. порядку значения в векторе.

     n 1:10 n[1]
    ## [1] 10

    Если вы знакомы с другими языками программирования (не MATLAB, там все так же) и уже научились думать, что индексация с 0 — это очень удобно и очень правильно (ну или просто свыклись с этим), то в R Вам придется переучиться обратно. Здесь первый индекс — это 1, а последний равен длине вектора — ее можно узнать с помощью функции length() . С обоих сторон индексы берутся включительно.

    С помощью индексирования можно не только вытаскивать имеющиеся значения в векторе, но и присваивать им новые:

     n[3] 20 n
    ## [1] 1 2 20 4 5 6 7 8 9 10

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

    ## [1] 4 5 6 7
     n[10:1]
    ## [1] 10 9 8 7 6 5 4 20 2 1

    Индексирование с минусом выдаст вам все значения вектора кроме выбранных (простите, пользователя Python, которые ожидают здесь отсчет с конца…):

    ## [1] 2 20 4 5 6 7 8 9 10
     n[c(-4, -5)]
    ## [1] 1 2 20 6 7 8 9 10

    Более того, можно использовать логический вектор для индексирования. В этом случае нужен логический вектор такой же длины:

     n[c(TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE,TRUE,FALSE)]
    ## [1] 1 20 5 7 9

    Ну а если они не равны, то тут будет снова работать правило ресайклинга!

     n[c(TRUE,FALSE)] #то же самое - recycling rule!
    ## [1] 1 20 5 7 9

    Есть еще один способ индексирования векторов, но он несколько более редкий: индексирование по имени. Дело в том, что для значений векторов можно (но не обязательно) присваивать имена:

     my_named_vector c(first = 1, second = 2, third = 3) my_named_vector['first']
    ## first ## 1

    А еще можно “вытаскивать” имена из вектора с помощью функции names() и присваивать таким образом новые.

     d 1:4 names(d) letters[1:4] d["a"]
    ## a ## 1

    letters - это “зашитая” в R константа - вектор букв от a до z. Иногда это очень удобно! Кроме того, есть константа LETTERS - то же самое, но заглавными буквами. А еще есть названия месяцев на английском и числовая константа pi .

    Теперь посчитаем среднее вектора n :

     mean(n)
    ## [1] 7.2

    А как вытащить все значения, которые больше среднего?

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

     larger n > mean(n) larger
    ## [1] FALSE FALSE TRUE FALSE FALSE FALSE FALSE TRUE TRUE TRUE

    А теперь используем его для индексирования вектора n :

     n[larger]
    ## [1] 20 8 9 10

    Можно все это сделать в одну строчку:

     n[n>mean(n)]
    ## [1] 20 8 9 10

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

    2.7.5 NA — пропущенные значения

    В реальных данных у нас часто чего-то не хватает. Например, из-за технической ошибки или невнимательности не получилось записать какое-то измерение. Для этого в R есть NA . NA — это не строка "NA" , не 0 , не пустая строка "" и не FALSE . NA — это NA . Большинство операций с векторами, содержащими NA будут выдавать NA :

     missed NA missed == "NA"
    ## [1] NA
     missed == ""
    ## [1] NA
     missed == NA
    ## [1] NA

    Заметьте: даже сравнение NA c NA выдает NA !

    Иногда NA в данных очень бесит:

     n[5] NA n
    ## [1] 1 2 20 4 NA 6 7 8 9 10
     mean(n)
    ## [1] NA

    Что же делать?
    Наверное, надо сравнить вектор с NA и исключить этих пакостников. Давайте попробуем:

     n == NA
    ## [1] NA NA NA NA NA NA NA NA NA NA

    Ах да, мы ведь только что узнали, что даже сравнение NA c NA приводит к NA .

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

     is.na(n)
    ## [1] FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE

    Результат выполнения is.na(n) выдает FALSE в тех местах, где у нас числа и TRUE там, где у нас NA . Нам нужно сделать наоборот. Здесь нам понадобится оператор ! (мы его уже встречали), который инвертирует логические значения:

     n[!is.na(n)]
    ## [1] 1 2 20 4 6 7 8 9 10

    Ура, мы можем считать среднее!

     mean(n[!is.na(n)])
    ## [1] 7.444444

    Теперь Вы понимаете, зачем нужно отрицание ( ! )

    Вообще, есть еще один из способов посчитать среднее, если есть NA . Для этого надо залезть в хэлп по функции mean():

     ?mean()

    В хэлпе мы найдем параметр na.rm = , который по дефолту FALSE . Вы знаете, что нужно делать!

     mean(n, na.rm = TRUE)
    ## [1] 7.444444

    NA может появляться в векторах других типов тоже. Кроме NA есть еще NaN — это разные вещи. NaN расшифровывается как Not a Number и получается в результате таких операций как 0/0 .

    2.7.6 В любой непонятной ситуации — ищите в поисковике

    Если вдруг вы не знаете, что искать в хэлпе, или хэлпа попросту недостаточно, то… ищите в поисковике!

    Нет ничего постыдного в том, чтобы искать в Интернете решения проблем. Это абсолютно нормально. Используйте силу интернета во благо и да помогут Вам Stackoverflow и бесчисленные R-туториалы!

    Главное, помните: загуглить работающий ответ всегда недостаточно. Надо понять, как и почему он работает. Иначе что-то обязательно пойдет не так.

    Кроме того, правильно загуглить проблему — не так уж и просто.

    Итак, с векторами мы более-менее разобрались. Помните, что вектора — это один из краеугольных камней Вашей работы в R. Если Вы хорошо с ними разобрались, то дальше все будет довольно несложно. Тем не менее, вектора — это не все. Есть еще два важных типа данных: списки (list) и матрицы (matrix). Их можно рассматривать как своеобразное “расширение” векторов, каждый в свою сторону. Ну а списки и матрицы нужны чтобы понять основной тип данных в R — data.frame.

    2.8 Матрицы (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

    Если мы знаем сколько значений в матрице и сколько мы хотим строк, то количество столбцов указывать необязательно:

     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

    Все остальное так же как и с векторами: внутри находится данные только одного типа. Поскольку матрица — это уже двумерный массив, то у него имеется два индекса. Эти два индекса разделяются запятыми.

    ## [1] 12
     A[2:4, 1:3]
    ## [,1] [,2] [,3] ## [1,] 2 7 12 ## [2,] 3 8 13 ## [3,] 4 9 14

    Первый индекс — выбор строк, второй индекс — выбор колонок. Если же мы оставляем пустое поле вместо числа, то мы выбираем все строки/колонки в зависимости от того, оставили мы поле пустым до или после запятой:

    ## [,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

    В принципе, это все, что нам нужно знать о матрицах. Матрицы используются в R довольно редко, особенно по сравнению, например, с MATLAB. Но вот индексировать матрицы хорошо бы уметь: это понадобится в работе с датафреймами.

    То, что матрица - это просто двумерный вектор, не является метафорой: в R матрица - это по сути своей вектор с дополнительными атрибутами dim и dimnames . Атрибуты — это неотъемлемые свойства объектов, для всех объектов есть обязательные атрибуты типа и длины и могут быть любые необязательные атрибуты. Можно задавать свои атрибуты или удалять уже присвоенные: удаление атрибута dim у матрицы превратит ее в обычный вектор. Про атрибуты подробнее можно почитать здесь или на стр. 99–101 книги “R in a Nutshell” (Adler 2010) .

    2.9 Списки (list)

    Теперь представим себе вектор без ограничения на одинаковые данные внутри. И получим список!

     l list(42, "Пам пам", TRUE) l
    ## [[1]] ## [1] 42 ## ## [[2]] ## [1] "Пам пам" ## ## [[3]] ## [1] TRUE

    А это значит, что там могут содержаться самые разные данные, в том числе и другие списки и векторы!

     lbig list(c("Wow", "this", "list", "is", "so", "big"), "16", l) lbig
    ## [[1]] ## [1] "Wow" "this" "list" "is" "so" "big" ## ## [[2]] ## [1] "16" ## ## [[3]] ## [[3]][[1]] ## [1] 42 ## ## [[3]][[2]] ## [1] "Пам пам" ## ## [[3]][[3]] ## [1] TRUE

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

     str(lbig)
    ## List of 3 ## $ : chr [1:6] "Wow" "this" "list" "is" . ## $ : chr "16" ## $ :List of 3 ## ..$ : num 42 ## ..$ : chr "Пам пам" ## ..$ : logi TRUE

    Как и в случае с векторами мы можем давать имена элементам списка:

     namedl list(age = 24, PhDstudent = T, language = "Russian") namedl
    ## $age ## [1] 24 ## ## $PhDstudent ## [1] FALSE ## ## $language ## [1] "Russian"

    К списку можно обращаться как с помощью индексов, так и по именам. Начнем с последнего:

     namedl$age
    ## [1] 24

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

     namedl[1]
    ## $age ## [1] 24

    Мы, по сути, получили элемент списка - просто как часть списка, т.е. как список длиной один:

     class(namedl)
    ## [1] "list"
     class(namedl[1])
    ## [1] "list"

    А вот чтобы добраться до самого элемента списка (и сделать с ним что-то хорошее) нам нужна не одна, а две квадратных скобочки:

     namedl[[1]]
    ## [1] 24
     class(namedl[[1]])
    ## [1] "numeric"

    Как и в случае с вектором, к элементу списка можно обращаться по имени.

     namedl[['age']]
    ## [1] 24

    Хотя последнее — практически то же самое, что и использование знака $.

    Списки довольно часто используются в R, но реже, чем в Python. Со многими объектами в R, такими как результаты статистических тестов, объекты ggplot и т.д. удобно работать именно как со списками — к ним все вышеописанное применимо. Кроме того, некоторые данные мы изначально получаем в виде древообразной структуры — хочешь не хочешь, а придется работать с этим как со списком. Но обычно после этого стоит как можно скорее превратить список в датафрейм.

    2.10 Data.frame

    Итак, мы перешли к самому главному. Самому-самому. Датафреймы (data.frames). Более того, сейчас станет понятно, зачем нам нужно было разбираться со всеми предыдущими темами.

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

     name c("Ivan", "Eugeny", "Lena", "Misha", "Sasha") age c(26, 34, 23, 27, 26) student c(FALSE, FALSE, TRUE, TRUE, TRUE) df = data.frame(name, age, student) df
     str(df)
    ## 'data.frame': 5 obs. of 3 variables: ## $ name : chr "Ivan" "Eugeny" "Lena" "Misha" . ## $ age : num 26 34 23 27 26 ## $ student: logi FALSE FALSE TRUE TRUE TRUE

    Вообще, очень похоже на список, не правда ли? Так и есть, датафрейм — это что-то вроде проименованного списка, каждый элемент которого является atomic вектором фиксированной длины. Скорее всего, список Вы представляли “горизонтально”. Если это так, то теперь “переверните” его у себя в голове. Так, чтоб названия векторов оказались сверху, а колонки стали столбцами. Поскольку длина всех этих векторов равна (обязательное условие!), то данные представляют собой табличку, похожую на матрицу. Но в отличие от матрицы, разные столбцы могут имет разные типы данных: первая колонка — character, вторая колонка — numeric, третья колонка — logical. Тем не менее, обращаться с датафреймом можно и как с проименованным списком, и как с матрицей:

     df$age[2:3]
    ## [1] 34 23

    Здесь мы сначала вытащили колонку age с помощью оператора $ . Результатом этой операции является числовой вектор, из которого мы вытащили кусок, выбрав индексы 2 и 3 .

    Используя оператор $ и присваивание можно создавать новые колонки датафрейма:

     df$lovesR TRUE #правило recycling - узнали?  df

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

     df[3:5, 2:3]

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

    А еще можно использовать названия колонок внутри квадратных скобок:

     df[1:2,"age"]
    ## [1] 26 34

    И здесь перед нами открываются невообразимые возможности! Узнаем, любят ли R те, кто моложе среднего возраста в группе:

     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

    В большинстве случаев подходят сразу несколько способов — тем не менее, стоит овладеть ими всеми.

    Датафреймы удобно просматривать в RStudio. Для это нужно написать команду View(df) или же просто нажать на названии нужной переменной из списка вверху справа (там где Environment). Тогда увидите табличку, очень похожую на Excel и тому подобные программы для работы с таблицами. Там же есть и всякие возможности для фильтрации, сортировки и поиска… Но, конечно, интереснее все эти вещи делать руками, т.е. с помощью написания кода.

    На этом пора заканчивать с введением и приступать к реальным данным.

    2.11 Начинаем работу с реальными данными

    Итак, пришло время перейти к реальным данным. Мы начнем с использования датасета (так мы будем называть любой набор данных) по Игре Престолов, а точнее, по книгам цикла “Песнь льда и пламени” Дж. Мартина. Да, будут спойлеры, но сериал уже давно закончился и сильно разошелся с книгами…

    2.11.1 Рабочая папка и проекты

    Для начала скачайте файл по ссылке

    Он, скорее всего, появился у Вас в папке “Загрузки”. Если мы будем просто пытаться прочитать этот файл (например, с помощью read.csv() — мы к этой функцией очень скоро перейдем), указав его имя и разрешение, то наткнемся на такую ошибку:

    Ошибка в file(file, “rt”) :не могу открыть соединение Вдобавок: Предупреждение: В file(file, “rt”) : не могу открыть файл ‘character-deaths.csv’: No such file or directory

    Это означает, что R не может найти нужный файл. Вообще-то мы даже не сказали, где искать. Нам нужно как-то совместить место, где R ищет загружаемые файлы и сами файлы. Для этого есть несколько способов.

    • Магомет идет к горе: перемещение файлов в рабочую папку.

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

     got read.csv("character-deaths.csv")
    • Гора идет к Магомету: изменение рабочей папки.

    Можно просто сменить рабочую папку с помощью setwd() на ту, где сейчас лежит файл, прописав путь до этой папки. Теперь файл находится в рабочей папке:

     got read.csv("character-deaths.csv")

    Этот вариант использовать не рекомендуется. Как минимум, это сразу делает невозможным запустить скрипт на другом компьютере.

    • Гора находит Магомета по месту прописки: указание полного пути файла.
     got read.csv("/Users/Username/Some_Folder/character-deaths.csv")

    Этот вариант страдает теми же проблемами, что и предыдущий, поэтому тоже не рекомендуется.

    Для пользователей Windows есть дополнительная сложность: знак / является особым знаком для R, поэтому вместо него нужно использовать двойной // .

    • Магомет использует кнопочный интерфейс: Import Dataset.

    Во вкладке Environment справа в окне RStudio есть кнопка “Import Dataset”. Возможно, у Вас возникло непреодолимое желание отдохнуть от написания кода и понажимать кнопочки — сопротивляйтесь этому всеми силами, но не вините себя, если не сдержитесь.

    • Гора находит Магомета в интернете.

    Многие функции в R, предназначенные для чтения файлов, могут прочитать файл не только на Вашем компьютере, но и сразу из интернета. Для этого просто используйте ссылку вместо пути:

     got read.csv("https://raw.githubusercontent.com/Pozdniakov/stats/master/data/character-deaths.csv")
    • Каждый Магомет получает по своей горе: использование проектов в RStudio.

    На первый взгляд это кажется чем-то очень сложным, но это не так. Это очень просто и ОЧЕНЬ удобно. При создании проекта создается отдельная папочка, где у Вас лежат данные, хранятся скрипты, вспомогательные файлы и отчеты. Если нужно вернуться к другому проекту — просто открываете другой проект, с другими файлами и скриптами. Это еще помогает не пересекаться переменным из разных проектов — а то, знаете, использование двух переменных data в разных скриптах чревато ошибками. Поэтому очень удобным решением будет выделение отдельного проекта под этот курс.

    2.11.2 Импорт данных

    Как Вы уже поняли, импортирование данных - одна из самых муторных и неприятных вещей в R. Если у Вас получится с этим справится, то все остальное - ерунда. Мы уже разобрались с первой частью этого процесса - нахождением файла с данными, осталось научиться их читать.

    Здесь стоит сделать небольшую ремарку. Довольно часто данные представляют собой табличку. Или же их можно свести к табличке. Такая табличка, как мы уже выяснили, удобно репрезентируется в виде датафрейма. Но как эти данные хранятся на компьютере? Есть два варианта: в бинарном и в текстовом файле.

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

    Для чтения данных из текстового файла есть довольно удобная функция read.table() . Почитайте хэлп по ней и ужаснитесь: столько разных параметров на входе! Но там же вы увидете функции read.csv() , read.csv2() и некоторые другие — по сути, это тот же read.table() , но с другими дефолтными параметрами, соответствующие формату файла, который мы загружаем. В данном случае используется формат .csv, что означает Comma Separated Values (Значения, Разделенные Запятыми). Это просто текстовый файл, в котором “закодирована” таблица: разные строчки разделяют разные строчки таблицы, а столбцы отделяются запятыми. С этим связана одна проблема: в некоторых странах (в т.ч. и России) принято использовать запятую для разделения дробной части числа, а не точку, как это делается в большинстве стран мира. Поэтому есть “другой” формат .csv, где значения разделены точкой с запятой ( ; ), а дробные значения - запятой ( , ). В этом и различие функций read.csv() и read.csv2() — первая функция предназначена для “международного” формата, вторая - для (условно) “Российского”.

    В первой строчке обычно содержатся названия столбцов - и это чертовски удобно, функции read.csv() и read.csv2() по дефолту считают первую строчку именно как название для колонок.

    Итак, прочитаем наш файл. Для этого используем только параметр file = , который идет первым, и для параметра stringsAsFactors = поставим значение FALSE :

     got read.csv("data/character-deaths.csv", stringsAsFactors = FALSE)

    По сути, факторы - это примерно то же самое, что и character, но закодированные числами. Когда-то это было придумано для экономии используемых времени и памяти, сейчас же обычно становится просто лишней морокой. Но некоторые функции требуют именно character, некоторые factor, в большинстве случаев это без разницы. Но иногда непонимание может привести к дурацким ошибкам. В данном случае мы просто пока обойдемся без факторов.

    Можете проверить с помощью View(got) : все работает! Если же вылезает какая-то странная ерунда или же просто ошибка - попробуйте другие функции и покопаться с параметрами. Для этого читайте Help.

    Кроме .csv формата есть и другие варианты хранения таблиц в виде текста. Например, .tsv - тоже самое, что и .csv, но разделитель - знак табуляции. Для чтения таких файлов есть функция read.delim() и read.delim2() . Впрочем, даже если бы ее и не было, можно было бы просто подобрать нужные параметры для функции read.table() . Есть даже функции, которые пытаются сами “угадать” нужные параметры для чтения — часто они справляются с этим довольно удачно. Но не всегда. Поэтому стоит научиться справляться с любого рода данными на входе.

    Тем не менее, далеко не всегда таблицы представлены в виде текстового файла. Самый распространенный пример таблицы в бинарном виде — родные форматы Microsoft Excel. Если Вы попробуете открыть .xlsx файл в Блокноте, то увидите кракозябры. Это делает работу с этим файлами гораздо менее удобной, поэтому стоит избегать экселевских форматов и стараться все сохранять в .csv.

    Для работы с экселевскими файлами есть много пакетов: readxl, xlsx, openxlsx. Для чтения файлов SPSS, Stata, SAS есть пакет foreign. Что такое пакеты и как их устанавливать мы изучим позже.

    2.12 Препроцессинг данных в R

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

     got read.csv("data/character-deaths.csv", stringsAsFactors = F)

    После загрузки данных стоит немного “осмотреть” получившийся датафрейм got .

    2.12.1 Исследование данных

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

    Во-первых, вспомним другую полезную функцию str() :

     str(got)
    ## 'data.frame': 917 obs. of 13 variables: ## $ Name : chr "Addam Marbrand" "Aegon Frey (Jinglebell)" "Aegon Targaryen" "Adrack Humble" . ## $ Allegiances : chr "Lannister" "None" "House Targaryen" "House Greyjoy" . ## $ Death.Year : int NA 299 NA 300 NA NA 300 300 NA NA . ## $ Book.of.Death : int NA 3 NA 5 NA NA 4 5 NA NA . ## $ Death.Chapter : int NA 51 NA 20 NA NA 35 NA NA NA . ## $ Book.Intro.Chapter: int 56 49 5 20 NA NA 21 59 11 0 . ## $ Gender : int 1 1 1 1 1 1 1 0 1 1 . ## $ Nobility : int 1 1 1 1 1 1 1 1 1 0 . ## $ GoT : int 1 0 0 0 0 0 1 1 0 0 . ## $ CoK : int 1 0 0 0 0 1 0 1 1 0 . ## $ SoS : int 1 1 0 0 1 1 1 1 0 1 . ## $ FfC : int 1 0 0 0 0 0 1 0 1 0 . ## $ DwD : int 0 0 1 1 0 0 0 1 0 0 . 

    Давайте разберемся с переменными в датафрейме:

    Колонка Name — здесь все понятно. Важно, что эти имена записаны абсолютно по-разному: где-то с фамилией, где-то без, где-то в скобочках есть пояснения. Колонка Allegiances — к какому дому принадлежит персонаж. С этим сложно, иногда они меняют дома, здесь путаются сами семьи и персонажи, лояльные им. Особой разницы между Stark и House Stark нет. Следующие колонки - Death Year , Book.of.Death , Death.Chapter , Book.Intro.Chapter — означают номер главы, в которой персонаж впервые появляется, а так же номер книги, глава и год (от завоевания Вестероса Эйгоном Таргариеном), в которой персонаж умирает. Gender — 1 для мужчин, 0 для женщин. Nobility — дворянское происхождение персонажа. Последние 5 столбцов содержат информацию, появлялся ли персонаж в книге (всего книг пока что 5).

    Другая полезная функция для больших таблиц — функция head() : она выведет первые несколько (по дефолту 6) строчек датафрейма.

     head(got)

    Есть еще функция tail() . Догадайтесь сами, что она делает.

    Для некоторых переменных полезно посмотреть таблицы частотности с помощью функции table():

     table(got$Allegiances)
    ## ## Arryn Baratheon Greyjoy House Arryn House Baratheon ## 23 56 51 7 8 ## House Greyjoy House Lannister House Martell House Stark House Targaryen ## 24 21 12 35 19 ## House Tully House Tyrell Lannister Martell Night's Watch ## 8 11 81 25 116 ## None Stark Targaryen Tully Tyrell ## 253 73 17 22 15 ## Wildling ## 40

    Уау! Очень просто и удобно, не так ли? Функция table() может принимать сразу несколько столбцов. Это удобно для получения таблиц сопряженности:

     table(got$Allegiances, got$Gender)
    ## ## 0 1 ## Arryn 3 20 ## Baratheon 6 50 ## Greyjoy 4 47 ## House Arryn 3 4 ## House Baratheon 0 8 ## House Greyjoy 1 23 ## House Lannister 2 19 ## House Martell 7 5 ## House Stark 6 29 ## House Targaryen 5 14 ## House Tully 0 8 ## House Tyrell 4 7 ## Lannister 12 69 ## Martell 7 18 ## Night's Watch 0 116 ## None 51 202 ## Stark 21 52 ## Targaryen 1 16 ## Tully 2 20 ## Tyrell 6 9 ## Wildling 16 24

    2.12.2 Subsetting

    Как мы обсуждали на прошлом занятии, мы можем сабсеттить (выделять часть датафрейма) датафрейм, обращаясь к нему и как к матрице: датафрейм[вектор_с_номерами_строк, вектор_с_номерами_колонок]

     got[100:115, 1:2]

    и используя имена колонок:

     got[508:515, "Name"]
    ## [1] "Mance Rayder" "Mandon Moore" "Maric Seaworth" "Marei" ## [5] "Margaery Tyrell" "Marillion" "Maris" "Marissa Frey"

    и даже используя вектора названий колонок!

     got[508:515, c("Name", "Allegiances", "Gender")]

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

     houses got$Allegiances unique(houses) #посмотреть все уникальные значения --- почти как с помощью table()
    ## [1] "Lannister" "None" "House Targaryen" "House Greyjoy" ## [5] "Baratheon" "Night's Watch" "Arryn" "House Stark" ## [9] "House Tyrell" "Tyrell" "Stark" "Greyjoy" ## [13] "House Lannister" "Martell" "House Martell" "Wildling" ## [17] "Targaryen" "House Arryn" "House Tully" "Tully" ## [21] "House Baratheon"

    Итак, давайте решим нашу первую задачу — вытащим в отдельный датасет всех представителей Ночного Дозора. Для этого нам нужно создать вектор логических значений — результат сравнений колонки Allegiances со значением "Night's Watch" и использовать его как вектор индексов для датафрейма.

     vectornight got$Allegiances == "Night's Watch" head(vectornight)
    ## [1] FALSE FALSE FALSE FALSE FALSE FALSE

    Теперь этот вектор с TRUE и FALSE нам надо использовать для индексирования строк. Но что со столбцами? Если мы хотем сохранить все столбцы, то после запятой внутри квадратных скобок нам не нужно ничего указывать:

     nightswatch got[vectornight,] head(nightswatch)

    Вуаля! Все это можно сделать проще и в одну строку:

     nightswatch got[got$Allegiances == "Night's Watch",]

    И не забывайте про запятую!

    Теперь попробуем вытащить одновременно всех Одичалых ( Wildling ) и всех представителей Ночного Дозора. Это можно сделать, используя оператор | (ИЛИ) при выборе колонок:

     nightwatch_wildling got[got$Allegiances == "Night's Watch" | got$Allegiances == "Wildling",] head(nightwatch_wildling)

    Кажется очевидным следующий вариант: got[got$Allegiances == c("Night's Watch", "Wildling"),] . Однако это выдаст не совсем то, что нужно, хотя результат может показаться верным на первый взгляд. Попробуйте самостоятельно ответить на вопрос, что происходит в данном случае и чем результат отличается от предполагаемого. Подсказка: вспомните правило recycling.

    Для таких случаев есть удобный оператор %in% , который позволяет сравнить каждое значение вектора с целым набором значений. Если значение вектора хотя бы один раз встречается в векторе справа от %in% , то результат — TRUE :

     1:6 %in% c(1,4,5)
    ## [1] TRUE FALSE FALSE TRUE TRUE FALSE
     nightwatch_wildling got[got$Allegiances %in% c("Night's Watch", "Wildling"),] head(nightwatch_wildling)

    2.12.3 Создание новых колонок

    Давайте создадим новую колонку, которая будет означать, жив ли еще персонаж (по книгам). Заметьте, что в этом датасете, хоть он и посвящен смертям персонажей, нет нужной колонки. Мы можем попытаться “вытащить” эту информацию. В колонках Death.Year , Death.Chapter и Book.of.Death стоит NA у многих персонажей. Например, у Arya Stark , которая и по книгам, и по сериалу живее всех живых и мертвых:

     got[got$Name == "Arya Stark",]

    Следовательно, если в Book.of.Death стоит NA , мы можем предположить, что Джордж Мартин еще не занес своей карающей руки над этим героем.

    Мы можем создать новую колонку Is.Alive :

     got$Is.Alive is.na(got$Book.of.Death)

    2.12.4 data.table vs. tidyverse

    В принципе, с помощью базового R можно сделать все, что угодно. Однако базовые инструменты R — не всегда самые удобные. Идея сделать работу с датафреймами в R еще быстрее и удобнее сподвигла разработчиков на создание новых инструментов — data.table и tidyverse ( dplyr ). Это два конкурирующих подхода, которые сильно перерабатывают язык, хотя это по-прежнему все тот же R — поэтому их еще называют “диалектами” R.

    Оба подхода обладают своими преимуществами и недостатками, но на сегодняшний день tidyverse считается более популярным. Основное преимущество этого подхода — в относительной легкости освоения. Обычно код, написанный в tidyverse можно примерно понять, даже не владея им.

    Преимущество data.table — в суровом лаконичном синтаксисе и наиболее эффективных алгоритмах. Последние обеспечивают очень серьезный прирост в скорости в работе с данными. Чтение файлов и манипуляция данными может быть на порядки быстрее, поэтому если Ваш датасет с трудом пролезает в оперативную память компьютера, а исполнение скрипта занимает длительное время - стоит задуматься о переходе на data.table .

    Что из этого учить — решать Вам, но знать оба совсем не обязательно: они решают те же самые задачи, просто совсем разными способами. За data.table — скорость, за tidyverse - понятность синтаксиса. Очень советую почитать обсуждение на эту тему здесь.

    Ссылки на литературу

    Adler, Joseph. 2010. R in a Nutshell: A Desktop Quick Reference. " O’Reilly Media, Inc.".

    Baesens, Bart, Veronique Van Vlasselaer, and Wouter Verbeke. 2015. Fraud Analytics Using Descriptive, Predictive, and Social Network Techniques: A Guide to Data Science for Fraud Detection. John Wiley & Sons.

    Brooks, Harvey, and Chester L Cooper. 2013. Science for Public Policy. Elsevier.

    Hansjörg, Neth. 2019. Data Science for Psychologists. self published.

    Provost, Foster, and Tom Fawcett. 2013. Data Science for Business: What You Need to Know About Data Mining and Data-Analytic Thinking. O’Reilly Media, Inc.

    R Core Team. 2019. R: A Language and Environment for Statistical Computing. Vienna, Austria: R Foundation for Statistical Computing. https://www.R-project.org/.

    Thomas, Niclas, and Laura Pallett. 2019. Data Science for Immunologists. CreateSpace Independent Publishing Platform.

    Wickham, Hadley, and Garrett Grolemund. 2016. R for Data Science: Import, Tidy, Transform, Visualize, and Model Data. O’Reilly Media, Inc.

    Python, корреляция и регрессия: часть 3

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

    Матрицы

    Матрица, — это двумерный массив чисел. Размерность матрицы выражается числом строк и столбцов.

    Например, A — это матрица с четырьмя строками и двумя столбцами:

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

    Массив numpy можно сконструировать из набора данных при помощи функции pandas df.values :

    def ex_3_16(): '''Конвертирование в массив (матрицу) numpy таблицы данных роста и веса''' df = swimmer_data()[['Рост, см', 'Вес']] return df.values

    В результате выполнения этого примера получим следующий одномерный массив:

    array([[166., 68.], [192., 89.], [173., 65.], . [188., 79.], [187., 78.], [183., 70.]])

    Можно также воспользоваться функцией из библиотеки numpy np.array , которая принимает последовательность скалярных величин либо последовательность последовательностей и, в случае если это возможно, конвертирует их в одномерный массив (в формате numpy.ndarray ):

    def ex_3_17(): '''Конвертирование в массив (матрицу) numpy данных числового ряда с данными о росте''' return swimmer_data()['Рост, см'].head(20).values # первые 20

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

    array([166., 192., 173., 179., 201., 190., 175., 160., 202., 173., 175., 205., 185., 175., 185., 170., 165., 179., 165., 170.])

    Матрицы часто могут вырастать до больших размеров, поэтому, чтобы не переполнять информацией окно интерпретатора, можно ограничить число выводимых элементов, воспользовавшись функциями pandas head и tail либо функционалом индексации библиотеки numpy ( result_array[:5] ); в обоих случаях будет выведено заданное число элементов.

    Библиотека pandas взаимодействует с функциями библиотеки numpy напрямую, получая выгоду от векторизованных операций, таких как log, exp, sqrt и др., на массивах/матрицах. С другой стороны, различные функции numpy могут без проблем использоваться с кадрами данных DataFrame (и рядами Series) библиотеки pandas при условии, что содержащиеся внутри данные являются числовыми. Например, np.exp(df), np.asarray(df), df.T.dot(df)).

    Размерность

    Элемент в i-ой строке и j-ом столбце обозначается как Aij. И поэтому в приведенном выше примере индексация будет такой:

    Одним из фундаментальных атрибутов матрицы является ее размерность. Библиотеки pandas и numpy предоставляют функцию shape , которая в обоих случаях возвращает кортеж с размерностями массива: число строк, столбцов и других размерностей.

    Векторы

    Векторы — это частный случай матрицы, которая содержит всего один столбец. Число строк в векторе называется его размерностью:

    Здесь y — это 4-мерный вектор; его i-й элемент обозначается как yi. Векторы в математической литературе индексируются, начиная с единицы, если не указано иное.

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

    Программный интерфейс библиотеки numpy в отличие от библиотеки pandas (где Series и DataFrame могут служить соответственно для представления векторов и матриц) не делает различие между векторами и одностолбцовыми матрицами, и мы можем создать вектор, передав в функцию np.array единственную последовательность.

    Сборка

    Как мы уже убедились, матрицы можно собирать из последовательностей Python и наборов данных pandas. Кроме того, матрицы можно собирать из более мелких конструктивных составляющих, при условии совпадения размерностей, надстраивая столбцы бок о бок и добавляя строки. В простейшем случае мы можем добавить столбец единиц в начало или конец таблицы и затем преобразовать в матрицу следующим образом:

    '''Добавление столбца в таблицу данных (массив)''' df = pd.DataFrame(<'x':[2, 3, 6, 7],'y':[8, 7, 4, 3]>) df['константа'] = 1 df
     x y константа 0 2 8 1 1 3 7 1 2 6 4 1 3 7 3 1

    На самом деле, нам потребуется это делать для члена смещения. Напомним, что β1 выражает константное значение, поэтому мы должны обеспечить, чтобы соответствующий x1 тоже был константой. Без смещения переменная y равнялось бы нулю, в случае когда значения x равны нулю.

    Сложение и скалярное произведение

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

    Матрично-матричное сложение работает путем сложения элементов в каждой соответствующей позиции. Складывать матрицы между собой можно только с одинаковыми размерностями. Если матрицы имеют одинаковые размерности, то о них говорят, что они совместимые.

    Имплементация на Python с использованием pandas:

    df1 = pd.DataFrame([[1,0],[2,5],[3,1]]) df2 = pd.DataFrame([[4,0.5],[2,5],[0,1]]) df1 + df2
     0 1 0 5 0.5 1 4 10.0 2 3 2.0

    Помимо скаляров и матриц, в pandas можно складывать совместимые матрицы, например, из матрицы можно вычитать скаляры либо совместимые матрицы. Умножение матриц на скаляр в результате дает матрицу, в которой каждый элемент умножен на скаляр.

    Имплементация на Python с использованием pandas:

    df1 * 3
     0 1 0 3 0 1 6 15 2 9 3

    Матрично-векторное умножение

    В функции dot с применением сложного алгоритма матричного умножения имплементирован стандартный способ умножения матриц. Например, результатом умножения матрицы размера 3 × 2 на матрицу размера 2 × 1 является матрица размера 3 × 1, при этом число столбцов слева должно совпадать с числом строк справа:

    Для получения Ax надо помножить каждую строку A поэлементно с соответствующим элементом матрицы x и сложить результаты. Например, первая строка матрицы A содержит элементы 1 и 3. Они попарно умножаются на элементы в векторе x: 1 и 5. Затем, произведения чисел складываются и в результате получаем 16. Эта операция называется точечным произведением, или скалярным произведением, для чего, собственно, и предназначено матричное умножение.

    Имплементация на Python с использованием pandas:

    df3 = pd.DataFrame([[1,3],[0,4],[2,1]]) vec = [1,5] df3.dot(vec)
    0 16 1 20 2 7 dtype: int64

    Матрично-матричное умножение

    Выполнение матрично-матричного умножения матриц очень походит на матрично-векторное умножение. Из соответствующих элементов матриц A и B попарно, строка за строкой и столбец за столбцом, берется сумма произведений.

    Как и ранее, мы можем умножать матрицы между собой, только когда число столбцов в первой матрице равно числу строк во второй. Если первая матрица A имеет размерность mA × nA, а вторая матрица B — размерность mB × nB, то для того чтобы их перемножить, nA и mB должны быть эквивалентными.

    В приведенном выше наглядном примере:

    К счастью, нам не нужно запоминать эту процедуру. В библиотеках pandas и numpy используются очень эффективные алгоритмы, которые выполняют матричную алгебру за нас.

    df3 = pd.DataFrame([[1,3],[0,4],[2,1]]) df4 = pd.DataFrame([[1,0],[5,6]]) df3.dot(df4)
     0 1 0 16 18 1 20 24 2 7 6

    Или то же самое в библиотеке numpy:

    np.matmul(df3,np.asarray(df4))

    Транспонирование

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

    Столбцы и строки изменились таким образом, что:

    Имплементация на Python с использованием pandas:

    df3.T
     0 1 2 0 1 0 2 1 3 4 1

    Подробнее об операциях с матрицами и векторами см. в моем репо на Gitgub для этой серии постов.

    Нейтральная матрица

    Некоторые матрицы имеют особые свойства и регулярно используются в матричной алгебре. Одной из самых важных таких матриц является нейтральная матрица, или единичная матрица. Это квадратная матрица с единицами вдоль главной диагонали и нулями в остальных позициях:

    Единичная матрица — это нейтральная по умножению матрица (или нейтральный элемент для умножения). Как и при скалярном умножении на число 1, матричное умножение на нейтральную матрицу не имеет никакого эффекта.

    Для получения нейтральной матрицы нам придется обратиться к функции библиотеке numpy np.identity , которая конструирует нейтральные матрицы. Учитывая, что они всегда квадратные, мы передаем всего один аргумент, который обозначает ширину и высоту одновременно.

    Имплементация на Python с использованием pandas:

    df = pd.DataFrame(np.identity(5)) df
     0 1 2 3 4 0 1.0 0.0 0.0 0.0 0.0 1 0.0 1.0 0.0 0.0 0.0 2 0.0 0.0 1.0 0.0 0.0 3 0.0 0.0 0.0 1.0 0.0 4 0.0 0.0 0.0 0.0 1.0

    Обратная матрица

    Если мы имеем квадратную матрицу A, то обратная для A матрица обозначается как A - 1 , и она будет иметь следующие свойства, где I — это нейтральная матрица:

    Нейтральная матрица является обратной самой себе. Не все матрицы обратимы. Необратимые матрицы также называются сингулярными или вырожденными. Обратная матрица вычисляется посредством функции np.linalg.pinv из модуля линейной алгебры библиотеки numpy.

    Имплементация на Python с использованием pandas:

    df5 = pd.DataFrame(np.random.rand(3, 3), list('abc'), list('xyz')) print(df5) df_inv = pd.DataFrame(np.linalg.pinv(df5.values), df5.columns, df5.index) print(df_inv)
     x y z a 0.625754 0.385261 0.462726 b 0.615084 0.111360 0.255420 c 0.723909 0.270869 0.221620 a b c x -1.451613 1.303231 1.528861 y 1.584699 -6.402303 4.070011 z 2.804750 3.568103 -5.456161

    Нормальное уравнение

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

    Мы читаем «для отыскания β надо умножить инверсию произведения транспонированной X и X, на произведение транспонированной X и y», где X — это матрица независимых переменных (включая член пересечения) и y — вектор, содержащий зависимые переменные нашей выборки. Результат β содержит вычисленные коэффициенты. Нормальное уравнение относительно легко выводится из уравнения множественной регрессии, применяя правила матричного умножения, однако соответствующие математические выкладки лежат за пределами объема данного поста.

    Указанное нормальное уравнение можно имплементировать на Python, используя для этого только те функции, с которыми мы только что познакомились:

    def normal_equation(x, y): '''Имплементация нормального уравнения''' # numpy.linalg.inv(A) фактически вызывает numpy.linalg.solve(A,I), # где I - это нейтральная матрица, и находит решение разложением # LU матрицы средствами динамической библиотеки lapack xtx = np.matmul(x.T.values, x.values) # вычислить мультипликативную инверсию матрицы xtxi = np.matmul(np.linalg.inv(np.matmul(xtx.T,xtx)),xtx.T) xty = np.matmul(x.T.values, y.values) return np.matmul(xtxi, xty) 

    Нормальное уравнение выражает математику регрессии методом обычных наименьших квадратов в очень сжатой форме. Его можно использовать следующим образом (помня о добавлении смещения):

    def ex_3_18(): '''Решение нормального уравнения на примере данных роста и веса''' df = swimmer_data() X = df[['Рост, см']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) return normal_equation(X, y)

    В результате выполнения этого примера получим следующую матрицу:

    array([ 1.69103131, 0.01429648])

    Это значения представляют коэффициенты β1 и β2, которые соответствуют параметрам пересечения и наклона. К счастью, они согласуются со значениями, которые мы вычислили ранее.

    Дополнительные признаки

    Часть мощи нормального уравнения состоит в том, что мы теперь имплементировали все, что нам нужно для того, чтобы обеспечить имплементацию множественной линейной регрессии. К счастью, нам не нужно специально создавать функцию для конвертации интересующих нас признаков в матрицу. Библиотека pandas позволяет за один прием отобрать отдельные столбцы для матрицы.

    В машинном усвоении закономерностей (Да-да, именно так. См. мои посты « Никто никого не обучает » и « Что такое machine learning? ») в качестве синонима для независимой переменной широко используется понятие «признак», англ. feature. Другими синонимами являются «предсказатель», «предиктор», «регрессор», «объяснительная переменная», либо просто «входная переменная».

    Для начала в качестве наших двух признаков отберем рост и возраст:

    def ex_3_19(): '''Пример создания матрицы признаков NumPy на примере данных роста и возраста''' X = swimmer_data()[['Рост, см', 'Возраст']] X.insert(0, 'константа', 1) return X.values

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

    array([[ 1., 166., 23.], [ 1., 192., 22.], [ 1., 173., 20.], . [ 1., 188., 24.], [ 1., 187., 19.], [ 1., 183., 22.]])

    Наша функция нормального уравнения примет эту новую матрицу без какого-либо дальнейшего изменения:

    def ex_3_20(): '''Решение нормального уравнения для данных роста и возраста в качестве независимых и веса в качестве зависимой переменной''' df = swimmer_data() X = df[['Рост, см', 'Возраст']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) return normal_equation(X, y)

    В результате получим нижеследующие коэффициенты:

    array([1.69002036, 0.01395437, 0.00279859])

    Эти три числа соответствуют соответственно пересечению, наклону (угловому коэффициенту) для роста (0.013954) и наклону для возраста (0.002799). В целях установления факта улучшения нашей модели за счет этих новых данных можно рассчитать значение R 2 нашей новой модели и сравнить его с представленным ранее.

    Множественный R-квадрат

    При расчете R 2 в ранее рассмотренном случае мы увидели, каким образом он выражает объем дисперсии, объясненной моделью:

    Учитывая, что дисперсия — это средневзвешенная квадратичная ошибка, мы можем умножить оба члена var(ε) и var(y) на размер выборки и прийти к приведенному ниже альтернативному уравнению для R 2 :

    Это попросту сумма квадратичных остатков на сумме квадратичных отклонений от среднего. При помощи функций библиотеки pandas dot функция суммы квадратов имплементируется элементарно, что во многом упрощает имплементацию матричного R-квадрата в исходном коде:

    def matrix_r_squared(coefs, x, y): '''Вычислить матричный R-квадрат''' fitted = x.dot(coefs) residuals = y - fitted difference = y - y.mean() rss = residuals.dot(residuals) # сумма квадратов ess = difference.dot(difference) return 1 - (rss / ess)

    Переменная rss обозначает остаточную сумму квадратов, от англ. residual sum of squares (RSS), переменная ess — объясненную сумму квадратов, от англ. explained sum of squares (ESS). Мы можем вычислить матричный R 2 для нашей новой модели следующим образом:

    def ex_3_21(): '''Вычислить матричный R-квадрат на данных роста и возраста в качестве независимых и веса в качестве зависимой переменной''' df = swimmer_data() X = df[['Рост, см', 'Возраст']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) beta = normal_equation(X, y) return matrix_r_squared(beta, X, y)
    0.7568466547183842

    В результате выполнения этого примера получим значение 0.757. Значение R 2 увеличилось на небольшую величину за счет включения возраста. Учитывая, что мы использовали несколько независимых переменных, R 2 теперь называется коэффициентом множественной детерминации.

    Скорректированный матричный R-квадрат

    Мотивом для добавления в регрессию больше независимых переменных может быть то, что наше значение R 2 всегда растет. Добавление новых независимых переменных не сделает предсказание зависимой переменной сложнее — если новая объяснительная переменная не имеет объяснительной силы, то ее коэффициент просто будет равен 0, и R 2 останется таким же, каким он был без этой независимой переменной.

    Вместе с тем, добавление новой переменной не говорит нам об улучшении или ухудшении модели. Если мы хотим узнать о том, помогает ли новая переменная на самом деле генерировать более качественную подгонку к данным, мы можем воспользоваться скорректированным , нередко записываемым как 2 . В отличие от R 2 , скорректированный 2 будет расти, только если новая независимая переменная увеличивает R 2 больше, чем ожидается вследствие случайности:

    def matrix_adj_r_squared(coefs, x, y): '''Вычислить скорректированный матричный R-квадрат''' r_squared = matrix_r_squared(coefs, x, y) n = y.shape[0] # строки p = coefs.shape[0] dec = lambda x: x-1 return 1 - (1 - r_squared) * (dec(n) / dec(n-p))

    Скорректированный 2 зависит от двух дополнительных параметров, n и p, которые относятся соответственно к размеру выборки и числу модельных параметров:

    def ex_3_22(): '''Вычислить скорректированный матричный R-квадрат на данных роста и возраста в качестве независимых и веса в качестве зависимой переменной''' df = swimmer_data() X = df[['Рост, см', 'Возраст']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) beta = normal_equation(X, y) return matrix_adj_r_squared(beta, X, y)
    0.7559934850858171

    Этот пример возвращает значение 0.756. Оно по-прежнему крупнее изначальной модели, поэтому возраст определенно несет некую объяснительную силу.

    Линейная модель в numpy и scipy

    Хотя имплементация нашей собственной версии нормального уравнения и 2 предоставляет ценную возможность познакомиться с матричной алгеброй, важно отметить, что библиотеки numpy и scipy предлагают соответственно функции np.linalg.lstsq и stats.linregress , которые делают все то, что мы рассмотрели и даже больше. В прилагаемых к данной серии постов примерах исходного кода имеется пример, в котором продемонстрирована работа этих функций.

    К примеру, функция numpy np.linalg.lstsq ожидает вызова с аргументами x и y (в виде последовательностей либо матриц, в случае множественной регрессии). Указанная функция вернет коллекцию x , содержащую решение методом обычных наименьших квадратов, остатки residuals , эффективный ранг матрицы rank и сингулярные значения s . Мы воспользуемся этой функцией для написания простой обертки, которую будем использовать вместо собственной имплементации нормального уравнения. Наша имплементация функции линейной модели будет возвращать только коэффициенты модели и будет использоваться для расчета беты, в частности, в приведенном ниже F-тесте:

    def linear_model(x, y): '''Обертка вокруг библиотечной функции линейной регрессии наименьшими квадратами, вместо собственной имплементации нормального уравнения normal_equation''' return np.linalg.lstsq(x,y,rcond=-1)[0]

    F-тест значимости модели

    Как мы выяснили в предыдущей серии постов о тестировании гипотез, проверка на основе F-теста применима, когда выполняется сразу несколько тестов статистической значимости. В случае с множественной линейной регрессией, мы проверяем статистическую неотличимость от нуля каких-либо коэффициентов модели, за исключением пересечения.

    Поэтому наши нулевая и альтернативная гипотезы будут следующими:

    Здесь j — это некий индекс в векторе параметров за исключением пересечения, т.е. свободного члена. Вычисляемая нами F-статистика представляет собой отношение объясненной дисперсии на необъясненной (остаточной) дисперсии. Она может быть выражена через отношение средневзвешенного квадрата регрессионной модели, от англ. mean squared model (MSM) на средневзвешенной квадратичной ошибке, от англ. mean square error (MSE):

    Средневзвешенный квадрат регрессионной модели (MSM) равен объясненной сумме квадратов (ESS) деленной на модельную степень свободы, где модельная степень свободы — это число параметров в модели за исключением свободного члена. Средневзвешенная квадратичная ошибка (MSE) равна остаточной сумме квадратов (RSS) деленной на остаточную степень свободы, где остаточная степень свободы — это размер выборки минус число модельных параметров.

    После расчета F-статистики мы отыскиваем ее в F-распределении, параметризованном теми же двумя степенями свободы:

    def f_test(fitted, x, y): '''F-тест коэффициентов регрессии''' difference = fitted - y.mean() residuals = y - fitted ess = difference.dot(difference) # сумма квадратов rss = residuals.dot(residuals) p = x.shape[1] # столбцы n = y.shape[0] # строки df1 = p - 1 df2 = n - p msm = ess / df1 mse = rss / df2 f_stat = msm / mse # mse модели / mse остатков f_test = 1-stats.f.cdf(f_stat, df1, df2) return f_test
    def ex_3_23(): '''Проверка значимости модели на основе F-теста на примере данных роста, возраста и веса''' df = swimmer_data() X = df[['Рост, см', 'Возраст']] X.insert(0, 'константа', 1.0) y = df['Вес'].apply(np.log) beta = linear_model(X, y) fittedvalues = np.dot(X,beta) # проверка коэффициентов модели return ('F-тест', f_test(fittedvalues, X, y))
    ('F-тест', 1.1102230246251565e-16)

    В результате проверки будет получено число 1.11x10e-16. Это ничтожно малое число, и, как следствие, можно быть уверенными в том, что модель значима.

    Отметим, что при малых выборках F-тест количественно измеряет увеличивающуюся неопределенность, что линейная модель допустима. В условиях случайной выборки из пяти элементов, например, данные иногда едва показывают какую-либо линейную связь вообще, и F-тест трактует данные, как незначимые даже при 50%-ом интервале уверенности.

    Категориальные и фиктивные переменные

    Теперь мы могли бы попытаться включить в регрессионный анализ «Пол» в качестве признака, однако мы столкнемся с проблемой. Входные данные выражены не числом, а как «М» или «Ж». Это пример категориальной переменной: переменной, которая может принимать одно из конечного множества неупорядоченных и (обычно) нечисловых значений. Другими примерами категориальных переменных является вид спорта, в котором спортсмен специализируется, или отдельно взятое состязание, в котором он наиболее квалифицирован.

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

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

    К счастью, многие категориальные переменные могут рассматриваться как дихотомические и, в действительности, наши выборочные данные содержат две категории половой принадлежности. Их можно внести в нашу регрессионную модель при условии, что мы преобразуем их в два числа, например, 0 и 1.

    Когда такая категория, как вид спорта, принимает более двух значений, для каждого вида спорта можно внести независимую переменную. При этом создают переменную для плавания и еще одну для тяжелой атлетики и т.д. Значение для плавания будет равно 1 для пловцов и 0 — для остальных.

    Поскольку половая принадлежность может оказаться для нашей регрессионной модели полезной объяснительной переменной, давайте преобразуем женский пол в 0 и мужской — в 1 и добавим производный столбец, который будет содержать фиктивную переменную.

    Давайте рассчитаем значение 2 , чтобы засвидетельствовать наличие или отсутствие улучшения в этом скорректированном показателе:

    def ex_3_25(): '''Обработка категориальных признаков (создание двоичной переменной)''' df = swimmer_data() df['бин_Пол'] = df['Пол'].map().astype(int) # строковое -> числовое X = df[['Рост, см', 'Возраст', 'бин_Пол']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) beta = linear_model(X, y) return matrix_adj_r_squared(beta, X, y)
    0.8082954905432824

    В результате выполнения этого примера получим 0.809. С участием таких признаков, как рост, возраст и пол, мы успешно объяснили более 80% дисперсии в весе наших олимпийских пловцов.

    Относительная мощность

    На этом этапе, возможно, было бы целесообразным поинтересоваться, какой признак играет самую важную роль в объяснении наблюдавшегося веса: возраст, пол или рост? Мы могли бы воспользоваться нашим скорректированным R 2 и взглянуть, насколько его значение изменяется, но это потребовало бы от нас повторно рассчитывать регрессию для каждой переменной, которую мы хотим проверить.

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

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

    В целях вычисления бета-коэффициента мы умножаем каждый коэффициент на отношение стандартных отклонений для связанной независимой переменной к модельной независимой переменной. Это можно имплементировать при помощи следующего ниже исходного кода на Python:

    def beta_weight(coefs, x, y): '''Вычисление относительного вклада каждого признака''' sdx = x.std() sdy = y.std() return [x / sdy * c for x,c in zip(sdx,coefs)] 
    def ex_3_26(): '''Относительный вклад каждого признака в предсказании веса на примере данных роста, возраста и пола''' df = swimmer_data() # получить двоичное поле df['бин_Пол'] = df['Пол'].map().astype(int) X = df[['Рост, см', 'Возраст', 'бин_Пол']] X.insert(0, 'константа', 1) y = df['Вес'].apply(np.log) beta = linear_model(X, y) res = beta_weight(beta, X, y) return res

    В результате мы получим (округлено до трех десятичных знаков):

    [0.0, 0.6501469135033348, 0.05842998157513067, 0.30387262631851747]

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

    Коллинеарность

    На данном этапе мы могли бы попытаться продолжить добавлять признаки в модель, стараясь увеличить объяснительную силу.

    Например, в нашем распоряжении также имеется столбец «Дата рождения», и может возникнуть соблазн попытаться внести и его. Это дата, но мы легко могли бы конвертировать ее в число, подходящее для использования в регрессии. Это можно сделать, попросту взяв год из даты рождения, воспользовавшись для этого библиотечной функцией pandas pd.to_datetime :

    '''Служебная функция приведения строкового представления даты к типу DateTime и извлечение года''' str_to_year = lambda x: pd.to_datetime(x).year def ex_3_27(): '''Относительный вклад признаков в предсказании веса с участием признака с датой (год)''' df = swimmer_data() df['бин_Пол'] = df['Пол'].map().astype(int) df['Год рождения'] = df['Дата рождения'].map(str_to_year) X = df[['Рост, см', 'Возраст', 'бин_Пол', 'Год рождения']] X.insert(0, 'константа', 1.0) y = df['Вес'].apply(np.log) beta = linear_model(X, y) return beta_weight(beta, X, y)
    [-0.0, 0.650070475196164, 0.09580282723307212, 0.3041431115029873, 0.03769748899125406]

    Новый признак «Год рождения» имеет бета-коэффициент всего 0.038, меньше веса признака «Возраст», который мы вычислили ранее. Однако, вес признака «Возраст» теперь показывает значение 0.096. Его относительная важность увеличилась более чем на 65%, поскольку мы добавили признак «Год рождения». Тот факт, что добавление нового признака изменило важность существующего признака, указывает на то, что имеется проблема.

    Включив дополнительный параметр «Год рождения», мы непреднамеренно нарушили правило регрессионного оценивания. Посмотрим почему:

    def ex_3_28(): '''График коллинеарности возраста спортсменов и даты их рождения''' df = swimmer_data() df['Год рождения'] = df['Дата рождения'].map(str_to_year) xs = df['Возраст'].apply(jitter(0.5)) ys = df['Год рождения'] pd.DataFrame(np.array([xs,ys]).T).plot.scatter(0, 1, s=3, grid=True) plt.xlabel('Возраст') plt.ylabel('Год рождения') #saveplot('ex_3_28.png') plt.show()

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

    Мультиколлинеарность

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

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

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

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

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

    Самый верный метод оценить степень мультиколлинеарности состоит в оценке регрессии каждой независимой переменной на всех других независимых переменных. Если любой R 2 из этих уравнений близок к 1.0, то имеется высокая мультиколлинеарность. На деле самый крупный из этих R 2 служит в качестве индикатора степени существующей мультиколлинеарности.

    После выявления мультиколлинеарности имеется несколько способов решить эту проблему:

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

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

    «Возраст» R 2 = 0.1049, тогда как «Год рождения» R 2 = 0.1050.

    Как и ожидалось, между двумя переменными практически нет разницы, и каждая из них объясняет порядка 10% дисперсии в весе. Поскольку год рождения объясняет чуть больше дисперсии, мы его оставим и отбросим признак «Возраст».

    Примеры исходного кода для этого поста находятся в моем репо на Github. Все исходные данные взяты в репозитории автора книги.

    В следующем коротком посте, посте №4, будет рассмотрен процесс предсказания.

    • python
    • data science
    • программирование
    • статистический анализ
    • статистика
    • регрессия

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *