Prepared by: Yuxiao Zhang
Maintained by: PUPIL Lab
Last updated: July 2026
Data note. The datasets included with this resource are small demonstration datasets provided for instructional practice. They are intended for learning R syntax, recoding, programming tools, duplicate checking, merging, and related examples in this handout.
This part uses the achievement dataset for programming examples and
the survey dataset for the merge example. Render or run this file from
inside the Part_3_Programming_Tools folder so the relative
paths work.
library(haven)
library(dplyr)
data <- read_sav("data/PISA_Achievement.sav")
survey_data <- read_sav("data/PISA_Survey.sav")
dim(data)
## [1] 178 13
dim(survey_data)
## [1] 177 12
The achievement file contains math scores and school/student
identifiers. The survey file contains survey variables that can be
merged with the achievement file by Stu_ID.
Flow control means deciding what the program should do next based on conditions or patterns in the data. In R, the most common types of flow control are conditional statements for making decisions and loops for repeating actions.
The simplest conditional function in R is ifelse(),
which allows R to test a condition and take one of two actions.
General syntax:
ifelse(test, A, B): check a condition; if it is met, do
A; otherwise, do B.
x <- 5
ifelse(x > 0, "Positive", "Non-positive")
## [1] "Positive"
x <- -2:2
ifelse(x > 0, "Positive", "Non-positive")
## [1] "Non-positive" "Non-positive" "Non-positive" "Positive" "Positive"
Note that ifelse() works element-wise for vectors, which
makes it very useful for recoding variables.
Let’s recode Math_Overall into a categorical variable
with three levels:
"Need Improvement""Mid Range""Advanced"data$Math_level <- ifelse(
data$Math_Overall < 400,
"Need Improvement",
ifelse(data$Math_Overall < 600, "Mid Range", "Advanced")
)
data[1:20, c("Math_Overall", "Math_level")]
## # A tibble: 20 × 2
## Math_Overall Math_level
## <dbl> <chr>
## 1 277. Need Improvement
## 2 399. Need Improvement
## 3 414. Mid Range
## 4 305. Need Improvement
## 5 485. Mid Range
## 6 387. Need Improvement
## 7 329. Need Improvement
## 8 297. Need Improvement
## 9 432. Mid Range
## 10 451. Mid Range
## 11 288. Need Improvement
## 12 335. Need Improvement
## 13 354. Need Improvement
## 14 499. Mid Range
## 15 478. Mid Range
## 16 721. Advanced
## 17 419. Mid Range
## 18 477. Mid Range
## 19 489. Mid Range
## 20 627. Advanced
table(data$Math_level)
##
## Advanced Mid Range Need Improvement
## 20 104 52
Here we nest one ifelse() inside another to handle
three possible outcomes. R checks the first condition,
and if it is not true, it moves to the next ifelse().
math_level_counts <- table(data$Math_level)
barplot(
math_level_counts,
main = "Math Level Distribution",
xlab = "Math Level",
ylab = "Frequency",
col = "gray70",
border = "gray20"
)
Figure 1. Bar chart of recoded Math Overall performance levels.
The bar chart shows how many students fall into each recoded performance level. In this example, most students are in the middle performance range.
The Sch_Type variable has three levels: private
independent, private government-dependent, and public. We want a simpler
variable with only two levels: private and public.
table(as_factor(data$Sch_Type))
##
## Public Private Government-dependent
## 156 8
## Private independent
## 11
data$Sch_Type_2g <- ifelse(
as_factor(data$Sch_Type) == "Public",
"Public",
"Private"
)
data[1:20, c("Sch_Type", "Sch_Type_2g")]
## # A tibble: 20 × 2
## Sch_Type Sch_Type_2g
## <chr> <chr>
## 1 Public Public
## 2 Public Public
## 3 Public Public
## 4 Public Public
## 5 Public Public
## 6 Public Public
## 7 Private Government-dependent Private
## 8 Public Public
## 9 Public Public
## 10 Public Public
## 11 Public Public
## 12 Public Public
## 13 Private independent Private
## 14 Public Public
## 15 Public Public
## 16 Public Public
## 17 Private independent Private
## 18 Public Public
## 19 Public Public
## 20 Public Public
table(data$Sch_Type_2g)
##
## Private Public
## 19 156
Here, the condition checks whether the school type is public. If
true, assign "Public"; otherwise, assign
"Private".
A loop repeats a block of code multiple times. In R, the most common
loop is the for loop.
General syntax:
for (variable in sequence) { code to be repeated }
for (i in 1:5) {
print(i^2)
}
## [1] 1
## [1] 4
## [1] 9
## [1] 16
## [1] 25
We will compute summary statistics for Math_Overall and
selected math sub-scores. The achievement dataset stores
Math_Overall in column 5 and the math sub-scores in columns
6 through 13.
Note:
data[, 5] extracts the fifth column as a single-column
data frame.data$Math_Overall extracts the column with the
specified name as a vector.data[[5]] extracts the fifth column as a vector.data[, 5] |> summary()
## Math_Overall
## Min. :242.0
## 1st Qu.:386.5
## Median :462.8
## Mean :467.0
## 3rd Qu.:526.8
## Max. :819.5
## NA's :2
data[[5]] |> summary()
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 242.0 386.5 462.8 467.0 526.8 819.5 2
We will use data[[i]] to extract each column as a vector
inside the loop.
for (i in 5:7) {
print(colnames(data)[i])
data[[i]] |> summary() |> print()
}
## [1] "Math_Overall"
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 242.0 386.5 462.8 467.0 526.8 819.5 2
## [1] "Math_Reason"
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 201.9 390.9 457.1 464.5 525.1 827.4
## [1] "Math_Relation"
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 244.3 399.8 465.3 471.9 529.1 799.1
The apply() function provides a more compact way to
repeat an operation across rows or columns. It performs implicit
looping: R handles the repetition internally, so you do not need to
write a for loop yourself.
General syntax:
apply(data, margin, function, ...)
data: A matrix or data frame.margin: 1 for rows or 2 for
columns.function: The function to be applied.summary_matrix <- apply(data[, 5:13], 2, summary)
summary_table <- data.frame(
Variable = colnames(data[, 5:13]),
Min = apply(data[, 5:13], 2, min, na.rm = TRUE),
Q1 = apply(data[, 5:13], 2, quantile, probs = 0.25, na.rm = TRUE),
Median = apply(data[, 5:13], 2, median, na.rm = TRUE),
Mean = apply(data[, 5:13], 2, mean, na.rm = TRUE),
Q3 = apply(data[, 5:13], 2, quantile, probs = 0.75, na.rm = TRUE),
Max = apply(data[, 5:13], 2, max, na.rm = TRUE),
Missing = apply(data[, 5:13], 2, function(x) sum(is.na(x))),
row.names = NULL
)
knitr::kable(
summary_table,
digits = 2,
caption = "Table 1. Summary statistics for achievement score variables."
)
| Variable | Min | Q1 | Median | Mean | Q3 | Max | Missing |
|---|---|---|---|---|---|---|---|
| Math_Overall | 241.95 | 386.49 | 462.80 | 466.96 | 526.79 | 819.51 | 2 |
| Math_Reason | 201.86 | 390.87 | 457.13 | 464.50 | 525.07 | 827.37 | 0 |
| Math_Relation | 244.31 | 399.76 | 465.30 | 471.90 | 529.08 | 799.06 | 0 |
| Math_Quantity | 242.05 | 390.99 | 457.95 | 466.09 | 539.27 | 832.19 | 0 |
| Math_Space | 142.38 | 382.04 | 444.25 | 455.48 | 513.53 | 769.39 | 0 |
| Math_Data | 232.35 | 391.26 | 464.86 | 479.39 | 551.45 | 872.75 | 0 |
| Math_Apply | 181.44 | 375.55 | 457.51 | 462.01 | 522.63 | 792.62 | 0 |
| Math_Solve | 220.90 | 387.72 | 445.51 | 463.20 | 518.39 | 790.75 | 0 |
| Math_Interpret | 234.39 | 396.24 | 472.28 | 476.76 | 544.43 | 805.10 | 0 |
The table summarizes the range, center, spread, and missingness for each achievement score variable in a more readable format than a wide console printout.
Defining your own functions in R lets you apply the same set of commands to different inputs without rewriting code each time. This makes your code cleaner, more modular, and reusable. It is an important step toward professional and efficient programming.
General syntax:
my_function <- function(arg1, arg2, ...) {
# Code to be executed
return(output)
}
A function typically:
ss_func <- function(a, b) {
ss <- a^2 + b^2
return(ss)
}
ss_func(3, 4)
## [1] 25
You can also use existing R functions inside your custom function.
range_func <- function(x) {
range <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)
return(range)
}
a <- c(3, 5, 1, 9, 7)
range_func(a)
## [1] 8
range_func(data$Math_Overall)
## [1] 577.555
A function can also include multiple commands. Here we create one that prints summary statistics and generates a histogram for a given variable.
summary_hist_func <- function(x) {
print(summary(x))
hist(
x,
main = "Histogram of Math Overall",
xlab = "Math Overall",
col = "gray70",
border = "gray20"
)
}
summary_hist_func(data$Math_Overall)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 242.0 386.5 462.8 467.0 526.8 819.5 2
Figure 2. Histogram of Math Overall scores produced by a custom function.
This function prints the summary statistics and creates a histogram. The histogram again shows that most math overall scores are in the middle of the observed range.
If we pass the variable as a one-column data frame, we can extract the variable name automatically for labeling the summary and plot.
summary_hist_func <- function(x) {
var_name <- names(x)
print(var_name)
x <- x[[1]]
print(summary(x))
hist(
x,
main = paste("Histogram of", var_name),
xlab = "Values",
col = "gray70",
border = "gray20"
)
}
summary_hist_func(data["Math_Overall"])
Key idea: Defining your own function allows you to bundle a set of related operations into one reusable unit. This is called modular programming: breaking your analysis into smaller, logical components that can be reused across different datasets or research projects.
R provides many built-in functions to help with everyday data
cleaning tasks. The duplicated() function checks for
repeated values in a vector or data frame. It returns a logical vector,
TRUE or FALSE, indicating which elements or
rows are duplicates.
x <- c(1, 3, 3, 4, 5, 7, 7, 7)
duplicated(x)
## [1] FALSE FALSE TRUE FALSE FALSE FALSE TRUE TRUE
x[!duplicated(x)]
## [1] 1 3 4 5 7
By default, the function marks the first occurrence of each duplicate
as the primary case and subsequent occurrences as duplicates. You can
change this behavior using the optional argument fromLast;
if TRUE, it keeps the last occurrence instead of the
first.
duplicated(x, fromLast = TRUE)
## [1] FALSE TRUE FALSE FALSE FALSE TRUE TRUE FALSE
The dataset has multiple records for some students. We can remove
duplicates using the Stu_ID column as the unique
identifier.
table(duplicated(data$Stu_ID))
##
## FALSE TRUE
## 176 2
data_unique <- data[!duplicated(data$Stu_ID), ]
data_unique_last <- data[!duplicated(data$Stu_ID, fromLast = TRUE), ]
table(duplicated(data_unique$Stu_ID))
##
## FALSE
## 176
The merge() function combines two data frames based on
one or more shared key variables, such as Stu_ID.
General syntax:
merge(x, y, by, all)
x, y: Data frames to be merged.by: The common column or columns used for
matching.all: Controls which cases to keep. TRUE
keeps all rows from both data frames, and FALSE keeps only
rows present in both data frames.survey_for_merge <- survey_data |>
select(-Sch_ID, -Gender, -Sch_Type)
merged_data <- merge(data_unique, survey_for_merge, by = "Stu_ID", all = TRUE)
dim(data)
## [1] 178 15
dim(survey_data)
## [1] 177 12
dim(merged_data)
## [1] 177 23
The merged dataset keeps achievement variables and survey variables together by student ID. Because the source files have slightly different numbers of cases, the merged dataset should be reviewed before analysis.
Outcome: You should now understand how to use flow control, including conditional statements and loops, and how to define your own functions in R. You should also be familiar with common data management tasks like identifying duplicates and merging datasets.
Use these resources to continue learning:
Next Step
Use these examples as a foundation for building reproducible R scripts for your own educational research projects.