Язык программирования R для анализа данных: лекция 2

Елена Убогоева

Повторим материал предыдущей лекции

  • Переменные

  • Векторы

    • Какие бывают типы векторов?

    • Неявное и явное приведение типов

    • Индексация векторов: по номеру индекса, логическим вектором

  • Логические операторы: И, ИЛИ, НЕ

  • Пропущенные значения:

    • Поиск пропущенных значений

    • Исключение пропущенных значений из вектора

Important

Важно! Значения в векторе могут быть только одного типа

План лекции

  • Матрицы

  • Списки

  • Датафреймы

  • Установка пакетов

  • Условия, циклы (и особенности их использования)

  • Векторизация как концепт, заменяющий циклы

Взаимоотношение типов данных в R

Матрицы и списки (lists) являются расширением векторов, в свою очередь датафреймы объединяют свойства матриц и списков.

Поэтому важно рассмотреть матрицы и списки перед разбором датафреймов.

Матрицы

Почти такое же понимание как и в линейной алгебре.

Создадим матрицу (укажем числа и количество строк):

M <- matrix(1:20, nrow = 5)
M
     [,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

Можно сказать, что матрица - вектор, имеющий размерность 2 (строки и столбцы). Матрица, как и вектор, может содержать данные только одного типа. Бывают логические, числовые и строковые матрицы (последние редко).

Индексация матриц

Для индексации используем квадратные скобки, матрицы имеют два измерения, следовательно, надо прописывать оба: matrix[rows, columns]

  • Поэлементное извлечение

    M[1, 1] # извлечь элемент первой строки первого столбца матрицы
    [1] 1
    M[3, 4] # извлечь элемент третьей строки четвертого столбца матрицы
    [1] 18
  • Извлечь целую строку или столбец

    M[1, ] # извлечь первую строку
    [1]  1  6 11 16
    M[, 4] # извлечь четвертый столбец
    [1] 16 17 18 19 20

Индексация матриц

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

M[1:3, ] # извлечь строки с первой по третью
     [,1] [,2] [,3] [,4]
[1,]    1    6   11   16
[2,]    2    7   12   17
[3,]    3    8   13   18
M[1:2, 2:3] # извлечь элементы первой-второй строк второго и третьего столбца
     [,1] [,2]
[1,]    6   11
[2,]    7   12
M[c(3, 1), ] # извлечь третью, первую строку
     [,1] [,2] [,3] [,4]
[1,]    3    8   13   18
[2,]    1    6   11   16

Правила индексации матриц нам очень понадобятся при работе с датафреймами

Замена элементов матриц

Также, как и при работе с векторами, часть элементов матрицы можно переписать.

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

x <- 1:10
x[5] <- 100
x
 [1]   1   2   3   4 100   6   7   8   9  10
M[1:3, 2:4] <- 100
M
     [,1] [,2] [,3] [,4]
[1,]    1  100  100  100
[2,]    2  100  100  100
[3,]    3  100  100  100
[4,]    4    9   14   19
[5,]    5   10   15   20

Матрицы, как тип данных, используются при создании тепловых карт.

Списки (lists)

Список в R может содержать данные разного типа, даже другие списки. Список можно создать функцией list().

list1 <- list(1:5, 'JoJo', TRUE)
list1
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "JoJo"

[[3]]
[1] TRUE

Вложенные списки

list_complex <- list(list1, c('this', 'is', 'complex list'))
list_complex
[[1]]
[[1]][[1]]
[1] 1 2 3 4 5

[[1]][[2]]
[1] "JoJo"

[[1]][[3]]
[1] TRUE


[[2]]
[1] "this"         "is"           "complex list"

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

Парсинг - сбор и структурирование данных с последующим анализом.

Структура листа

У сложных списков удобно посмотреть структуру, используя функцию str()

str(list_complex)
List of 2
 $ :List of 3
  ..$ : int [1:5] 1 2 3 4 5
  ..$ : chr "JoJo"
  ..$ : logi TRUE
 $ : chr [1:3] "this" "is" "complex list"

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

named_list <- list(names = c('Joseph', 'Jotaro'), 
                   part = c(2, 3))
named_list
$names
[1] "Joseph" "Jotaro"

$part
[1] 2 3

Индексация списков

Индексация списков устроена несколько сложнее, чем векторов и матриц.

Для начала попробуем индексацию по номеру

list1[1]
[[1]]
[1] 1 2 3 4 5
class(list1[1])
[1] "list"
length(list1[1])
[1] 1

Получился список длиной 1. Как же извлечь элемент списка не в виде списка?

Иллюстрация:

Взято из твиттера @hadleywickham

Индексация списков

Чтобы извлечь элемент списка не в виде списка можно использовать две квадратные скобки sample_list[[1]].

list1[[1]]
[1] 1 2 3 4 5

Также можно использовать индексацию по имени: используя знак $ или имя элемента в квадратных скобках в кавычках.

named_list$part
[1] 2 3
named_list[['part']]
[1] 2 3

Создание новых элементов списка

Удобнее всего создать новый элемент с помощью знака $

named_list$year <- c(2012, 2014)
named_list
$names
[1] "Joseph" "Jotaro"

$part
[1] 2 3

$year
[1] 2012 2014

Датафреймы

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

Попробуем создать датафрейм: функция data.frame()

df <- data.frame(names = c('Alexandra', 'Vlad', 'Ekaterina'), 
                 year = c(5, 3, 6),
                 tasks = c(TRUE, FALSE, TRUE))
df
      names year tasks
1 Alexandra    5  TRUE
2      Vlad    3 FALSE
3 Ekaterina    6  TRUE

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

Индексация датафреймов

Индексация датафреймов объединяет принципы индексации матриц и списков.

Индексация датафреймов как матриц

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

Поэлементное извлечение:

df[2, 1]
[1] "Vlad"

Извлечение конкретных строк и столбцов

df[2:3, 1:2]
      names year
2      Vlad    3
3 Ekaterina    6

Извлечь целые строки или столбцы:

df[2, ]
  names year tasks
2  Vlad    3 FALSE
df[, 3]
[1]  TRUE FALSE  TRUE

Tip

При расстановке пробелов пользуемся правилами пунктуации: пробел ставится после запятой.

Индексация датафреймов как списков

С помощью знака $ или можно с помощью квадратных скобок

df$names
[1] "Alexandra" "Vlad"      "Ekaterina"
df$year
[1] 5 3 6
df[['names']]
[1] "Alexandra" "Vlad"      "Ekaterina"
df$names == df[, 1]
[1] TRUE TRUE TRUE

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

Похоже на обращение к существующей, но пишем новое название и заполняем значениями.

df$email <- c('email1', 'email2', 'email3')
df
      names year tasks  email
1 Alexandra    5  TRUE email1
2      Vlad    3 FALSE email2
3 Ekaterina    6  TRUE email3

Подробнее про работу с датафреймами еще поговорим в лекции по tidyverse.

Установка пакетов

Пакеты с CRAN-а скачиваются командой install.packages('название пакета').

install.packages('tidyverse')

После того, как мы скачали пакет, его необходимо подгружать каждый раз при работе командой library(название пакета)

library(tidyverse)

Иногда можно подгрузить одну функцию из пакета, используя оператор ::

dplyr::case_when(<выражение>) # про это чуть дальше

Установка пакетов не из CRAN

  • С github-a или других репозиториев. Используется пакет remotes.

    remotes::install_github('ggpattern')
  • С биокондактора. Сначала нужно установить сам bioconductor - менеджер биологических пакетов.

    if (!require("BiocManager", quietly = TRUE))
        install.packages("BiocManager")

    Далее устанавливаем нужный пакет, хранящийся в биокондакторе:

    BiocManager::install(c("GenomicFeatures", "DESeq2"))

Условные конструкции: if

Синтаксис условной конструкции: if (condition) true_action

number <- 10
if (number > 0) print('Положительное число')
[1] "Положительное число"

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

number <- 10
if (number > 0) {
  print('Положительное число')
}
[1] "Положительное число"

Условные конструкции: if, else

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

if (condition) true_action else false_action

random_number <- sample(-5:5, 1) # чтобы извлечь случайное число 
# из набора чисел от -5 до 5
random_number
[1] 2
if (random_number > 0) {
  print('Положительное число')
} else {
  print('Отрицательное число или ноль')
}
[1] "Положительное число"

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

Векторизованный if

Оператор if на вход принимает только одно число. Что делать, если нам нужно проверить на какое-то условие целый вектор?

Вот это работать не будет (начиная с версии R 4.0):

if (-3:3 > 0) print('Положительное число')
Error in if (-3:3 > 0) print("Положительное число"): условие длиной > 1

Для операции над векторами используем функцию ifelse() из base R или if_else() из библиотеки dplyr.

x <- 2:11
ifelse(x %% 2 == 0, 'Четное число', 'Нечетное число')
 [1] "Четное число"   "Нечетное число" "Четное число"   "Нечетное число"
 [5] "Четное число"   "Нечетное число" "Четное число"   "Нечетное число"
 [9] "Четное число"   "Нечетное число"

if_else() из библиотеки dplyr

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

dplyr::if_else(2:11 %% 2 == 0, 'Четное', 'Нечетное')
 [1] "Четное"   "Нечетное" "Четное"   "Нечетное" "Четное"   "Нечетное"
 [7] "Четное"   "Нечетное" "Четное"   "Нечетное"

Что произойдет, если мы используем разный тип данных для выражений выполненного условия и невыполненного?

library(dplyr)
if_else(-3:3 == 0, 0, 'Не ноль')
Error in `if_else()`:
! `false` must be a double vector, not a character vector.
ifelse(-3:3 == 0, 0, 'Не ноль')
[1] "Не ноль" "Не ноль" "Не ноль" "0"       "Не ноль" "Не ноль" "Не ноль"

Значения, в случае, если условие выполнилось, и если не выполнилось, должны быть одного типа.

Оператор case_when()

Если условий больше двух (или даже больше одного), на помощь приходит функция case_when() из пакета dplyr.

dplyr::case_when(x > 0 ~ 'Положительное число', 
                 x == 0 ~ 'Ноль', 
                 x < 0 ~ 'Отрицательное число')
 [1] "Положительное число" "Положительное число" "Положительное число"
 [4] "Положительное число" "Положительное число" "Положительное число"
 [7] "Положительное число" "Положительное число" "Положительное число"
[10] "Положительное число"

Здесь тоже работает требование об одинаковом типе данных на значения после выполнения условия.

Про пакет dplyr поговорим подробнее в лекции 4 про tidyverse.

Циклы

Циклы в R не рекомендованы к использованию, однако знать их синтаксис не повредит.

Синтаксис:

for (item in vector) {
  <набор действий>
}

Например:

for (i in 1:6) {
  print(i ^ 2)
}
[1] 1
[1] 4
[1] 9
[1] 16
[1] 25
[1] 36

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

(1:6) ^ 2
[1]  1  4  9 16 25 36

Как использовать циклы правильно?

Одна из частых ошибок новичков при написании циклов - попасться в ловушку копирования.

Допустим, мы написали цикл, в котором на каждой итерации вектор увеличивается на 1 элемент. В R каждый раз происходит копирование и выделение нового места под вектор и это extremely slow.

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

Плохо:

values <- 11:20
cube_values <- vector()
for (i in 1:length(values)) {
  cube_values <- c(cube_values, values[i] ^ 3) # копирование на каждой итерации
}
cube_values
 [1] 1331 1728 2197 2744 3375 4096 4913 5832 6859 8000

Лучше создать вектор нужного размера перед циклом:

cube_values <- vector('numeric', 
  length = length(values)) 
for (i in 1:length(values)) {
  cube_values[i] <- values[i] ^ 3
}
cube_values
 [1] 1331 1728 2197 2744 3375 4096 4913 5832 6859 8000

Совсем правильно использовать seq_along():

cube_values <- vector(mode = 'numeric', 
                      length = length(values))
for (i in seq_along(values)) {
  cube_values[i] <- values[i] ^ 3
}
cube_values
 [1] 1331 1728 2197 2744 3375 4096 4913 5832 6859 8000

Рекомендуется использовать seq_along(), чтобы наверняка избежать проблемы с пустым вектором.

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

Tip

Совет от Соника: не использовать циклы вообще👍

Как использовать циклы правильно?

Вторая распространенная ошибка: проблема с 1:length(vector)

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

empty_vector <- vector()
for (i in 1:length(empty_vector)) {
  print(i ^ 2)
}
[1] 1
[1] 0

Похоже, что мы ожидали не этого. Чтобы этого избежать, вместо 1:length() используем seq_along().

for (i in seq_along(empty_vector)) {
  print(i ^ 2)
}

Векторизация - вместо циклов

Вообще правильный подход - использовать векторизацию вместо циклов.

Векторизация - это применение какой-либо функции над каждым элементом вектора.

Можно записать идею в математической нотации:

Пусть f() - векторизованная функция. Тогда \(y = f(x)\) означает, что мы применяем функцию f() к каждому элементу вектора x, на выходе получаем вектор y такой же длины как вектор x.

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

Фраза векторизованная функция означает, что операции производятся сразу над каждым элементом вектора.

Еще немного векторизации

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

(1:10) ^ 2
 [1]   1   4   9  16  25  36  49  64  81 100
log(1:10, 2)
 [1] 0.000000 1.000000 1.584963 2.000000 2.321928 2.584963 2.807355 3.000000
 [9] 3.169925 3.321928

Не являются векторизованными: mean(), sum(). Как думаете, почему?

mean(1:100)
[1] 50.5
sum(1:100)
[1] 5050

Применение векторизации

Например, нам нужно посчитать средние значения в списке, а функция mean() не векторизована. Что делать? Использовать функции семейства *apply().

list_values <- list(rnorm(10), rnorm(10, mean = 5, sd = 2),
                    rnorm(10, mean = 10, sd = 3))
list_values
[[1]]
 [1]  2.07983997  0.30414912  0.77383315 -0.63807175  0.28889034  0.23361007
 [7] -0.09532000  0.66944603  0.08062912 -1.00874211

[[2]]
 [1] 3.907073 2.732515 2.606927 5.805492 4.360885 3.701539 6.144990 5.664932
 [9] 7.398804 7.892041

[[3]]
 [1]  8.188229  6.341163  7.984042 10.407791 11.736316 11.862250  8.177342
 [8]  6.138101 15.692003 16.317226

rnorm() генерирует числа из нормального распределения со среднем 0 и стандартным отклонением 1 (по умолчанию).

lapply(list_values, mean)
[[1]]
[1] 0.2688264

[[2]]
[1] 5.021519

[[3]]
[1] 10.28445

Спасибо за внимание!

Подписывайтесь на телеграм-канал о статистике: