Перейти к основному содержимому

Типы данных и векторные операции

Разработчику Архитектору

Дальше: Справочник R · таблица Операции для атомарных векторов — ниже


Типы данных и векторные операции

Практический вход в эту главу — первая программа и блок с data.frame в основах. Здесь систематически разбираются атомарные типы, факторы, списки, NA и индексация — то, без чего легко получить "тихие" ошибки при чтении CSV или построении модели. Обобщённо про переменные и типы в коде — раздел "Код"; табличные данные — данные и разметка.

Язык R изначально создавался как среда для статистических вычислений, и его система типов отражает эту специализацию. В R всё является объектом: у каждого объекта есть тип, атрибуты и значение. Ниже — от скалярных векторов до data.frame, copy-on-modify и работы с пропусками.


Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…


Объекты и их типы

В R не существует примитивных типов в том смысле, как они понимаются в таких языках, как C или Java. Даже одиночное число или логическое значение представляет собой полноценный объект. Каждый объект принадлежит к одному из пяти основных атомарных типов:

  • logical — логический тип, принимающий значения TRUE или FALSE. Это основа булевой логики в R.
  • integer — целочисленный тип, задаётся с суффиксом L, например 42L. Без суффикса число интерпретируется как числовой тип с плавающей точкой.
  • double — числовой тип с плавающей точкой двойной точности. Именно этот тип используется по умолчанию для всех числовых литералов, таких как 3.14 или -7.
  • complex — комплексный тип, позволяющий работать с числами вида a + bi, где i — мнимая единица. Например, 1 + 2i.
  • character — символьный тип, представляющий строки текста. Строки всегда заключаются в кавычки, одинарные или двойные: "hello" или 'R language'.

Эти типы называются атомарными, потому что они не содержат внутри себя других объектов. Любой вектор в R, даже если он состоит из одного элемента, будет иметь один из этих типов. Это важное свойство: в R нет отдельного "скаляра" как самостоятельного типа — скалярное значение всегда является вектором длины один.

Помимо атомарных типов, существуют рекурсивные типы, которые могут содержать другие объекты:

  • list — список, гетерогенная коллекция объектов, каждый из которых может иметь собственный тип.
  • function — функция, являющаяся исполняемым объектом, который можно вызывать, передавать как аргумент или возвращать из другой функции.
  • expression — выражение, представляющее собой последовательность команд, предназначенных для отложенного выполнения.
  • environment — окружение, структура, хранящая связки имён и значений, используемая для разрешения переменных во время выполнения.

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

  • NA — значение, обозначающее отсутствие данных. Оно может появляться в любом типе и сохраняет информацию о своём исходном типе (NA_integer_, NA_real_, NA_character_, NA_complex_).
  • NULL — пустое значение, указывающее на отсутствие объекта. В отличие от NA, NULL не имеет типа и используется для обозначения того, что переменная не содержит никакого значения.
  • Inf и -Inf — положительная и отрицательная бесконечность, возникающие при делении на ноль или переполнении.
  • NaN — "не число", результат неопределённых операций, таких как 0/0.

Переменные и присваивание

Переменная в R — это имя, связанное с объектом в текущем окружении. Присваивание осуществляется с помощью операторов <- или =, хотя каноническим и рекомендуемым способом считается использование <-. Например:

x <- 42
name <- "Anna"

Когда выполняется присваивание, R создаёт новый объект в памяти и связывает указанное имя с этим объектом. Важно понимать, что в R используется семантика копирования при модификации. Это означает, что если одна переменная ссылается на объект, а затем другая переменная получает значение первой, то обе переменные ссылаются на один и тот же объект до тех пор, пока одна из них не будет изменена. При изменении R автоматически создаёт копию объекта, чтобы избежать побочных эффектов. Такой подход называется copy-on-modify.

Например:

a <- c(1, 2, 3)
b <- a
b[1] <- 99

После этого a остаётся неизменным, а b содержит новую копию вектора. Это поведение обеспечивает безопасность и предсказуемость, особенно при работе с большими наборами данных.

Имена переменных в R могут содержать буквы, цифры, точки и подчёркивания, но не могут начинаться с цифры. Имена чувствительны к регистру: MyVar и myvar — это разные переменные. Также существуют зарезервированные слова, которые нельзя использовать в качестве имён, такие как if, else, function, TRUE, FALSE и другие.


Векторы — основа всех данных

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

logical → integer → double → complex → character

Например:

c(TRUE, 5) # → c(1, 5) — логическое значение превращается в целое
c(3, "text") # → c("3", "text") — число превращается в строку
c(1L, 2.5) # → c(1.0, 2.5) — целое становится числом с плавающей точкой

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

Векторы могут быть созданы с помощью функции c(), с помощью последовательностей (: или seq()), повторений (rep()), или путём чтения данных из внешних источников. Каждый вектор имеет атрибут length, показывающий количество элементов, и атрибут typeof, указывающий его внутренний тип.


Факторы — категориальные данные

Факторы — это специальный тип данных, предназначенный для представления категориальных переменных. Они хранят значения как уровни (levels) — уникальные категории, и используют целочисленные коды для внутреннего представления. Например:

gender <- factor(c("male", "female", "female", "male"))

Внутри gender хранится вектор целых чисел c(2, 1, 1, 2) и атрибут levels = c("female", "male"). Факторы полезны при статистическом анализе, так как многие функции R (например, lm() или table()) корректно обрабатывают категориальные переменные только в виде факторов.

Порядок levels важен для регрессий и графиков — factor(c("low", "high"), levels = c("low", "high")). Упорядоченные категории задают ordered = TRUE.

С R 4.0 при чтении CSV по умолчанию не преобразуют строки в факторы (stringsAsFactors = FALSE в read.csv()). Старые скрипты, рассчитывавшие на автоматические факторы, могут вести себя иначе — тип столбца лучше проверять через str() после загрузки.


Списки — гетерогенные коллекции

Play ITЗагрузка интерактивного демо…

Списки в R — это рекурсивные объекты, способные содержать элементы разных типов, включая другие списки, векторы, функции и даже окружения. Список создаётся с помощью функции list():

person <- list(name = "Ivan", age = 30, scores = c(85, 90, 78))

Элементы списка могут быть проиндексированы по позиции (person[[1]]) или по имени (person$name). Различие между одинарными и двойными квадратными скобками важно — person[1] возвращает список длины один, содержащий первый элемент, тогда как person[[1]] возвращает сам элемент (в данном случае строку "Ivan").

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

Операции (атомарный вектор vnumeric, character и т.д.):

ДействиеСинтаксис
Добавить в конецc(v, new) или append(v, new)
Прочитать / заменитьv[i], v[i] <- value
Вставитьappend(v, value, after = i)
Удалитьv[-i] (исключить индекс)

Именованный список — lst$name, lst[[i]], lst$i <- val.


Сводка операций по основным коллекциям R

Эта таблица нужна как быстрый "чек-лист" во время практики: что и как меняется у каждого типа коллекции.

Тип коллекцииДобавитьПрочитатьЗаменитьУдалить
Атомарный векторc(v, x)v[i]v[i] <- xv <- v[-i]
Списокappend(lst, list(x))lst[[i]] / lst$namelst[[i]] <- xlst[[i]] <- NULL
data.frame (строки)rbind(df, row)df[i, ]df[i, "col"] <- xdf <- df[-i, ]
data.frame (столбцы)df$new <- xdf[["col"]]df[["col"]] <- xdf$col <- NULL
Матрицаrbind(m, row) / cbind(m, col)m[i, j]m[i, j] <- xпересборка без индекса

Отдельно про словарь и множество в R:

  • роль "словаря" обычно выполняет именованный список (list(a = 1)) или environment;
  • роль "множества" обычно реализуют как уникальный вектор (unique(x)) или пакетные структуры (sets, data.table::foverlaps в специфичных задачах).

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


Play ITЗагрузка интерактивного демо…


Фреймы данных — табличные структуры

Фрейм данных (data.frame) — это основной формат для хранения табличных данных в R. Он представляет собой список векторов одинаковой длины, где каждый вектор соответствует столбцу. Столбцы могут иметь разные типы — один — числовой, другой — символьный, третий — фактор. Это делает фреймы данных гибкими для представления реальных наборов данных.

Фреймы данных поддерживают индексацию по строкам и столбцам, именование строк и столбцов, а также множество методов для агрегации, фильтрации и преобразования данных. При выборке одной строки используйте drop = FALSE, иначе R "схлопнет" результат в вектор:

df[1, , drop = FALSE] # остаётся data.frame

Пакет tibble (tibble::as_tibble()) даёт строгую печать и предсказуемое подмножество столбцов; базовый data.frame по-прежнему везде совместим.


Матрицы и массивы

Матрицы — это двумерные структуры данных, состоящие из элементов одного типа. Они создаются с помощью функции matrix() и имеют атрибуты nrow и ncol. Массивы (array) обобщают матрицы на три и более измерений. Обе структуры полезны для численных вычислений, линейной алгебры и многомерного анализа.

Важно помнить, что матрицы и массивы в R — это векторы с дополнительными атрибутами размерности (dim). Это позволяет эффективно выполнять операции над ними, используя векторизацию.


Атрибуты объектов

Любой объект в R может иметь атрибуты — дополнительные метаданные, такие как имена (names), размеры (dim), уровни (levels), класс (class) и пользовательские атрибуты. Атрибуты не влияют на содержимое объекта, но определяют его поведение в различных контекстах. Например, наличие атрибута class = "data.frame" заставляет R применять специальные методы печати и индексации.

Атрибуты можно просматривать с помощью attributes() и устанавливать с помощью attr() или специализированных функций, таких как names(), dim(), class().


Преобразование и проверка типов

R предоставляет богатый набор функций для проверки и преобразования типов объектов. Проверка типа выполняется с помощью семейства функций is.*(), например:

  • is.logical(x) — проверяет, является ли объект логическим;
  • is.numeric(x) — проверяет, является ли объект числовым (включает как целые, так и числа с плавающей точкой);
  • is.character(x) — проверяет символьный тип;
  • is.list(x), is.matrix(x), is.data.frame(x) — проверяют принадлежность к соответствующим структурам.

Эти функции возвращают логическое значение TRUE или FALSE, что позволяет строить условную логику на основе типа данных.

Преобразование типов осуществляется с помощью функций as.*(). Например:

  • as.numeric("42") преобразует строку в число;
  • as.character(TRUE) превращает логическое значение в строку "TRUE";
  • as.list(c(1, 2, 3)) создаёт список из вектора.

Важно понимать, что не все преобразования допустимы. Попытка преобразовать нечисловую строку в число, например as.numeric("hello"), приведёт к получению NA и предупреждению. Это поведение помогает избежать скрытых ошибок, но требует внимательности при работе с данными, особенно при чтении из внешних источников.

R также различает понятия mode, storage mode и class. Функция mode() возвращает базовый тип данных (например, "numeric", "character"), storage.mode() указывает, как данные хранятся в памяти ("double", "integer" и т.д.), а class() определяет поведенческий тип объекта, используемый для диспетчеризации методов. Например, фрейм данных имеет class = "data.frame", хотя внутренне он реализован как список.

Объектtypeof()class()
c(1, 2)doubleNULL
c(1L, 2L)integerNULL
data.frame(x = 1)listdata.frame
factor("a")integerfactor
typeof(c(1L, 2L)) # "integer"
class(mtcars) # "data.frame"
inherits(mtcars, "data.frame")

Векторизация и элементарные операции

Одна из ключевых особенностей R — векторизация. Почти все операции в R применяются поэлементно к векторам. Например:

x <- c(1, 2, 3)
y <- c(4, 5, 6)
x + y # → c(5, 7, 9)

Если векторы имеют разную длину, R автоматически повторяет более короткий вектор (recycling rule). Это мощный механизм, но опасный, если длины не кратны друг другу — появится предупреждение:

c(1, 2, 3) + 1:2 # recycling; смотрите warning

Логические операции (==, >, &, |) также векторизованы и возвращают логический вектор той же длины. Эти векторы часто используются для индексации:

x[x > 2] # → c(3)

Такой подход делает код компактным и выразительным, избегая явных циклов, которые в R работают медленнее.


Индексация и выборка

Индексация в R поддерживает несколько форм:

  • Позиционнаяx[1], x[c(1, 3)];
  • Логическаяx[c(TRUE, FALSE, TRUE)];
  • Именованная — если вектор имеет имена (names(x) <- c("a", "b", "c")), можно использовать x["a"].

Для списков и фреймов данных доступны дополнительные формы: [[ ]] для извлечения элемента, $ для обращения по имени столбца.

Отрицательная индексация (x[-1]) исключает указанные позиции. Индексация нулём (x[0]) возвращает пустой объект того же типа.

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

Практическая памятка по индексам:

x <- c(a = 10, b = 20, c = 30)
x[2] # 20
x["b"] # 20
x[-1] # удалить первый
x[c(TRUE, FALSE, TRUE)] # фильтр по маске

Для data.frame важно различать одно и два измерения:

df <- data.frame(id = 1:3, v = c(10, 20, 30))
df[1, ] # первая строка
df[, "v"] # столбец (вектор)
df[, "v", drop = FALSE] # столбец как data.frame

Копирование, ссылки и производительность

Как уже упоминалось, R использует семантику copy-on-modify. Это означает, что при присваивании объект не копируется немедленно, а лишь тогда, когда происходит его изменение. Такой подход сочетает удобство работы с данными и разумное использование памяти.

Однако при работе с большими объектами (например, фреймами данных на миллионы строк) частые модификации могут привести к значительным накладным расходам. В таких случаях рекомендуется использовать специализированные пакеты, такие как data.table или dplyr, которые оптимизированы для эффективной обработки.

Аргументы в функции передаются по значению в смысле семантики языка, но физически объект копируется только при изменении (copy-on-modify / REFCNT). Это помогает избегать побочных эффектов, пока функция не меняет переданный объект явно.


Работа с отсутствующими и специальными значениями

Отсутствующие данные (NA) — неотъемлемая часть аналитики. R корректно обрабатывает их во всех операциях — арифметические действия с NA дают NA, логические — тоже, за исключением NA & FALSE и NA | TRUE, которые возвращают FALSE и TRUE соответственно.

Многие функции принимают аргумент na.rm = TRUE, чтобы игнорировать NA при вычислениях:

mean(c(1, 2, NA), na.rm = TRUE) # → 1.5

Функция is.na() позволяет идентифицировать отсутствующие значения, а complete.cases() — находить строки без NA в фреймах данных.

Специальные значения Inf, -Inf и NaN возникают при граничных вычислениях. Они обрабатываются согласно стандарту IEEE 754 и могут быть проверены с помощью is.infinite(), is.nan().


Создание пользовательских классов

Хотя R не является строго объектно-ориентированным языком в классическом смысле, он поддерживает несколько систем ООП — S3, S4, R6. Наиболее простая и широко используемая — S3.

Чтобы создать объект с пользовательским классом, достаточно присвоить ему атрибут class:

person <- list(name = "Alex", age = 28)
class(person) <- "Person"

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

print.Person <- function(x) {
cat("Имя:", x$name, "\nВозраст:", x$age, "\n")
}

Теперь вызов print(person) будет использовать этот метод. Такой подход позволяет расширять поведение объектов, сохраняя совместимость с существующим кодом.


Внутреннее представление объектов

R реализован на языке C, и его объектная система основана на структуре SEXP (S-expression). Каждый объект в R имеет заголовок, содержащий информацию о типе, длине, атрибутах и ссылках. Это позволяет эффективно управлять памятью и выполнять операции над данными.

Для пользователя эта деталь обычно остаётся скрытой, но понимание того, что все объекты имеют единый внутренний формат, помогает объяснить поведение функций typeof(), mode() и storage.mode(). Например, typeof(c(1L, 2L)) возвращает "integer", а typeof(c(1, 2))"double", несмотря на то, что оба вектора выглядят как числовые. Это различие важно при работе с низкоуровневыми функциями или при взаимодействии с кодом на C через интерфейс .Call().

Атрибуты объектов также хранятся в виде списка, присоединённого к основному телу объекта. Доступ к ним осуществляется через функции attributes() и attr(). Удаление всех атрибутов, кроме dim и names, выполняется с помощью unclass(), что полезно при отладке или преобразовании объектов.


Имена и окружения

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

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

x <- 10
f <- function() {
x <- 20
x
}
f() # → 20
x # → 10

Здесь внутри функции создаётся новая локальная переменная x, которая не влияет на глобальную. Если требуется изменить глобальную переменную изнутри функции, используется оператор присваивания <<-.

Окружения сами являются объектами и могут быть созданы явно с помощью new.env(). Это позволяет строить замыкания и управлять состоянием в функциональном стиле.


Символы и выражения

Помимо значений, R оперирует с символьными представлениями кода. Символ (symbol) — это имя переменной, например quote(x) возвращает символ x, а не его значение. Выражение (expression) — это дерево вызовов и символов, которое можно анализировать или выполнять позже с помощью eval().

Эти возможности лежат в основе метапрограммирования в R и используются в таких пакетах, как dplyr и ggplot2, где формулы и спецификации передаются в нестандартной форме вычислений (non-standard evaluation, NSE).

Хотя эти темы выходят за рамки базового понимания типов данных, они демонстрируют глубину системы объектов R: даже код является объектом, который можно манипулировать как данные.


Практические рекомендации

При работе с типами данных в R стоит придерживаться следующих принципов:

  • Явно указывайте типы при создании векторов, если это возможно. Например, используйте integer(0) вместо c() для пустого целочисленного вектора.
  • Избегайте смешивания типов в векторах без необходимости — это приводит к неявному приведению к строке, что может нарушить логику анализа.
  • Проверяйте наличие NA перед выполнением агрегирующих операций, особенно если результат будет использоваться в дальнейших вычислениях.
  • Используйте str() для быстрого осмотра структуры объекта — эта функция показывает тип, длину, атрибуты и первые элементы.
  • Предпочитайте векторизованные операции циклам for или while — они не только короче, но и значительно быстрее.
  • При работе с большими данными избегайте частых модификаций объектов — лучше собирать результат в один проход или использовать специализированные структуры данных.

Типичные ошибки и как их избежать

ОшибкаПочему возникаетКак исправить
Неожиданное приведение к characterсмешали числа и строки в c(...)заранее привести тип, проверить typeof()
as.numeric("abc") даёт NAстрока не парсится как числопредварительная очистка, readr::parse_number()
if (x) на векторе длины > 1if ждёт один TRUE/FALSEif (any(x)) или if (all(x))
Потеря формы таблицызабыли drop = FALSEявно указывать drop = FALSE при подвыборках
Тихая ошибка с recyclingдлины векторов не кратныпроверять length(x) %% length(y) == 0 перед операцией

Эти проверки особенно важны в скриптах, где данные приходят из внешних источников — CSV, API, БД. Смежная практика с реальными примерами есть в простых приложениях.


Совместимость с другими системами

При импорте данных из внешних источников (CSV, Excel, базы данных) R автоматически пытается определить тип каждого столбца. Однако алгоритмы определения не всегда точны — строки, содержащие числа и буквы, могут быть прочитаны как факторы или строки, а даты — как обычные текстовые поля.

Поэтому после загрузки данных рекомендуется проверять типы столбцов с помощью sapply(df, class) и при необходимости выполнять явное преобразование. Например, даты следует приводить к классу Date или POSIXct с помощью as.Date() или as.POSIXct().

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