Module 2: Basic Programming Operations

Arithmetic, data types, vectors, functions, and control flow in R.

Learning objectives — This module will introduce how to:

  • Perform arithmetic and logical operations
  • Work with the core R data types: numeric, character, logical, factor, and date
  • Create, index, and modify vectors
  • Understand and use lists and data frames
  • Write and call your own functions
  • Use if/else statements and for loops

2.1 Arithmetic Operations

R works as a calculator straight out of the box:

# Basic arithmetic
10 + 3        # addition
10 - 3        # subtraction
10 * 3        # multiplication
10 / 3        # division
10 ^ 2        # exponentiation (also: 10**2)

# Order of operations follows standard mathematics
2 + 3 * 4     # 14, not 20
(2 + 3) * 4   # 20

Useful mathematical functions:

abs(-7)          # absolute value: 7
sqrt(81)         # square root: 9
log(100)         # natural log
log10(100)       # base-10 log: 2
exp(1)           # e^1: 2.718...
round(3.1415, 2) # round to 2 decimal places: 3.14
ceiling(2.1)     # round up: 3
floor(2.9)       # round down: 2

2.2 Variables and Assignment

# Assign with <- (preferred) or =
whale_count    <- 47
survey_date    <- "2024-07-15"
is_offshore    <- TRUE

# Print a variable by typing its name or using print()
whale_count
print(survey_date)

# Update a variable
whale_count <- whale_count + 3   # now 50

# Remove a variable
rm(whale_count)

# See all variables in your environment
ls()

Naming rules:

  • Use lowercase with underscores: whale_count, sea_state
  • Names are case-sensitive: Count ≠ count
  • Cannot start with a number: 1survey ✗
  • Avoid names of built-in functions: mean, data, c — technically allowed but confusing

2.3 Data Types

Use the class() function to reveal the data type of a particular object.

# Numeric (double) — decimal numbers
depth_m <- 120.5
class(depth_m)        # "numeric"

# Integer — whole numbers (rarely need to specify)
n_calves <- 3L
class(n_calves)       # "integer"

# Character (string) — text, always in quotes
species <- "Megaptera novaeangliae"
class(species)        # "character"

# Logical — TRUE or FALSE only
sighted <- TRUE
class(sighted)        # "logical"

# Factor — categorical variable with fixed levels
sea_state <- factor("calm", levels = c("calm", "choppy", "rough"))
class(sea_state)      # "factor"
levels(sea_state)     # "calm" "choppy" "rough"

# Check and convert types
is.numeric(depth_m)        # TRUE
as.character(depth_m)      # "120.5"
as.numeric("42")           # 42
as.logical(0)              # FALSE (0 = FALSE, anything else = TRUE)

Missing values

# NA represents a missing value — it is contagious
x <- NA
is.na(x)              # TRUE
x + 10                # NA (not 10!)
sum(c(1, 2, NA))      # NA
sum(c(1, 2, NA), na.rm = TRUE)  # 3 — remove NAs before computing

2.4 Vectors

A vector is the fundamental data structure in R — an ordered sequence of values all of the same type.

# Create with c() — "combine"
depths    <- c(50, 120, 85, 200, 45)
species   <- c("humpback", "fin", "humpback", "blue", "minke")
sightings <- c(TRUE, FALSE, TRUE, TRUE, FALSE)

# Length
length(depths)     # 5

# Arithmetic on vectors is element-wise
depths * 3.28084   # convert metres to feet
depths - mean(depths)  # centre the values

# Common summary functions
mean(depths)
median(depths)
sd(depths)         # standard deviation
var(depths)        # variance
min(depths)
max(depths)
range(depths)      # c(min, max)
sum(depths)

Indexing vectors

# Use square brackets — R is 1-indexed (first element is [1])
depths[1]          # 50 — first element
depths[3]          # 85 — third element
depths[c(1, 3)]    # 50 85 — first and third
depths[-2]         # all except second
depths[2:4]        # elements 2, 3, 4

# Logical indexing
depths[depths > 100]    # 120 200 — values greater than 100
species[sightings]      # "humpback" "humpback" "blue" — where TRUE

Vector operations and recycling

a <- c(1, 2, 3, 4)
b <- c(10, 20)

a + b   # b is recycled: c(11, 22, 13, 24)
        # works cleanly when longer is a multiple of shorter

2.5 Logical Operators

# Comparison operators — return TRUE or FALSE
5 > 3          # TRUE
5 == 5         # TRUE  (== for equality, not =)
5 != 3         # TRUE  (not equal)
5 >= 5         # TRUE
5 < 3          # FALSE

# Boolean operators
TRUE & FALSE   # AND: FALSE
TRUE | FALSE   # OR:  TRUE
!TRUE          # NOT: FALSE

# Vectorised versions (element-wise)
c(1,2,3) > 2                          # FALSE FALSE TRUE
c(TRUE,FALSE,TRUE) & c(TRUE,TRUE,FALSE)  # TRUE FALSE FALSE

# Useful helpers
4 %in% c(1, 2, 3, 4, 5)   # TRUE — is 4 in that vector?
any(depths > 150)           # TRUE — at least one?
all(depths > 0)             # TRUE — all positive?

2.6 Lists and Data Frames

Lists

Lists can hold elements of different types, including other lists:

survey <- list(
  id       = "S001",
  date     = "2024-07-15",
  n_whales = 4,
  species  = c("humpback", "fin")
)

# Access list elements
survey$id              # "S001"
survey[["n_whales"]]   # 4
survey[[3]]            # 4 (by position)

Data frames

A data frame is a table — a list of equal-length vectors. It is the most important structure for data analysis in R.

# Create a small data frame manually
whale_df <- data.frame(
  survey_id = c("S001", "S002", "S003"),
  species   = c("humpback", "fin", "blue"),
  depth_m   = c(50, 120, 200),
  sighted   = c(TRUE, TRUE, FALSE),
  stringsAsFactors = FALSE
)

whale_df           # print the whole table
nrow(whale_df)     # 3 rows
ncol(whale_df)     # 4 columns
dim(whale_df)      # c(3, 4)
names(whale_df)    # column names
str(whale_df)      # compact structure overview
summary(whale_df)  # descriptive statistics per column

# Access columns
whale_df$species           # character vector
whale_df[["species"]]      # same thing

# Access rows and cells [row, column]
whale_df[1, ]              # first row, all columns
whale_df[, 3]              # all rows, third column
whale_df[2, "species"]     # "fin"

# Filter rows
whale_df[whale_df$depth_m > 100, ]

2.7 Control Flow

if / else

depth <- 150

if (depth > 200) {
  print("Deep dive")
} else if (depth > 100) {
  print("Mid-water")
} else {
  print("Shallow")
}
# "Mid-water"

# Compact version: ifelse() works on vectors
ifelse(depths > 100, "deep", "shallow")
# "shallow" "deep" "shallow" "deep" "shallow"

for loops

species_list <- c("humpback", "fin", "blue", "minke")

for (sp in species_list) {
  cat("Processing sightings for:", sp, "\n")
}

# Loop over indices when you need position
for (i in seq_along(species_list)) {
  cat(i, "-", species_list[i], "\n")
}

while loops

searches <- 0
whales_found <- 0

while (whales_found < 5) {
  searches <- searches + 1
  whales_found <- whales_found + sample(0:3, 1)  # 0-3 random whales
}

cat("Found 5+ whales after", searches, "searches\n")
Tip

In R, for loops are slower than in other programming languages so are often replaced by vectorised operations or apply-family functions (sapply, lapply, vapply) which are faster.


2.8 Writing Functions

Functions package reusable code. They take inputs (arguments) and return an output.

# Basic syntax
function_name <- function(argument1, argument2 = default_value) {
  # body — code that runs
  result <- argument1 + argument2
  return(result)          # explicitly return a value
  # OR: just put the value on the last line — R returns it implicitly
}

# A real example: Beaufort sea state label
beaufort_label <- function(scale) {
  if (scale == 0) return("Glassy")
  if (scale <= 2) return("Calm")
  if (scale <= 4) return("Moderate")
  if (scale <= 6) return("Rough")
  return("Very rough")
}

beaufort_label(0)   # "Glassy"
beaufort_label(3)   # "Moderate"
beaufort_label(7)   # "Very rough"
# Function with multiple arguments and a default
sighting_rate <- function(n_whales, hours_searched, per_hours = 1) {
  rate <- (n_whales / hours_searched) * per_hours
  return(round(rate, 2))
}

sighting_rate(12, 8)           # 1.5 whales/hour
sighting_rate(12, 8, per_hours = 100)  # 150 whales/100 hours


2.9 The Pipe |>

As your code grows, you often need to apply several operations in sequence. One approach is to nest function calls, but this quickly becomes hard to read:

# Nested — read from the inside out
round(mean(c(12, 7, 3, NA, 5), na.rm = TRUE), 1)

The pipe operator |> solves this by passing the result of the left-hand side as the first argument of the right-hand side, letting you chain steps left-to-right like a recipe:

# Piped — read top to bottom
c(12, 7, 3, NA, 5) |>
  mean(na.rm = TRUE) |>
  round(1)

Both produce the same result (6.8), but the piped version reads in the order the operations actually happen.

Tip

Shortcut: Ctrl+Shift+M (Windows) / Cmd+Shift+M (macOS) inserts |> in RStudio.
Go to Tools → Global Options → Code and tick “Use native pipe operator” if the shortcut produces %>% instead.

A more realistic example — filter a vector, compute its mean, then round:

depths <- c(50, 120, 85, NA, 200, 45, 310)

depths |>
  na.omit() |>       # remove NA
  mean() |>          # arithmetic mean
  round(1)           # round to 1 decimal place
# 135

You will use |> constantly from Module 3 onwards, where it chains dplyr data-manipulation steps together. For now, the key idea is: the result of each line flows into the next.

Note

You may also see %>% in older R code and Stack Overflow answers — this is the magrittr pipe, loaded with the tidyverse. It behaves identically in almost all cases. The native |> (available since R 4.1) is preferred in new code as it requires no packages.

2.10 Dates

library(lubridate)   # part of tidyverse, makes dates easy

# Parse dates from strings using built-in functions
d <- ymd("2024-07-15")         # year-month-day
d2 <- dmy("15/07/2024")        # day-month-year
d3 <- mdy("July 15, 2024")     # month-day-year

# Parse dates from strings manually
d4 <- as.Date("2025 Apr 03", format = "%Y %b %d") # see ?strptime for details

class(d)   # "Date"

# Extract components
year(d)     # 2024
month(d)    # 7
day(d)      # 15
wday(d, label = TRUE)   # Mon, Tue, ...

# Arithmetic
d + 30         # 30 days later
d2 - d         # Time difference in days

2.11 POSIX Date-Times

A plain Date object stores only a calendar day. When you need sub-day precision — R uses the POSIX datetime classes.

Creating POSIXct objects

# From a string — always specify the format and time zone
sighting_time <- as.POSIXct("2024-07-15 09:32:47",
                             format = "%Y-%m-%d %H:%M:%S",
                             tz = "America/New_York")

class(sighting_time)   # "POSIXct" "POSIXt"
sighting_time          # 2024-07-15 09:32:47 EDT

# lubridate helpers are easier for common formats
sighting_time <- ymd_hms("2024-07-15 09:32:47", tz = "America/New_York")
ymd_hm("2024-07-15 09:32", tz = "UTC")   # no seconds

The Unix epoch

POSIXct is stored internally as the number of seconds elapsed since 1970-01-01 00:00:00 UTC (the Unix epoch). You can inspect and use this directly:

as.numeric(sighting_time)   # e.g. 1721047967 — seconds since epoch

# Current time
now_utc <- Sys.time()              # POSIXct in your system tz
as.numeric(now_utc)                # seconds since epoch

# Recreate a POSIXct from a raw epoch value
as.POSIXct(1721047967, origin = "1970-01-01", tz = "UTC")

Extracting components

# Base R
format(sighting_time, "%H:%M:%S")    # "09:32:47"
format(sighting_time, "%A %d %B %Y") # "Monday 15 July 2024"

# lubridate (cleaner)
hour(sighting_time)    # 9
minute(sighting_time)  # 32
second(sighting_time)  # 47
date(sighting_time)    # 2024-07-15  (strips time -> plain Date)

Arithmetic and durations

start <- ymd_hms("2024-07-15 06:00:00", tz = "UTC")
end   <- ymd_hms("2024-07-15 14:45:30", tz = "UTC")

# Difference — returns a difftime object
elapsed <- end - start
elapsed                        # Time difference of 8.758333 hours
as.numeric(elapsed, "hours")   # 8.758333
as.numeric(elapsed, "secs")    # 31530

# Add fixed durations with lubridate
start + hours(2)               # 08:00:00
start + minutes(90)            # 07:30:00
start + seconds(3600)         # 07:00:00  (duration in seconds)

Time zones

# Inspect the time zone of an object
tz(sighting_time)   # "America/New_York"

# Convert to a different time zone (same instant, different clock time)
with_tz(sighting_time, "UTC")           # 2024-07-15 13:32:47 UTC
with_tz(sighting_time, "Europe/London") # accounts for BST offset

# Force a new time zone WITHOUT converting (use with caution!)
force_tz(sighting_time, "UTC")          # relabels as UTC, keeps clock time

# List all available time zone names
OlsonNames()   # ~600 entries
Tip

We typically operate in eastern time US/Eastern, or Atlantic time Canada/Atlantic

Note

It is best practice to store timestamps in UTC in your data files and convert to local time only for display. This avoids daylight-saving ambiguity and makes cross-timezone comparisons straightforward.

2.12 Project Organization

A few quick notes on project structure and organization before we move ahead. It’s important to keep your project organized and documented so it is reproducible by you or others now or in the future. There are many ways to about this, but typically each project should have the following basic directory structure:

example_project/
├── R/                    # R scripts
├── data/raw/             # raw data files that are NEVER edited  
├── data/processed/       # processed data files that are created from raw data using R scripts  
├── figures/              # figures created with R scripts  
├── example_project.Rproj # R project file
└── readme.md             # a readme file describing the project goal, status and workflow  

This is flexible, but there are a few key points: * Always have a raw data directory that you NEVER edit - this allows you and others to reproduce your analyses. * A readme file is very, very helpful for providing notes about the project that allow you and others to execute the analysis again * An R Project file allows you to quickly and conveniently switch between different projects