Getting Started with R
Run R from a script rather than a console, load a dataset into a tibble, and learn the two vector behaviours that surprise everyone coming from Python.
R is a language built around the data frame. A table is the primary type, not something a
library adds — which is why the same code cleans a dataset, plots it, fits a model to it, and,
through dbplyr, runs as SQL against a warehouse.
This track covers both halves: the analysis work R is known for (lessons 6-8 — visualisation, exploratory statistics, modelling) and the pipeline work it is quietly good at (lessons 3, 9 and 10 — Parquet, databases, reproducible runs).
Setting up
R --version
Rscript -e 'install.packages(c("tidyverse", "arrow", "DBI", "duckdb"), repos="https://cloud.r-project.org")'
R version 4.5.1 (2026-06-14) -- "Pile of Leaves"
Copyright (C) 2026 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu
* installing *source* package 'tidyverse' ...
* DONE (tidyverse)
* DONE (arrow)
* DONE (DBI)
* DONE (duckdb)
This track runs everything with Rscript, not from an interactive console — pipeline code
has to run unattended, and a script is the unit that gets scheduled.
The first script
# intro.R
library(tidyverse)
orders <- tibble(
order_id = c(1001L, 1002L, 1003L, 1004L, 1005L),
customer_id = c(1L, 2L, 1L, 3L, 2L),
ordered_at = as.Date(c("2026-01-04", "2026-01-05", "2026-01-07",
"2026-01-09", "2026-01-11")),
status = c("completed", "completed", "returned", "completed", "pending"),
amount = c(25.50, 12.00, 40.00, 8.75, 63.20)
)
print(orders)
Rscript intro.R
── Attaching core tidyverse packages ─────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.1 ✔ tibble 3.2.1
✔ lubridate 1.9.3 ✔ tidyr 1.3.1
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
# A tibble: 5 × 5
order_id customer_id ordered_at status amount
<int> <int> <date> <chr> <dbl>
1 1001 1 2026-01-04 completed 25.5
2 1002 2 2026-01-05 completed 12
3 1003 1 2026-01-07 returned 40
4 1004 3 2026-01-09 completed 8.75
5 1005 2 2026-01-11 pending 63.2
The tibble print is doing a lot of work: dimensions on the first line, and a type under every
column name — <int>, <date>, <chr>, <dbl>. Reading types at a glance is how you catch
the classic bug of a numeric column that arrived as text.
Silence the startup banner in scripts:
suppressPackageStartupMessages(library(tidyverse))
Everything is a vector
x <- c(1, 2, 3, 4, 5)
print(x * 2)
print(x + c(10, 20, 30, 40, 50))
print(sqrt(x))
print(sum(x))
print(x[x > 3])
[1] 2 4 6 8 10
[1] 11 22 33 44 55
[1] 1.000000 1.414214 1.732051 2.000000 2.236068
[1] 15
[1] 4 5
No loop anywhere. Operations apply element-wise, which is why R code that looks like it
handles one value usually handles a whole column. [1] at the start of each line is the index
of the first element on that line, not part of the data.
Indexing starts at 1, and negative indices mean exclusion rather than counting backwards:
print(x[1])
print(x[-1])
print(x[c(1, 3)])
print(x[length(x)])
[1] 1
[1] 2 3 4 5
[1] 1 3
[1] 5
x[-1] dropping the first element rather than returning the last is the single most common
error for people arriving from Python.
Recycling, which will bite you
print(c(1, 2, 3, 4) + c(10, 20))
print(c(1, 2, 3) + c(10, 20))
[1] 11 22 13 24
Warning message:
In c(1, 2, 3) + c(10, 20) :
longer object length is not a multiple of shorter object length
[1] 11 22 13
The shorter vector is repeated to match the longer one. When the lengths divide evenly it happens silently — the first line produced a plausible wrong answer with no warning at all. This is worth knowing before it produces a quietly incorrect column.
Missing values propagate
amounts <- c(25.50, NA, 12.00, 8.75)
print(sum(amounts))
print(sum(amounts, na.rm = TRUE))
print(mean(amounts, na.rm = TRUE))
print(is.na(amounts))
print(amounts == NA)
[1] NA
[1] 46.25
[1] 15.41667
[1] FALSE TRUE FALSE FALSE
[1] NA NA NA NA
Two rules. Any arithmetic touching NA returns NA, so a single missing value poisons a
total unless you pass na.rm = TRUE — that explicitness is deliberate, and better than
silently ignoring the gap. And == NA is always NA, never TRUE: use is.na().
Reading a real file
# orders.csv
order_id,customer_id,ordered_at,status,amount
1001,1,2026-01-04,completed,25.50
1002,2,2026-01-05,completed,12.00
1003,1,2026-01-07,returned,40.00
1004,3,2026-01-09,completed,8.75
1005,2,2026-01-11,pending,63.20
1006,9,2026-01-12,completed,19.99
orders <- read_csv("orders.csv", show_col_types = FALSE)
print(orders)
glimpse(orders)
# A tibble: 6 × 5
order_id customer_id ordered_at status amount
<dbl> <dbl> <date> <chr> <dbl>
1 1001 1 2026-01-04 completed 25.5
2 1002 2 2026-01-05 completed 12
3 1003 1 2026-01-07 returned 40
4 1004 3 2026-01-09 completed 8.75
5 1005 2 2026-01-11 pending 63.2
6 1006 9 2026-01-12 completed 19.99
Rows: 6
Columns: 5
$ order_id <dbl> 1001, 1002, 1003, 1004, 1005, 1006
$ customer_id <dbl> 1, 2, 1, 3, 2, 9
$ ordered_at <date> 2026-01-04, 2026-01-05, 2026-01-07, 2026-01-09, 2026-01-11, 2026-…
$ status <chr> "completed", "completed", "returned", "completed", "pending", "com…
$ amount <dbl> 19.99, 25.50, 12.00, 40.00, 8.75, 63.20
readr inferred the date column without being told. glimpse() is the function to reach for
on a wide table — it prints one row per column, so forty columns stay readable.
Guessing types is convenient and not what you want in a pipeline. Declare them:
orders <- read_csv(
"orders.csv",
col_types = cols(
order_id = col_integer(),
customer_id = col_integer(),
ordered_at = col_date(format = "%Y-%m-%d"),
status = col_factor(levels = c("completed", "returned", "refunded", "pending")),
amount = col_double()
)
)
print(orders)
# A tibble: 6 × 5
order_id customer_id ordered_at status amount
<int> <int> <date> <fct> <dbl>
1 1001 1 2026-01-04 completed 25.5
2 1002 2 2026-01-05 completed 12
...
Now a rogue value fails loudly instead of turning the column into text:
Warning: One or more parsing issues, call `problems()` on your data frame for details
# A tibble: 1 × 5
row col expected actual file
<int> <int> <chr> <chr> <chr>
1 5 5 a double n/a orders.csv
problems() gives you the row, the column, what was expected and what arrived — the same
information a good ingestion tool provides, and the reason to declare types.
The pipe
orders |>
filter(status == "completed") |>
mutate(amount_with_vat = round(amount * 1.20, 2)) |>
arrange(desc(amount)) |>
print()
# A tibble: 4 × 6
order_id customer_id ordered_at status amount amount_with_vat
<int> <int> <date> <fct> <dbl> <dbl>
1 1001 1 2026-01-04 completed 25.5 30.6
2 1006 9 2026-01-12 completed 19.99 24.0
3 1002 2 2026-01-05 completed 12 14.4
4 1004 3 2026-01-09 completed 8.75 10.5
|> is base R’s native pipe: x |> f(y) is f(x, y). You will also meet %>% from magrittr
in older code — near-identical for everyday use. Prefer |> in new code; it needs no package.
Practice
1. Build a tibble and read its column types.
customers <- tibble(
customer_id = 1:4,
full_name = c("Ada Lovelace", "Grace Hopper", "Alan Turing", "Katherine Johnson"),
country = c("GB", "US", "GB", "US")
)
print(customers)
# A tibble: 4 × 3
customer_id full_name country
<int> <chr> <chr>
1 1 Ada Lovelace GB
2 2 Grace Hopper US
3 3 Alan Turing GB
4 4 Katherine Johnson US
1:4 produced <int> while c(1, 2, 3, 4) gives <dbl> — R’s numeric literals are doubles
unless you write 1L or use a range.
2. Add two vectors of different lengths.
print(c(1, 2, 3, 4) + c(10, 20))
[1] 11 22 13 24
No warning, because 4 is a multiple of 2. Recycling is a feature for scalars — x * 2 relies
on it — and a silent bug generator for anything longer.
3. Sum a column containing NA.
print(sum(c(25.50, NA, 12.00)))
print(sum(c(25.50, NA, 12.00), na.rm = TRUE))
[1] NA
[1] 37.5
The NA result is the language refusing to guess. Reach for na.rm = TRUE deliberately, and
count the missing values rather than dropping them silently.
4. Declare column types and feed the reader a bad value.
Warning: One or more parsing issues, call `problems()` on your data frame for details
row col expected actual
1 5 5 a double n/a
With type guessing the whole column becomes <chr> and every later arithmetic operation
fails somewhere less obvious. Declaring types moves the failure to the boundary.
Next: dplyr — the five verbs that cover most transformation work.