Foundations: vectors to tables

The R Statistical Language · Session 1

1 What is R, and why use it?

1.1 Why R

  • A programming language purpose-built for statistics and data science.
  • Open source with a fast-growing ecosystem.
  • Packages for (almost) everything:
    • data processing and cleaning, visualization, statistical modelling;
    • interactive web apps ({shiny});
    • typesetting, articles, slides (Quarto!).
  • A high-level language: it shields you from low-level technicalities so you can focus on the analysis.

Press C any time. A code popover (the </> button bottom-right) reveals the source behind any slide: read ahead, or ignore it for now.

1.2 The R ecosystem

R centres on two complementary toolkits:

Base R: ships with every installation, so it needs zero setup (works offline, even in locked-down environments). Functions like mean(), plot(), c(), [. Powerful and always available, but its ergonomics date back decades.

The tidyverse: an opinionated collection of packages (dplyr, ggplot2, tidyr, readr) sharing one grammar. Install once, then library(tidyverse) loads them all. The default toolkit for modern data work.

In this course we start from base R so you understand the foundations, then lean on the tidyverse for day-to-day work.

1.3 Alternatives to R

R isn’t the only player. Knowing the landscape helps you choose the right tool.

Stata / SAS / SPSS: proprietary statistical software. Easy for standard methods, certified in regulated settings (health, insurance), but not good programming languages.

Python: a general-purpose language, standard in industry. Not built for statistics, but extremely active in machine learning. More boilerplate for everyday data work; you’ll meet it again in the Reproducible Workflows course.

Julia: a young, fast numerical language. Growing in machine learning, less focused on out-of-the-box data wrangling.

R vs the rest? R gives you the richest statistics ecosystem and the least friction between idea and analysis: at the cost of some “different from other languages” idioms.

1.4 Resources to learn R

Keep these bookmarked:

R for Data Science (2nd ed.): the tidyverse workflow, free online: https://r4ds.hadley.nz

An Introduction to R: the base-R manual: https://cran.r-project.org/doc/manuals/r-release/R-intro.pdf

Advanced R: for later, to become a good R programmer: https://adv-r.hadley.nz

Cheatsheets (1-page package summaries): https://posit.co/resources/cheatsheets/

Hint. The single most useful habit when you’re stuck: read the help page, start at the Examples section at the bottom, then adapt. Help is one keystroke away (?function).

1.5 A quick orientation tour

1Foundations
2Explore your data
3Repeat & package

This session’s map:

  • the console and your first R script;
  • R as a calculator, functions, and assignment;
  • data types: numeric, integer, character, logical;
  • vectors: the workhorse data structure, and how to subset them;
  • lists: containers of anything;
  • data frames & tibbles: the data-table you’ll analyse;
  • reading & writing data files.

Each block hands off to a short hands-on lab: slides get you running, lab makes it stick.

2 Setting up your environment

2.1 Your toolbox

Four installations get you a full data-science environment:

R: the language. Installed from CRAN.

Positron: the IDE we use in this course (a data-science-focused editor running both R and Python).

Quarto: the publishing system. We’ll use it for reproducible reports and (later) your portfolio.

The tidyverse: the collection of R packages underpinning the modern workflow.

2.2 Install & check your setup

Follow the step-by-step guide now, then verify your install runs:

https://astamm.github.io/r-ensai → Setup Tab

R version check

R.version.string   # e.g. "R version 4.6.1"

Terminal: quarto --version

quarto --version     # e.g. 1.10.x

Live block (≈15 min). Install R, Positron, Quarto, then install.packages("tidyverse"); confirm with library(tidyverse). Already set up? Verify R.version.string / quarto --version, then help a neighbour. If a check errors, fix it now: broken installs slow every lab.

3 The console, scripts and R as a calculator

3.1 Console vs script

Every R statement runs in one of two places:

The Console: type code, see results immediately. Great for quick experiments (2 + 3), inspecting objects (str(x)), and trying one-liners. But what you type is not saved: nothing remains for tomorrow.

An R Script (.R file): your code lives in a file, is persistent, and can be re-run any time (source("my_script.R"), or Cmd/Ctrl+Enter to run the current line). You also get syntax highlighting and autocomplete.

For real work, write in an R script and run it line by line up into the console. The console is for experiments; the script is your reproducible record.

3.2 The R console

  • When the last line shows the prompt >, R is ready for your instruction.
  • Types after >:
    • library(ggplot2) loads a package;
    • x <- 2 assigns a value to a name;
    • mean(x) runs a function and prints its result.

The R console

R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages.

>

Note. If you don’t see > (e.g. a + continuation), R is mid-expression (finish it or press CTRL + C to cancel).

3.3 R as a calculator

Calculator

2 + 3
[1] 5
log(2)
[1] 0.6931472
2^10
[1] 1024

Basic operators: + - / * ^, %*% (matrix multiply), | (OR), & (AND), ! (NOT). Maths functions: log(), exp(), sin(), sqrt(), …

Find the right function

?Math       # help on math functions
?Ops        # help on arithmetic/comparison operators
?Summary    # sum, mean, min, max, ...

3.4 Using functions (1/2)

A function takes arguments and returns a value (just like a mathematical function):

Positional vs named

sqrt(4)        # positional
[1] 2
sqrt(x = 4)    # named
[1] 2

Prefer pkg::function(parameter = argument)

The shortcut function(argument) is risky: two loaded packages can export a same-named function, and dropping parameter names fills arguments by position, so a reordered or missing one fails deep inside the call with a cryptic error. Spelling out package and names keeps calls unambiguous and self-documenting.

Arguments vs parameters. x is the parameter (the name in the function’s definition); 4 is the argument (the value you supply). Some arguments are mandatory, others optional (they have a default value); name the non-obvious ones.

3.5 Using functions (2/2)

Help pages

Access help with ?, the fastest shortcut to a function’s manual.

Find a function

?solve     # solve linear systems

How to read a help page (top → bottom):

  • Usage: the function signature and defaults;
  • Arguments: what each parameter means;
  • Value: what the function returns;
  • Examples: copy-paste these first; the fastest way to learn.

Habit. “Help → Examples → adapt” solves most “how do I use this?” moments.

3.6 Storing values: assignment

assignment.R

x <- 2         # assign 2 to the name x
x
[1] 2
(x <- 3)       # assign AND print in one go
[1] 3
x <- "Hello"   # now x holds a character string
x
[1] "Hello"

Keyboard shortcut

You can insert <- with:

  • Alt + - (Windows),
  • Option + - (Mac).

Use the assignment operator <- to bind a value to an object so you can reuse it later.

<- vs =

You can use = to assign, but don’t: = is also used to set an argument inside a function call, so its meaning depends on context. <- is unambiguous.

3.7 Class of an object

Every object has a class: the type of data it stores. Check it with class():

The class() function

class(1)
[1] "numeric"
class(1L)       # the L marks an integer literal
[1] "integer"
class("Hello")
[1] "character"
class(TRUE)
[1] "logical"

Why it matters. R dispatches to appropriate behaviour based on class: a data.frame prints differently from a list, a factor plots differently from a character. Knowing an object’s class tells you what you can safely do with it.

3.8 Basic data types

Numeric

x <- 2
x
[1] 2
class(x)
[1] "numeric"

Character

x <- "Hello"
x
[1] "Hello"
class(x)
[1] "character"

Logical

x <- TRUE
x
[1] TRUE
class(x)
[1] "logical"

Integer

x <- 2L         # the L marks an integer literal
x
[1] 2
class(x)
[1] "integer"

Complex

x <- 2 + 3i     # a native, built-in data type
x
[1] 2+3i
class(x)
[1] "complex"
Re(x); Im(x); Mod(x); Conj(x)
[1] 2
[1] 3
[1] 3.605551
[1] 2-3i

3.9 Special values

R has its own vocabulary for edge cases:

NA: missing (“Not Available”). A hole in your data. x <- NA.

NaN: Not a Number, e.g. 0/0. A kind of missing numeric.

Inf: Infinity, e.g. 1/0. Overflow or divergence.

NULL: empty/absent. Means “no value here at all”, different from NA.

Anticipate missingness. NA is contagious: most functions on data containing NA return NA unless you say na.rm = TRUE. We’ll meet this again (and handle it properly) in the data-cleaning lab.

3.10 Your turn: first steps

Your turn (≈15 min): work on your own, then compare with a neighbour. Open a new R script in Positron (File → New File → R Script) and:

  1. Compute the mean, variance and standard deviation of c(2, 4, 6, 8, 10).
  2. Assign the result of log(100) to a variable named log_100, then print it.
  3. What is the class of NA, TRUE, 1L, "one"? Check each with class().
  4. Create an object w <- 3 + 4i. Compute its modulus and argument.

After ~8 minutes, swap machines or compare your answers with a neighbour for the last 5.

4 Vectors

4.1 Atomic vectors

Use c() (concatenate) to build a vector, and values must share a type:

Build vectors with c()

x <- c(1, 2, 3); class(x)
[1] "numeric"
y <- c("a", "b", "c"); class(y)
[1] "character"
z <- c(TRUE, FALSE, TRUE); class(z)
[1] "logical"

A sequence is the shortcut for runs of integers:

Colon & seq()

seq <- 1:10          # colon: 1 to 10 with step 1
seq
 [1]  1  2  3  4  5  6  7  8  9 10
seq(1, 5, by = 2)    # seq(): custom step
[1] 1 3 5

Shortcuts

seq_len(5)           # 1:n, handy in loops
[1] 1 2 3 4 5
seq_along(letters)   # 1:length(x), matches a vector
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26

Atomic vectors

An atomic vector can only hold values of a single data type. Mix types and R will coerce (e.g. c(1, "a") becomes character), often silently, so watch out.

4.2 Operations on atomic vectors: vectorization

The same operation applies to every element at once:

Arithmetic

height <- c(168, 177, 177, 177, 178, 172, 165, 171, 178, 170)
weight <- c(88, 72, 85, 52, 71, 69, 61, 61, 51, 75)
bmi <- weight / ((height / 100)^2)   # one op -> every element
bmi
 [1] 31.17914 22.98190 27.13141 16.59804 22.40879 23.32342 22.40588 20.86112
 [9] 16.09645 25.95156
mean(bmi)
[1] 22.89377
var(bmi)
[1] 20.73245

Negate

!c(TRUE, FALSE, TRUE)
[1] FALSE  TRUE FALSE

Compare

c("Hello", "World") == "Hello"
[1]  TRUE FALSE

4.3 Generating data from distributions

R ships with families of probability functions (package {stats}): density, probability (cdf), quantile and random:

The normal distribution family

mu <- 3; sigma <- 2
dnorm(0, mean = mu, sd = sigma)
[1] 0.0647588
pnorm(0, mean = mu, sd = sigma)
[1] 0.0668072
qnorm(0.5, mean = mu, sd = sigma)
[1] 3
rnorm(5, mean = mu, sd = sigma)   # random — values differ each run
[1] 2.978844 4.108082 1.773639 6.634199 4.357477

Extendable. The same 4-letter pattern (r*, d*, p*, q*) covers every distribution in ?Distributions, a big engine for simulation.

4.4 Your turn: vectors

Your turn (≈12 min): work alone, then compare with a neighbour.

  1. Build a vector of temperatures in °C using rexp() (exponential), then convert to °F with the vectorized formula \(F = C \times \frac{9}{5} + 32\).
  2. Sample 100 values from a Weibull distribution (?Weibull) with shape 2, scale 1. Then compute its sum, mean, var, sd, median, quantiles, min, max. Hint: help.search("quantile") finds the function.
  3. Using the height/weight vectors, compute the correlation between them (?cor).

4.5 Subsetting atomic vectors

There are four ways to pick elements with [, grouped into the three kinds below, plus the mental model that unifies them:

Positive

x <- c("one", "two", "three", "four", "five")
x[c(3, 2, 5)]   # keep by position
[1] "three" "two"   "five" 

Negative

x <- c("one", "two", "three", "four", "five")
x[c(-1, -3, -5)]   # drop by position, equivalent to
[1] "two"  "four"
x[-c(1, 3, 5)]
[1] "two"  "four"

Logical

x <- c(10, 3, NA, 5, 8, 1, NA)
x[!is.na(x)]
[1] 10  3  5  8  1
x[x %% 2 == 0]
[1] 10 NA  8 NA

Character

x <- c(abc = 1, def = 2, xyz = 5)
x[c("xyz", "def")]
xyz def 
  5   2 

x[selector] returns elements where the selector is TRUE: the logical form.

mental model

x <- c(5, 1, NA, 8, 2)
x[x > 3]        # "x, where x is greater than 3"
[1]  5 NA  8
x[!is.na(x)]    # "x, where x is not missing"
[1] 5 1 8 2

NA trap. x[x %% 2 == 0] keeps NAs too: NA %% 2 == 0 is NA.

4.6 Recycling

When two vectors differ in length, R recycles the shorter one to match the longer:

recycling

c(1, 2, 3, 4) + c(10, 20)
[1] 11 22 13 24

equivalent to

c(1, 2, 3, 4) + c(10, 20, 10, 20)
[1] 11 22 13 24

Stretch / gotcha

Recycling is powerful but silent; it can zip wrongly-sized vectors together without a warning if the longer length isn’t a multiple. c(1,2,3) == c(1,2) gives TRUE TRUE FALSE with a warning; always sanity-check lengths after mixing.

4.7 Your turn: subsetting

Your turn (≈10 min): work alone, then compare with a neighbour.

  1. Build 1:100 three ways (1:100, seq(1, 100), seq_len(100)). Then subset to the odd numbers, those divisible by 3, those divisible by 3 or 5, and those divisible by 3 or 5 but not both.
  2. Create a vector of length 5 and one of length 2. Add them. What happens, and why?
  3. For x <- c("one","two","three","four","five"), what is x[c(1,1,5,5,5,2)]?

5 Lists

5.1 Lists: containers of anything

A list can mix types

x <- list(1, "a", TRUE, c(1, 2, 3))
x
[[1]]
[1] 1

[[2]]
[1] "a"

[[3]]
[1] TRUE

[[4]]
[1] 1 2 3

The object class is “list”

x <- list(1, "a", TRUE, c(1, 2, 3))
class(x)
[1] "list"

While an atomic vector needs one type, a list can hold objects of different types.

Operations don’t apply to a list

Functions like mean and + don’t work on a list; apply them to each element instead.

5.2 Applying a function to every element

The *apply family does “for each element, run this function”:

lapply / sapply

my_list <- list(var1 = rnorm(n = 20L), var2 = runif(n = 12L), var3 = rbinom(n = 42L, size = 1, prob = 0.5))
lapply(my_list, FUN = mean)   # returns a list
$var1
[1] 0.06836996

$var2
[1] 0.3685767

$var3
[1] 0.452381
sapply(my_list, FUN = mean)   # tries to simplify to an atomic vector
      var1       var2       var3 
0.06836996 0.36857672 0.45238095 

vapply for safety. vapply(my_list, mean, FUN.VALUE = numeric(1)) promises the output type up front and fails loudly: prefer it in code you ship.

5.3 The [, [[, $ subsetting

[ (a sub-list)

x <- list(a = 1, b = "a", c = TRUE)
x["a"]
$a
[1] 1
class(x["a"])
[1] "list"

[[ (the element)

x <- list(a = 1, b = "a", c = TRUE)
x[[1]]            # by position
[1] 1
class(x[[1]])
[1] "numeric"
x[["a"]]          # by name
[1] 1
class(x[["a"]])
[1] "numeric"

$ (by name)

x <- list(a = 1, b = "a", c = TRUE)
x$a
[1] 1
class(x$a)
[1] "numeric"

Named selection

x <- list(a = 1, b = "a", c = TRUE)
x[c("a", "b")]
$a
[1] 1

$b
[1] "a"
class(x[c("a", "b")])
[1] "list"

5.4 The pepper shaker

Figure 27.1 in R for Data Science: list subsetting at a glance. [ gives you a shaker containing pepper; [[ or $ gives you the pepper.

Read as a metaphor. [ returns a list (the container); [[ and $ reach inside to the single element. Use [ when you want a sub-collection, [[/$ when you want one value.

5.5 Live coding: lists

Live code (≈10 min): I type and narrate at the console; you follow on your own machine. Pause on the [[ vs $ distinction, the most common beginner stumble with lists.

  1. shaker <- list(shape = "round", contents = list("salt", "cayenne")): build a nested list modelling the shaker.
  2. What does shaker[[1]][1] return? What about shaker[[1]][[1]]?
  3. Extend a list: assign to a new name (shaker$lid <- "open"). What happened to the list?
  4. Try shaker$contents[[2]] vs shaker[["contents"]][[2]].

5.6 Your turn: lists

Your turn (≈15 min): work alone, then compare with a neighbour.

  1. Build a list of three vectors of lengths 10, 20, 30. Compute the mean of each with sapply(), then vapply().
  2. Make a list of two length-5 vectors. Use lapply() to double each element; store the result. Hint: lapply() needs a function to apply; a one-liner \(.x) .x * 2 does the doubling.
  3. What happens if you use [[ with an index larger than the list length? With a name that doesn’t exist? (Try it: read the error message.)
  4. Given shaker, what is shaker[[1]][1] vs shaker[[1]][[1]]?
  5. How do you extend a list?

6 Data frames and tibbles

6.1 Data frames

A data frame is a data table: a list of named columns, all of the same length.

iris is a data frame

class(iris)
[1] "data.frame"
head(iris, n = 3L)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa

Beyond & beside base R

  • A tibble (next slide) is the tidyverse’s friendlier data frame: better printing, safer subsetting, interoperable via as_tibble() / as.data.frame().
  • Other data-frame ecosystems: polars (fast, lazy) and data.table (concise, very fast on large data).

6.2 Tibbles: the modern data frame

Defined in the eponymous {tibble} package:

as_tibble()

iris_tbl <- tibble::as_tibble(iris)   # tidyverse already loaded
class(iris_tbl)
[1] "tbl_df"     "tbl"        "data.frame"
head(iris_tbl, n = 3L)
# A tibble: 3 × 5
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
         <dbl>       <dbl>        <dbl>       <dbl> <fct>  
1          5.1         3.5          1.4         0.2 setosa 
2          4.9         3            1.4         0.2 setosa 
3          4.7         3.2          1.3         0.2 setosa 

A tibble is a data frame with friendlier defaults:

  • prints only the size of the data, the first 10 rows, only columns that fit (the rest of them are listed as ...) and their types;
  • column names must be syntactic (safe to reference: snake_case, no spaces, no punctuation except _, no leading numbers);
  • never drops empty rows/columns on subsetting;
  • no silent type conversion.

6.3 data.frame vs tibble

data.frame tibble
Printing full data set first 10 rows
Row names numbers strings only
Column names can be non-syntactic must be syntactic
Subsetting drops empty rows keeps them
Type conversion silent none

Takeaway. Prefer tibbles; they interoperate with data frames via as_tibble() / as.data.frame().

6.4 Subsetting data frames

The [row, col] grid of base R:

Subsetting a data frame

df <- data.frame(x = 1:3, y = c("a", "e", "f"), z = runif(3))
df[1, 2]            # row 1, col 2
[1] "a"
df[, c("x", "y")]   # all rows, cols x & y
  x y
1 1 a
2 2 e
3 3 f
df[df$x > 1, ]      # rows where x > 1  (z values vary each run)
  x y         z
2 2 e 0.3599995
3 3 f 0.8094811

6.5 Subsetting tibbles

Same grid, safer behaviour:

Subsetting a tibble

df <- tibble(x = 1:3, y = c("a", "e", "f"), z = runif(3))
df[1, 2]          # row 1, col 2
# A tibble: 1 × 1
  y    
  <chr>
1 a    
df[df$x > 1, ]    # rows where x > 1
# A tibble: 2 × 3
      x y         z
  <int> <chr> <dbl>
1     2 e     0.343
2     3 f     0.462

Stretch: tibble dropping

Tibbles keep empty rows/columns and never auto-convert, so df[, 1] stays a tibble (base R would coerce to a vector), usually what you want, but a common surprise for base-R newcomers.

6.6 Your turn: data frames

Your turn (≈12 min): work alone, then compare with a neighbour. Refer back to the “four ways to subset a vector”: column selection reuses the same ideas in a [row, col] grid.

  1. Build a data frame with three columns (a numeric vector, a character vector, a logical vector), each with ≥ 5 entries.
  2. Convert it to a tibble.
  3. Extract rows 2–3 of column 1 (subsetting).
  4. Extract all rows of columns 1 and 3.
  5. Extract all rows where the logical column is TRUE.

7 Reading and writing data

7.1 Working directory & relative paths

Every file path in R is resolved relative to your working directory (the folder R is currently “in”), unless you write an absolute path (/Users/you/...).

getwd()        # where is R right now?
list.files()   # what is in that folder?
setwd("...")   # move elsewhere (avoid in scripts!)

Think in terms of projects

Open your course folder as a project and work there: the working directory is that folder, so a relative path like read_csv("data/raw/penguins.csv") just works. A small, predictable layout keeps things tidy:

my-course/
├── data/
│   ├── raw/
│   └── processed/
└── scripts/

Prefer relative paths; they let a script run on any machine with the same folder layout.

7.2 Read rectangular data

The {readr} package reads flat files fast and guesses column types:

readr::read_csv("penguins.csv")     # comma-separated
readr::read_tsv("file.tsv")         # tab-separated
readr::read_table("file.txt")       # space-separated
readr::read_csv2("file.csv")        # European ; decimal comma
readr::read_delim("file.dat", delim = ";")

Also handy:

  • Excel: readxl::read_excel() (via {readxl});
  • SPSS / Stata / SAS: haven::read_spss() / read_stata() / read_sas() (via {haven});
  • JSON: jsonlite::read_json() (via {jsonlite});
  • R’s own binary format: read_rds() / saveRDS().

7.3 Read & write: choosing the right reader

Choosing the right reader

Open the file in a text editor first:

  • What separates values?
  • Is there a header?
  • Are decimals . or ,?

Then pick the reader.

Writing data

{readr}’s writes mirror its reads:

Choosing the right writer

readr::write_csv2(penguins, "penguins_clean.csv") # European ; decimal comma
readr::write_csv(penguins, "penguins_clean.csv")
readr::write_tsv(penguins, "penguins_clean.tsv")

7.4 First steps towards data cleaning

A first data-cleaning pass is a handful of foreseeable chores. We’ll put each to practice in the hands-on that follows:

  • Import: parse the file correctly (right delimiter, header, decimals) with the right reader;
  • Fix wrong guessed types: readr guesses column types, and numeric data arriving as character, or NA where numbers belong, are the usual failures;
  • Decode missing values: real files encode “no data” many ways (NA, empty cells, "", 999, -, ?), and R must be told which values count as missing;
  • Tame non-syntactic names: column names with spaces or accents (e.g. body mass g) are awkward to type; you rename them once, up front.

Each is small and knowable. Knowing they exist before hitting them is half the exercise.

7.5 Your turn: data cleaning

Your turn (≈30 min), pair up: this is the course’s first data-cleaning exercise. Work in pairs, alternating who types.

  1. Download penguins_messy.txt.
  2. Open it in a text editor. How are values separated? Is there a header? Are there missing values, and how do they appear?
  3. Read it into R, choosing the right readr function. Look at the column types: are any wrong?
  4. Fix the column types as needed (numeric vs character vs factor).
  5. Write the cleaned data as penguins_clean.csv.

8 Wrap up

8.1 Three structures, one idea

Build, reach, combine.

8.2 Vectors, lists, data frames

Three structures, one flow.

Vectors

Build them with c(), compute element-wise, and subset with [.

Lists

Hold anything; reach in with [[/$; apply functions with lapply/vapply.

Data frames

Just a list of equal-length vectors, bound as named columns; read with readr.

8.3 Data frames: the workhorse

Data frames power data science.

Data frames

The structure you’ll reach for first: each column is a vector, and the frame is just a list of them. Subset with [/$, reshape with the tidyverse, and read any table with readr.

8.4 Next time

Session 2: explore your data, the tidyverse.

Between sessions: finish lab-2-dataframes.R if needed, and skim R for Data Science, ch. 2–4 for the syntax you’ll reuse all term.