R Foundations

This tutorial introduces R objects and vectors, rectangular data, transformation with dplyr, visualization with ggplot2, and reproducible project organization. Examples use a synthetic student dataset.

Interactive R examples

Edit a runnable example and select Run to execute it.

Reset code restores the original example. Each block runs independently. The first run downloads the browser-based R runtime and requires an internet connection. Long-running code cannot be interrupted; reload the page if execution stalls.

Chapter 1

Getting started with R

R is a programming language and statistical computing environment. RStudio is an integrated development environment (IDE) for editing scripts, running R, inspecting objects, and viewing plots.

Install R and RStudio

Runnable examples on this page require no local installation. File-based analyses and saved outputs use a local installation of R and RStudio.

  1. Install the current version of R from CRAN. Choose the download for your operating system and follow its installer.
  2. Install RStudio Desktop, then open it. R must be installed first.
  3. In RStudio, choose File → New Project → New Directory → New Project, choose a name and location, then select Create Project.
  4. Choose File → New File → R Script. Type 2 + 2, leave the cursor on that line, then click Run or press Cmd+Enter on macOS / Ctrl+Enter on Windows or Linux. You should see [1] 4 in the Console.
  5. Save the script as analysis.R. In the Files pane, use New Folder three times to create data, figures, and results inside the project.

Console and scripts

Use the Console for temporary commands and scripts as the reproducible record of an analysis.

Your first commands

RFirst commands
# Anything after # is a comment.
2 + 2

minutes <- 90
hours <- minutes / 60
hours

mean(c(3, 6, 9))
round(10 / 3, digits = 2)
Expected output for the original code[1] 4 [1] 1.5 [1] 6 [1] 3.33

The assignment arrow <- stores a value in an object. R usually prints nothing when an object is created. Type the object name to see its value. The [1] in printed output marks the position of the first displayed value; it is not part of the answer.

How to read a function call

round(10 / 3, digits = 2)
  • round is the function: the action R will perform.
  • The values inside parentheses are inputs, called arguments.
  • digits = 2 is a named argument that changes how the function behaves.
  • The returned value is the result.

Messages, warnings, and errors

Messages report context or progress. Warnings identify potential problems while evaluation continues; errors stop the current expression. Read the condition message before revising the code.

Chapter 2

Objects and data types

An object is a name that points to a value in memory. The value has a type, which tells R which operations make sense. Adding numbers makes sense; adding two student IDs does not.

Common R data types and classes
Type or class Purpose Example
logicalTrue or falseTRUE, FALSE
integerWhole number stored explicitly as an integer12L
doubleGeneral numeric value12, 3.14
characterText and identifiers"S001", "Biology"
factorCategories with defined levelsControl, Treatment
DateCalendar dateas.Date("2026-09-08")
RCreate and inspect objects
student_id <- "S001"
quiz_score <- 84
completed <- TRUE
lesson_date <- as.Date("2026-09-08")

class(student_id)
class(quiz_score)
class(completed)
class(lesson_date)

typeof(quiz_score)

class() reports the class used by most analysis methods; typeof() reports the underlying storage type.

Identifiers are usually text

Store an ID such as "S001" as character data. Arithmetic on an ID is meaningless, and converting it to a number can erase leading zeros.

Explicit type conversion

RConvert only when the meaning is clear
text_number <- "42"
as.numeric(text_number)

condition <- factor(
  c("Control", "Treatment", "Control"),
  levels = c("Control", "Treatment")
)

levels(condition)
Chapter 3

Vectors and missing values

A vector is an ordered, one-dimensional collection. Ordinary atomic vectors contain one type. Many R operations are vectorized: one instruction is applied to every element.

RCreate, inspect, and subset a vector
scores <- c(68, 84, NA, 93, 76)

length(scores)
scores[2]
scores[c(1, 4)]
scores[-3]

scores + 5
scores >= 80
Expected output for the original code[1] 5 [1] 84 [1] 68 93 [1] 68 84 93 76 [1] 73 89 NA 98 81 [1] FALSE TRUE NA TRUE FALSE

R indexing starts at 1. A positive index keeps a position; a negative index removes it. The missing element stays missing because R cannot determine the result of arithmetic or comparison for an unknown value.

Zero and missing are different

0
A known numeric value equal to zero.
NA
The value is unavailable or unknown.
"0"
A character string containing the symbol zero.
""
An empty character string; it is not automatically missing.
RWork with missing values deliberately
mean(scores)
mean(scores, na.rm = TRUE)

is.na(scores)
sum(is.na(scores))
sum(scores >= 80, na.rm = TRUE)
Expected output for the original code[1] NA [1] 80.25 [1] FALSE FALSE TRUE FALSE FALSE [1] 1 [1] 2

Do not test missingness with ==

NA == NA returns NA, not TRUE, because two unknown values cannot be proven equal. Use is.na(x). Count missing values before deciding whether to remove them.

Vector constructors

RThree ways to build sequences
1:5
seq(from = 0, to = 1, by = 0.25)
rep(c("A", "B"), times = 3)
Exercise: Convert 160, 172, and 168 centimeters to meters
SolutionVectorized division
height_cm <- c(160, 172, 168)
height_m <- height_cm / 100
height_m
# 1.60 1.72 1.68
Chapter 4

Datasets and importing data

In R, rectangular data are typically stored as data frames or tibbles. Rows represent observations, columns represent variables, and cells contain individual values.

Rows, columns, and cells in a rectangular dataset A small table where one row is highlighted as an observation, one column as a variable, and one intersection as a cell. student_id major hours score one row one column one cell
A cell belongs to both a row and a column. Its meaning comes from the observation identified by the row and the variable named by the column.

Download and import the practice file

Download r_student_scores.csv, place it inside your project’s data folder, and run the following code.

RDesktop R only · install and import
# Run this once on your computer:
install.packages("tidyverse")

# Run this at the beginning of each new session:
library(tidyverse)

students <- read_csv(
  "data/r_student_scores.csv",
  show_col_types = FALSE
)

Use project-relative paths

Project-relative paths remain valid when the project directory is moved or shared; user-specific absolute paths do not. Use getwd() to inspect the current working directory.

Inspect before analyzing

RThese commands do not change the data
students
glimpse(students)
dim(students)
names(students)
head(students, 3)
summary(students)

colSums(is.na(students))
anyDuplicated(students$student_id)

glimpse() shows column types and sample values. dim() should return 12 rows and 6 columns. anyDuplicated() returns 0 when no repeated ID is found. These checks should precede transformation and analysis.

Select values by name or position

RColumns, cells, and small tables
students$quiz_score
students[["quiz_score"]]
students$quiz_score[4]
students[1:3, c("student_id", "quiz_score")]

The first two forms return the same score vector. The third returns the fourth score, 93. The final command returns a small two-column table. Prefer column names over numeric column positions in an analysis script.

Chapter 5

Transform and summarize a dataset

The native pipe |> passes the result on its left to the next function call. Pipelines express transformations in execution order.

Core dplyr verbs
VerbQuestion it answers
select()Which columns do I need?
filter()Which rows meet a condition?
arrange()In what order should rows appear?
mutate()Which new or changed columns do I need?
summarise()Which compact summary should I calculate?
group_by()For which categories should I repeat that summary?
RRows, columns, and ordering
# Keep three columns.
students |>
  select(student_id, major, quiz_score)

# Keep rows where completed is TRUE and score is at least 80.
students |>
  filter(completed == TRUE, quiz_score >= 80)

# Highest quiz score first.
students |>
  arrange(desc(quiz_score))

Inside filter(), commas mean AND. Use | for OR and %in% to match any value in a set. select() changes columns; filter() changes rows.

  • The first result has 12 rows and only 3 columns.
  • The filter returns S002, S004, S007, S009, and S011.
  • The descending arrangement begins with S004, whose quiz score is 93.
RCreate variables and grouped summaries
students_prepared <- students |>
  mutate(
    study_minutes = study_hours * 60,
    major = factor(
      major,
      levels = c("Mathematics", "Biology", "Public Health")
    )
  )

major_summary <- students_prepared |>
  group_by(major) |>
  summarise(
    students = n(),
    completed = sum(completed == TRUE),
    mean_hours = mean(study_hours),
    mean_score = mean(quiz_score),
    .groups = "drop"
  )

major_summary

Assignment creates a new table named students_prepared; the original students table is unchanged. The expression completed == TRUE produces TRUE/FALSE values, and sum() counts the TRUE values when no completion values are missing.

Expected grouped summary
majorstudentscompleted mean_hoursmean_score
Mathematics443.82580.25
Biology433.05076.00
Public Health412.20066.75

Descriptive summaries do not establish causation

These twelve synthetic observations support only descriptive summaries. Differences in group means do not establish causal effects; inference requires an appropriate design, sampling framework, uncertainty quantification, and assessment of confounding.

Exercise: Keep Biology and Public Health students with attendance at least 80%
SolutionFilter, then select
students |>
  filter(
    major %in% c("Biology", "Public Health"),
    attendance_pct >= 80
  ) |>
  select(student_id, major, attendance_pct)
Chapter 6

Visualize with ggplot2

A ggplot is built in layers. You provide a dataset, map variables to visual properties inside aes(), choose a geometric layer, then add labels and a theme.

Conceptual patternPlaceholders — do not run
ggplot(DATA, aes(x = X_VARIABLE, y = Y_VARIABLE)) +
  geom_...() +
  labs(...) +
  theme_minimal()

Uppercase tokens and ellipses are placeholders; replace them with data, variables, layers, and labels.

Chart selection by question and variable type
QuestionVariablesStarting layer
How many observations are in each category?One categoricalgeom_bar()
How is one numeric variable distributed?One numericgeom_histogram()
How do two numeric variables relate?Two numericgeom_point()
How does a number vary across groups?Numeric + categoricalPoints or geom_boxplot()
How does a value change through ordered time?Time + numericgeom_line()

Scatterplot: two numeric variables

ROne point represents one student
score_plot <- ggplot(
  students,
  aes(
    x = study_hours,
    y = quiz_score,
    color = major,
    shape = completed
  )
) +
  geom_point(size = 3, alpha = 0.85) +
  labs(
    title = "Study time and quiz score",
    subtitle = "Synthetic data for code practice",
    x = "Study time (hours)",
    y = "Quiz score",
    color = "Major",
    shape = "Completed"
  ) +
  theme_minimal(base_size = 12)

score_plot

Position encodes study hours and score; color and shape encode major and completion status. Association in these synthetic data does not establish causation.

Mapping versus setting

aes(color = major) maps color to a variable and creates groups plus a legend. geom_point(color = "#315e7d") sets one fixed color for every point. A quoted color name inside aes() is usually a mistake.

Bar chart: count rows in categories

Rgeom_bar counts rows
ggplot(students, aes(x = major)) +
  geom_bar(fill = "#315e7d", width = 0.7) +
  labs(
    title = "Students in each major",
    x = NULL,
    y = "Number of students"
  ) +
  theme_minimal(base_size = 12)

Each bar has height 4 because each major appears in four rows. If you already have a summarized count column, use geom_col() instead.

Histogram and boxplot

RDistribution and grouped values
ggplot(students, aes(x = quiz_score)) +
  geom_histogram(binwidth = 5, boundary = 0) +
  labs(x = "Quiz score", y = "Number of students") +
  theme_minimal()

ggplot(students, aes(x = major, y = quiz_score)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter(width = 0.08, height = 0, alpha = 0.8) +
  labs(x = NULL, y = "Quiz score") +
  theme_minimal()

A histogram groups numeric values into intervals; changing binwidth changes the display. The boxplot summarizes a distribution, while jittered points keep the twelve observations visible. With such a small dataset, focus on the points and avoid strong conclusions.

Export a plot

RDesktop R only · export to a file
dir.create("figures", showWarnings = FALSE)

ggsave(
  filename = "figures/study-time-vs-score.png",
  plot = score_plot,
  width = 7,
  height = 4.5,
  dpi = 300
)
Exercise: Color points by completion and make one panel per major
SolutionMapping plus facets
ggplot(
  students,
  aes(x = attendance_pct, y = quiz_score, color = completed)
) +
  geom_point(size = 3) +
  facet_wrap(~ major) +
  labs(
    x = "Attendance (%)",
    y = "Quiz score",
    color = "Completed"
  ) +
  theme_minimal()
Chapter 7

Reproducible analysis workflow

A reproducible analysis regenerates its outputs from the same inputs and script in a fresh R session. Manual or undocumented steps prevent exact regeneration.

Recommended project structure

cycle-analysis/ ├── analysis.R ├── data/ │ └── r_student_scores.csv ├── figures/ └── results/
RDesktop R only · full project workflow
library(tidyverse)

# 1. Import
students <- read_csv(
  "data/r_student_scores.csv",
  show_col_types = FALSE
)

# 2. Inspect and validate
glimpse(students)
colSums(is.na(students))

stopifnot(nrow(students) > 0)
stopifnot(anyDuplicated(students$student_id) == 0)
stopifnot(!anyNA(students$quiz_score))
stopifnot(all(students$quiz_score >= 0))
stopifnot(all(students$quiz_score <= 100))

# 3. Prepare
students_clean <- students |>
  mutate(
    major = factor(
      major,
      levels = c("Mathematics", "Biology", "Public Health")
    )
  )

# 4. Summarize
major_summary <- students_clean |>
  group_by(major) |>
  summarise(
    students = n(),
    completed = sum(completed == TRUE),
    mean_score = mean(quiz_score),
    .groups = "drop"
  )

# 5. Visualize
p <- ggplot(
  students_clean,
  aes(x = study_hours, y = quiz_score, color = major)
) +
  geom_point(size = 3) +
  labs(
    title = "Study time and quiz score",
    subtitle = "Synthetic tutorial data",
    x = "Study time (hours)",
    y = "Quiz score",
    color = "Major"
  ) +
  theme_minimal(base_size = 12)

# 6. Save outputs
dir.create("results", showWarnings = FALSE)
dir.create("figures", showWarnings = FALSE)

write_csv(major_summary, "results/major-summary.csv")
ggsave(
  "figures/study-time-vs-score.png",
  plot = p,
  width = 7,
  height = 4.5,
  dpi = 300
)

# 7. Save package and R versions
capture.output(
  sessionInfo(),
  file = "results/session-info.txt"
)

Fresh-session test

  1. Save analysis.R.
  2. Restart R so the Environment is empty.
  3. Run the complete script from top to bottom.
  4. Confirm that the table and figure are recreated without manual editing.

If it works only before restarting, your earlier session contained an undocumented object or manual step.

Chapter 8

Exercises and reference

Complete each exercise before opening the supplied solution, and interpret each result in words.

Exercise 1: Find students scoring above 80 and order them from highest to lowest
SolutionFilter, arrange, select
students |>
  filter(quiz_score > 80) |>
  arrange(desc(quiz_score)) |>
  select(student_id, major, quiz_score)

The result has five students: S004, S011, S007, S002, and S009.

Exercise 2: Summarize attendance by completion status
SolutionGrouped summary
students |>
  group_by(completed) |>
  summarise(
    students = n(),
    mean_attendance = mean(attendance_pct),
    .groups = "drop"
  )

Expected result: FALSE has 4 students with mean attendance 68.5; TRUE has 8 students with mean attendance 88.125. Always report group counts with means, especially in a small dataset.

Exercise 3: Create and save an attendance-versus-score plot
SolutionDesktop R only · plot and export
attendance_plot <- ggplot(
  students,
  aes(x = attendance_pct, y = quiz_score, shape = completed)
) +
  geom_point(size = 3, color = "#315e7d") +
  labs(
    title = "Attendance and quiz score",
    subtitle = "Synthetic tutorial data",
    x = "Attendance (%)",
    y = "Quiz score",
    shape = "Completed"
  ) +
  theme_minimal(base_size = 12)

dir.create("figures", showWarnings = FALSE)

ggsave(
  "figures/attendance-vs-score.png",
  plot = attendance_plot,
  width = 7,
  height = 4.5,
  dpi = 300
)
Core syntax and functions
GoalFunction or syntax
Create an objectname <- value
Combine valuesc(...)
Inspect a tableglimpse(), head(), summary()
Find missing valuesis.na(), sum(is.na(x))
Choose columnsselect()
Choose rowsfilter()
Create a columnmutate()
Repeat a summary by categorygroup_by() then summarise()
Start a scatterplotggplot(data, aes(x, y)) + geom_point()
Get help?mean, help("mean")

Learning outcomes

  • the difference between R, RStudio, a script, and the Console;
  • why NA, 0, "0", and an empty string are not interchangeable;
  • the difference between select() and filter();
  • the difference between mapping and setting a ggplot property; and
  • why the entire script should run in a fresh session.

References and further reading