Типы данных и векторные операции
Дальше: Справочник 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, поскольку многие сложные структуры данных, такие как фреймы данных, реализованы как специальные виды списков.
Операции (атомарный вектор v — numeric, 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] <- x | v <- v[-i] |
| Список | append(lst, list(x)) | lst[[i]] / lst$name | lst[[i]] <- x | lst[[i]] <- NULL |
data.frame (строки) | rbind(df, row) | df[i, ] | df[i, "col"] <- x | df <- df[-i, ] |
data.frame (столбцы) | df$new <- x | df[["col"]] | df[["col"]] <- x | df$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) | double | NULL |
c(1L, 2L) | integer | NULL |
data.frame(x = 1) | list | data.frame |
factor("a") | integer | factor |
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) на векторе длины > 1 | if ждёт один TRUE/FALSE | if (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 имеют прямые аналоги. Комплексные числа, факторы и пользовательские классы могут потребовать дополнительной сериализации.