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

5.23. Типы данных

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

Типы данных

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


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

В 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()) корректно обрабатывают категориальные переменные только в виде факторов.

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


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

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

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

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

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


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

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

Фреймы данных поддерживают индексацию по строкам и столбцам, именование строк и столбцов, а также множество методов для агрегации, фильтрации и преобразования данных. Хотя в последние годы пакет tibble из экосистемы tidyverse предлагает более современную и строгую альтернативу, классический data.frame остаётся стандартом и совместим со всеми базовыми функциями R.


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

Матрицы — это двумерные структуры данных, состоящие из элементов одного типа. Они создаются с помощью функции 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", хотя внутренне он реализован как список.


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

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

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

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

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

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 на соответствующих позициях, что отражает принцип распространения отсутствующих данных.


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

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

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

Функции в R передают аргументы по значению, но благодаря механизму copy-on-modify фактическое копирование происходит только при необходимости. Это позволяет писать чистые функции без побочных эффектов.


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

Отсутствующие данные (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 — они не только короче, но и значительно быстрее.
  • При работе с большими данными избегайте частых модификаций объектов — лучше собирать результат в один проход или использовать специализированные структуры данных.

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

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

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

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