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.

1. Preparing the Example Data

1.1 Load packages and data

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.

2. Conditional Statements and Recoding

2.1 Flow control

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.

2.2 Conditional statements with ifelse()

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.

2.3 Recoding a continuous variable

Let’s recode Math_Overall into a categorical variable with three levels:

  • Score below 400: "Need Improvement"
  • Score from 400 up to but not including 600: "Mid Range"
  • Score 600 or above: "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"
)
Bar chart showing counts for Need Improvement, Mid Range, and Advanced Math Overall performance levels in the achievement dataset.

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.

2.4 Recoding a categorical variable

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".

3. Loops and apply()

3.1 Loops

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

3.2 Summaries for math scores

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

3.3 Using apply()

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."
)
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.

4. Defining Functions

4.1 Why define functions?

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:

  • Takes one or more inputs or arguments.
  • Performs some operations.
  • Returns an output.

4.2 Sum of squares function

ss_func <- function(a, b) {
  ss <- a^2 + b^2
  return(ss)
}

ss_func(3, 4)
## [1] 25

4.3 Range function

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

4.4 Function for summaries and histograms

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
Histogram generated by a custom function showing the distribution of Math Overall scores in the achievement dataset.

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.

5. Identifying Duplicates and Merging Datasets

5.1 Identifying duplicates

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

5.2 Removing duplicates by student ID

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

5.3 Merging datasets

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.

5.4 Example: merge achievement and survey data

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.

6. Further Learning

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.