rguides

Working with Vectors and Atomic Types in R

Mastering R basics starts with vectors, the most fundamental data structure in the language. Unlike many other programming languages, everything in R is a vector: even a single value is technically a vector of length one. Understanding vectors and their types is essential for writing effective R code that takes advantage of vectorised operations.

This tutorial covers creating vectors, understanding atomic types, performing operations, and managing type coercion.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with R Basics. By the end, you will know how to apply the core functions in real data analysis workflows.

What are vectors?

A vector is an ordered collection of values of the same type. R stores vectors efficiently in memory and provides vectorized operations that work on entire vectors at once.

Creating vectors with c()

The c() function (combine) creates vectors by joining values together:

# Numeric vector
numbers <- c(1, 2, 3, 4, 5)
numbers
# [1] 1 2 3 4 5

# Character vector
names <- c("Alice", "Bob", "Charlie")
names
# [1] "Alice"   "Bob"     "Charlie"

# Logical vector
flags <- c(TRUE, FALSE, TRUE)
flags
# [1]  TRUE FALSE  TRUE

Checking vector properties

c() is the workhorse for manual vector creation, but once you have a vector, you need to inspect it. R provides introspection functions that tell you how many elements the vector contains, what type it stores, and whether R considers it a vector at all. These are used routinely in data analysis to verify that an operation produced the expected result:

# Length of a vector
length(numbers)
# [1] 5

# Check if it's a vector
is.vector(numbers)
# [1] TRUE

Atomic types in R

R has six atomic vector types: logical, integer, double (numeric), complex, character, and raw. The four most common — numeric, integer, character, and logical — cover nearly all data analysis needs. Each type has distinct behaviour in arithmetic, comparison, and coercion, so knowing the type hierarchy saves you from silent bugs later.

Numeric

The default type for numbers in R is double-precision floating-point. Any literal like 3.14 or 42 (without an L suffix) creates a numeric vector. R uses double-precision IEEE 754 arithmetic, which is fast and accurate for most purposes but can produce small rounding errors in edge cases:

# Numeric (double) is the default
x <- 3.14
typeof(x)
# [1] "double"

y <- c(1.5, 2.5, 3.5)
typeof(y)
# [1] "double"

Integer

Whole numbers in R are stored as a separate integer type, distinct from numeric doubles. Integers use less memory and guarantee exact arithmetic for counting operations, but they are not the default; you must explicitly request them with the L suffix. This explicit opt-in prevents accidental integer overflow and ensures you are aware of the type distinction when it matters, such as when calling C or Java interfaces that require 32-bit integers:

# Create integer with L suffix
whole <- c(1L, 2L, 3L)
typeof(whole)
# [1] "integer"

# Also from sequences
seq_int <- 1:5
typeof(seq_int)
# [1] "integer"

Character

Character vectors store text data. In R, strings are always wrapped in either single or double quotes — there is no bare-word string type. Behind the scenes, R stores character data in a global string pool to avoid duplicating identical strings in memory, which makes character vectors surprisingly efficient for repeated text. Character vectors power everything from data labels and factor levels to file paths and plot titles:

greeting <- "Hello, R!"
typeof(greeting)
# [1] "character"

words <- c("one", "two", "three")
typeof(words)
# [1] "character"

Logical

Logical vectors hold Boolean values: TRUE and FALSE. They are the result of comparison operators (>, <, ==) and form the backbone of subsetting and conditional filtering in R. Logical values also coerce to numeric (TRUE = 1, FALSE = 0), which lets you count matches by summing a logical vector or compute proportions by taking the mean. This dual nature makes logical vectors surprisingly versatile:

is_active <- c(TRUE, FALSE, TRUE, FALSE)
typeof(is_active)
# [1] "logical"

# Can also use T and F (not recommended)

Creating vectors: beyond c()

While c() is the most common way to build vectors by hand, R provides several specialised functions for generating sequences, repeating patterns, and sampling. These avoid the tedium of typing long numeric sequences manually and produce cleaner, less error-prone code.

seq() for sequences

seq() generates evenly spaced numeric sequences, offering finer control than the colon operator. You can specify the step size with by or the desired number of elements with length.out, making it the go-to tool for generating axis tick positions, grid points, or evenly spaced evaluation points for plotting:

# Sequence from 1 to 10
1:10
# [1]  1  2  3  4  5  6  7  8  9 10

# Using seq()
seq(1, 10, by = 2)
# [1] 1 3 5 7 9

seq(0, 1, length.out = 5)
# [1] 0.00 0.25 0.50 0.75 1.00

rep() for repeating values

rep() repeats values to fill vectors of arbitrary length. The times argument controls how many times the entire vector is repeated, while each repeats individual elements before moving to the next. This distinction matters in experimental design: each is used for blocking (all replicates of condition A, then all of condition B), while times produces alternating patterns (A, B, A, B):

# Repeat single value
rep(1, times = 5)
# [1] 1 1 1 1 1

# Repeat each element
rep(c(1, 2), each = 3)
# [1] 1 1 1 2 2 2

# Repeat entire vector
rep(c(1, 2), times = 3)
# [1] 1 2 1 2 1 2

Vector operations

One of R’s strengths is vectorized operations — arithmetic and logical operations that apply element-by-element across entire vectors without explicit loops. Because these operations are implemented in compiled C code, they run orders of magnitude faster than equivalent for loops. Understanding how element-wise operations and recycling interact is key to writing fast, correct R code:

# Arithmetic operations
c(1, 2, 3) + c(4, 5, 6)
# [1] 5 7 9

c(10, 20, 30) / c(2, 4, 5)
# [1] 5 5 6

# Recycling: shorter vectors are recycled
c(1, 2, 3, 4) + c(1, 2)
# [1] 2 4 4 6
# Warning: longer object length not a multiple of shorter

Recycling with caution

When two vectors of unequal length appear in an operation, R recycles the shorter one by repeating it until it matches the longer vector’s length. This is convenient for scalar operations, such as adding a single number to every element, but dangerous when the length mismatch is unintentional. R warns only when the longer vector’s length is not an exact multiple of the shorter one, which means partial recycling bugs can pass silently:

# What you might expect
c(1, 2, 3) + c(1, 2)
# [1] 2 4 4 (1+1, 2+2, 3+1)

# Always use vectors of equal length for clarity

Checking and converting types

Knowing what type your data is stored as matters for correctness. Numeric operations on a character vector fail, and integer arithmetic on doubles can introduce subtle bugs. R provides two families of functions for working with types: is.*() for checking without modifying, and as.*() for converting.

Type checking functions

R provides is.*() functions to test whether an object belongs to a specific type. These return TRUE or FALSE and are commonly used inside if() statements, validators, and quality checks in data pipelines:

x <- c(1, 2, 3)

is.numeric(x)
# [1] TRUE

is.integer(x)
# [1] FALSE (defaults to double)

is.character(x)
# [1] FALSE

# To create integer
y <- c(1L, 2L, 3L)
is.integer(y)
# [1] TRUE

Type conversion functions

Convert between types with as.*() functions like as.character(), as.numeric(), and as.logical(). Unlike many languages where type conversion throws an error on failure, R quietly returns NA with a warning. This silent partial failure means you should always verify the result after converting from character to numeric, especially when reading data from CSV files where a stray non-numeric entry can corrupt an entire column:

# Numeric to character
as.character(c(1, 2, 3))
# [1] "1" "2" "3"

# Character to numeric
as.numeric(c("1", "2", "3"))
# [1] 1 2 3

# Numeric to logical (0 = FALSE, non-zero = TRUE)
as.logical(c(0, 1, 2, -5))
# [1] FALSE  TRUE  TRUE  TRUE

# Character to logical
as.logical(c("TRUE", "FALSE", "true"))
# [1]  TRUE FALSE    NA

Implicit type coercion

When mixing types in a vector, R does not throw an error; it silently coerces all elements to the most flexible type in the hierarchy. The coercion order goes from least to most flexible: logical → integer → numeric → character. This means that a single character value in a numeric vector turns every number into a string, which is a common source of confusion when reading messy data:

# Numeric + character = character
c(1, "two", 3)
# [1] "1"  "two" "3"

# Logical + numeric = numeric
c(TRUE, 2, 3)
# [1] 1 2 3

# Order: logical → integer → double → character

Common gotchas

Floating point comparison

Not all numbers that look equal are actually equal inside the computer. Floating-point arithmetic in R follows the IEEE 754 standard, which represents decimal fractions as binary approximations. The expression 0.1 + 0.2 evaluates to a number that is extremely close to, but not exactly, 0.3. Using == for floating-point comparison almost always leads to bugs. The safe approach is all.equal(), which checks for equality within a small tolerance:

# This might surprise you
0.1 + 0.2 == 0.3
# [1] FALSE

# Use near-equality
all.equal(0.1 + 0.2, 0.3)
# [1] TRUE

NA values

NA is R’s sentinel for missing or unavailable data. Unlike NULL, which means “nothing here,” NA means “a value exists but we don’t know what it is.” Every atomic type has its own typed NA variant (NA_integer_, NA_real_, NA_character_), and operations involving NA propagate the uncertainty — any calculation with a missing input produces a missing output. This propagation is intentional: it prevents you from accidentally ignoring gaps in your data:

# NA is typelogical by default
typeof(NA)
# [1] "logical"

# Check for NA
x <- c(1, 2, NA, 4)
is.na(x)
# [1] FALSE FALSE  TRUE FALSE

# NA in operations propagates
sum(c(1, 2, NA, 4))
# [1] NA

# Use na.rm = TRUE
sum(c(1, 2, NA, 4), na.rm = TRUE)
# [1] 7

Working with vectors in practice

Vector operations, type checking, and coercion are building blocks. The real utility of vectors emerges when you need to pull out specific elements, filter based on conditions, or build lookup tables. These subsetting patterns carry over directly to data frames, lists, and matrices.

Subsetting vectors

Access elements using square brackets ([). R supports four subsetting modes: positive integers (keep these positions), negative integers (drop these positions), logical vectors (keep where TRUE), and character names (look up by name). Each mode has its own use case, and mixing them in a single expression is the source of many subtle bugs:

x <- c("a", "b", "c", "d", "e")

# By position
x[1]
# [1] "a"

x[c(1, 3)]
# [1] "a" "c"

# Negative indices remove elements
x[-1]
# [1] "b" "c" "d" "e"

# With logical vector
x[c(TRUE, FALSE, TRUE, FALSE, TRUE)]
# [1] "a" "c" "e"

Naming vector elements

Giving each element a name turns a vector into a simple lookup table. Named vectors let you retrieve values by key rather than by position, making your code self-documenting. Names persist through most operations: subsetting, arithmetic, and sorting all preserve element names. This pattern is used throughout R, from named function arguments to labeled model coefficients:

# Create named vector
scores <- c(Alice = 95, Bob = 87, Charlie = 92)
scores
#    Alice      Bob Charlie 
#       95       87       92 

# Access by name
scores["Alice"]
# Alice 
#     95

names(scores)
# [1] "Alice"   "Bob"     "Charlie"

Atomic vector types

R has six atomic vector types: logical (TRUE/FALSE), integer (1L, 2L), double (1.5, 2.7), complex (1+2i), character ("text"), and raw (bytes). All elements of an atomic vector share the same type. typeof() returns the underlying type; class() returns the higher-level class.

Type coercion

When operations involve mixed types, R coerces to the most flexible type: logical < integer < double < complex < character. c(TRUE, 1L, 2.5) coerces to double: c(1.0, 1.0, 2.5). c(1, "a") coerces to character: c("1", "a"). This implicit coercion is powerful but can cause unexpected results when combining vectors of different types.

Vectorized operations

R operations apply to entire vectors at once. x + 1 adds 1 to every element. x > 0 returns a logical vector. sum(x > 0) counts positive elements. Vectorized operations are implemented in C and are orders of magnitude faster than explicit loops over elements. Write vectorized R code by operating on entire vectors rather than iterating with for.

Vector recycling

When operating on vectors of different lengths, R recycles the shorter vector. c(1, 2, 3, 4) + c(10, 20) returns c(11, 22, 13, 24), the pair c(10, 20) is recycled twice. If the longer vector’s length is not a multiple of the shorter’s, R issues a warning. Recycling is useful for adding a scalar to a vector (x + 1), but accidental recycling with longer vectors causes bugs.

Named vectors

Vectors can have names: x <- c(a = 1, b = 2, c = 3) or names(x) <- c("a", "b", "c"). Access by name: x["a"] returns 1. Names survive most operations: x * 2 keeps names. setNames(x, new_names) creates a named vector from an existing one. Named vectors are commonly used for lookup tables and for labeling results.

Missing values

NA is R’s missing value. Each atomic type has its own typed NA: NA_integer_, NA_real_, NA_character_, NA_complex_. NA itself has type logical and is coerced to the vector’s type when included in a vector. is.na(x) detects missing values. na.omit(x) removes them. Operations on NA return NA: 1 + NA = NA. Use mean(x, na.rm = TRUE) to ignore missing values in calculations.

Subsetting with logical vectors

x[x > 0] returns elements greater than zero. x[!is.na(x)] removes missing values. x[c(TRUE, FALSE, TRUE)] selects alternating elements. Logical subsetting returns a vector the same type as x. Negative integer indexing excludes: x[-1] drops the first element. For data frames, df[df$age > 30, ] filters rows, note the trailing comma to keep all columns.

Common type conversions

as.numeric("3.14") converts a string to a number (returns NA with a warning if conversion fails). as.integer(3.7) truncates to 3. as.character(42) converts to “42”. as.logical(1) converts to TRUE; as.logical(0) to FALSE. as.logical("TRUE") works; as.logical("yes") returns NA. Check for conversion failures by testing is.na(as.numeric(x)).

Atomic vector operations

Vectors are R’s fundamental data structure. All operations in base R are vectorized, they apply to all elements simultaneously. x + 1 adds 1 to every element of x. x > 0 compares every element to 0. sqrt(x) computes the square root of every element. This eliminates most loops.

Recycling: when two vectors in an operation have different lengths, R repeats the shorter vector to match the longer. c(1, 2, 3, 4) + c(10, 20) gives c(11, 22, 13, 24), the vector c(10, 20) is recycled. This is convenient for scalars (length 1 recycles to any length) but can silently produce wrong results when the lengths are different by accident. R warns only when the longer vector is not a multiple of the shorter.

Named vectors: x <- c(a = 1, b = 2, c = 3) creates a named vector. names(x) returns the names. x["b"] retrieves by name. x[c("a", "c")] retrieves multiple by name. Named vectors are useful for lookup tables: lookup <- c(M = "Male", F = "Female"); lookup[gender_codes] translates codes to labels.

Numeric types and precision

R has two numeric types: integer (32-bit) and double (64-bit floating-point). Numeric literals are doubles by default; append L for integer: 1L, 100L. is.double(1) is TRUE; is.integer(1L) is TRUE.

Floating-point arithmetic introduces rounding errors. 0.1 + 0.2 == 0.3 is FALSE in R (and most languages). Use all.equal(0.1 + 0.2, 0.3) for approximate equality tests. round(x, digits = 10) can eliminate insignificant floating-point noise before comparison.

Integer overflow: base R integers are 32-bit, with maximum value 2^31 - 1 = 2,147,483,647. 2147483647L + 1L overflows to NA with a warning. Use as.numeric() to coerce to double before large computations.

Logical vectors and boolean algebra

Logical vectors contain TRUE, FALSE, and NA. They behave as 0/1 in arithmetic: sum(x > 0) counts positive values; mean(is.na(x)) gives the fraction missing.

& and | are vectorized (element-wise); && and || are scalar (single-value, short-circuit). Use & and | for filtering data; use && and || in if statements. Passing a vector to && or || uses only the first element.

any(x > 0) is TRUE if any element is positive. all(x > 0) is TRUE if all elements are positive. Both handle NA: any(c(TRUE, NA)) is TRUE (one TRUE is enough); any(c(FALSE, NA)) is NA (might be TRUE if the NA were TRUE). Pass na.rm = TRUE to handle this in most functions.

Factor details

Factors are integers with a levels attribute. Under the hood, factor(c("a", "b", "a")) stores c(1L, 2L, 1L) with attribute levels = c("a", "b"). This integer encoding is memory-efficient and enables ordered comparison.

levels(f) returns the level labels in order. nlevels(f) returns the count. as.integer(f) returns the underlying codes (1-based). table(f) counts each level, including empty levels. as.character(f) converts back to character labels.

Factor levels can be specified explicitly: factor(x, levels = c("high", "medium", "low")) sets a custom order. factor(x, levels = c("high", "medium", "low"), ordered = TRUE) creates an ordered factor where "low" < "medium" < "high". Ordered factors interact differently with <, >, and model contrasts.

Special values

NA (not available) represents missing data. Propagates through most operations: 1 + NA is NA. is.na(x) detects NAs. Different types have type-specific NA variants: NA_integer_, NA_real_, NA_character_, NA_complex_.

NULL represents the absence of a value. length(NULL) is 0. c(1, NULL, 3) drops the NULL: c(1, 3). NULL removes list elements: lst[["key"]] <- NULL deletes the key. is.null(x) tests for NULL.

Inf and -Inf result from division by zero and overflow. NaN (not a number) results from 0/0 and undefined operations. is.infinite(x) and is.nan(x) detect these. is.finite(x) is TRUE only for real, non-NA, non-infinite, non-NaN numbers — the strictest test for “a valid numeric value.”

Summary

  • Vectors are R’s fundamental data structure: collections of values of the same type
  • The four main atomic types are: numeric, integer, character, and logical
  • Use c(), seq(), and rep() to create vectors
  • Vectorized operations work on entire vectors, making R efficient
  • Type checking: is.*() functions; conversion: as.*() functions
  • R coerces types implicitly when needed: understand the hierarchy

Next steps

Now that you understand vectors, continue with the next tutorial in the r-fundamentals series: Data Frames and Tibbles, where you’ll learn how to work with rectangular data.

See also